{"thread":{"id":"64568","subject":"[PATCH] lockfile: add PID file for debugging stale locks","startedAt":"2025-12-02T15:07:30Z","lastAt":"2026-02-06T19:31:21Z","messageCount":36,"participants":["Paulo Casaretto via GitGitGadget","D. Ben Knoble","Torsten Bögershausen","Jeff King","Junio C Hamano","Taylor Blau","Patrick Steinhardt","Ben Knoble","Eric Sunshine","Johannes Sixt"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"531560","messageId":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":null,"subject":"[PATCH] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-02T15:07:27Z","receivedAt":"2025-12-02T15:07:30Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion .lock.pid\nfile alongside each lock file, containing the PID of the lock holder.\n\nThe .lock.pid file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks if the PID from the .pid\nfile is still running using kill(pid, 0). This allows providing\ncontext-aware error messages. With PID info enabled:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nWithout PID info (default):\n\n  Another git process seems to be running in this repository.\n  Wait for it to finish, or remove the lock file to continue.\n\nThe feature is opt-in via GIT_LOCK_PID_INFO=1 environment variable.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\n Documentation/git.adoc  |  10 ++++\n lockfile.c              | 109 +++++++++++++++++++++++++++++++++++++---\n lockfile.h              |  21 +++++---\n t/meson.build           |   1 +\n t/t0031-lockfile-pid.sh |  73 +++++++++++++++++++++++++++\n 5 files changed, 199 insertions(+), 15 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/git.adoc b/Documentation/git.adoc\nindex ce099e78b8..6fdd509d34 100644\n--- a/Documentation/git.adoc\n+++ b/Documentation/git.adoc\n@@ -1010,6 +1010,16 @@ be in the future).\n \tthe background which do not want to cause lock contention with\n \tother operations on the repository.  Defaults to `1`.\n \n+`GIT_LOCK_PID_INFO`::\n+\tIf this Boolean environment variable is set to `1`, Git will create\n+\ta `.lock.pid` file alongside each lock file containing the PID of the\n+\tprocess that created the lock. This information is displayed in error\n+\tmessages when a lock conflict occurs, making it easier to identify\n+\tstale locks or debug locking issues. The PID files are automatically\n+\tcleaned up via signal and atexit handlers; however, if a process is\n+\tterminated abnormally (e.g., SIGKILL), the file may remain as a stale\n+\tindicator. Disabled by default.\n+\n `GIT_REDIRECT_STDIN`::\n `GIT_REDIRECT_STDOUT`::\n `GIT_REDIRECT_STDERR`::\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..4a694b9c3d 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,6 +74,62 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a .lock.pid file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ */\n+\n+static int lock_pid_info_enabled(void)\n+{\n+\treturn git_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *lock_path, int mode)\n+{\n+\tstruct strbuf pid_path = STRBUF_INIT;\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd;\n+\n+\tif (!lock_pid_info_enabled())\n+\t\treturn NULL;\n+\n+\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n+\tfd = open(pid_path.buf, O_WRONLY | O_CREAT | O_TRUNC, mode);\n+\tif (fd >= 0) {\n+\t\tstrbuf_addf(&content, \"%\"PRIuMAX\"\\n\", (uintmax_t)getpid());\n+\t\tif (write_in_full(fd, content.buf, content.len) < 0)\n+\t\t\twarning_errno(_(\"could not write lock pid file '%s'\"),\n+\t\t\t\t      pid_path.buf);\n+\t\tclose(fd);\n+\t\tpid_tempfile = register_tempfile(pid_path.buf);\n+\t}\n+\tstrbuf_release(&content);\n+\tstrbuf_release(&pid_path);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *lock_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf pid_path = STRBUF_INIT;\n+\tstruct strbuf content = STRBUF_INIT;\n+\tint ret = -1;\n+\n+\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n+\tif (strbuf_read_file(&content, pid_path.buf, LOCK_PID_MAXLEN) > 0) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(content.buf, &endptr, 10);\n+\t\tif (*pid_out > 0 && (*endptr == '\\n' || *endptr == '\\0'))\n+\t\t\tret = 0;\n+\t\telse\n+\t\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path.buf);\n+\t}\n+\tstrbuf_release(&pid_path);\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n \t\t     int mode)\n@@ -80,9 +139,12 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,\n \tstrbuf_addstr(&filename, path);\n \tif (!(flags & LOCK_NO_DEREF))\n \t\tresolve_symlink(&filename);\n-\n \tstrbuf_addstr(&filename, LOCK_SUFFIX);\n+\n \tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(filename.buf, mode);\n+\n \tstrbuf_release(&filename);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n@@ -151,13 +213,36 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tstrbuf_addf(&lock_path, \"%s%s\", absolute_path(path), LOCK_SUFFIX);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\tif (lock_pid_info_enabled() &&\n+\t\t    !read_lock_pid(lock_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0)\n+\t\t\t\tpid_status = 1;\n+\t\t\telse\n+\t\t\t\tpid_status = -1;\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\"PRIuMAX\". \"\n+\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"),\n+\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\"PRIuMAX\", \"\n+\t\t\t    \"which is no longer running. Remove the stale lock file to continue\"),\n+\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n+\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"));\n+\n+\t\tstrbuf_release(&lock_path);\n \t} else\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n@@ -207,6 +292,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +303,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..b2d5a1bc6c 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,15 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/* Suffix for PID file that stores PID of lock holder: */\n+#define LOCK_PID_SUFFIX \".pid\"\n+#define LOCK_PID_SUFFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/* Environment variable to enable lock PID info (default: disabled) */\n+#define GIT_LOCK_PID_INFO_ENVIRONMENT \"GIT_LOCK_PID_INFO\"\n \n /*\n  * Flags\n@@ -319,13 +329,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/t/meson.build b/t/meson.build\nindex dc43d69636..9a880db57c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..3c1e9bf60e\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,73 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via GIT_LOCK_PID_INFO=1.\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\techo \"99999\" >.git/index.lock.pid &&\n+\t\ttest_must_fail env GIT_LOCK_PID_INFO=1 git add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"Remove the stale lock file\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\techo \"99999\" >.git/index.lock.pid &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\techo $$ >.git/index.lock.pid &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail env GIT_LOCK_PID_INFO=1 git add file 2>err &&\n+\t\ttest_grep \"held by process $$\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tenv GIT_LOCK_PID_INFO=1 git add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\t! test -f .git/index.lock &&\n+\t\t! test -f .git/index.lock.pid\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\t! test -f .git/index.lock.pid\n+\t)\n+'\n+\n+test_done\n\nbase-commit: 6ab38b7e9cc7adafc304f3204616a4debd49c6e9\n-- \ngitgitgadget\n"},{"id":"531592","messageId":"CALnO6CB1igUL7nv6ByUmwMRc9tqEvs=18wD81GNpaA=FLpL2vw@mail.gmail.com","threadId":"64568","inReplyTo":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-02T22:29:40Z","receivedAt":"2025-12-02T22:29:53Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Dec 2, 2025 at 10:07 AM Paulo Casaretto via GitGitGadget\n<gitgitgadget@gmail.com> wrote:\n>\n> From: Paulo Casaretto <pcasaretto@gmail.com>\n>\n> When a lock file is held, it can be helpful to know which process owns\n> it, especially when debugging stale locks left behind by crashed\n> processes. Add an optional feature that creates a companion .lock.pid\n> file alongside each lock file, containing the PID of the lock holder.\n>\n> The .lock.pid file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n>\n> When a lock conflict occurs, the code checks if the PID from the .pid\n> file is still running using kill(pid, 0). This allows providing\n> context-aware error messages. With PID info enabled:\n>\n>   Lock is held by process 12345. Wait for it to finish, or remove\n>   the lock file to continue.\n>\n> Or for a stale lock:\n>\n>   Lock was held by process 12345, which is no longer running.\n>   Remove the stale lock file to continue.\n>\n> Without PID info (default):\n>\n>   Another git process seems to be running in this repository.\n>   Wait for it to finish, or remove the lock file to continue.\n>\n> The feature is opt-in via GIT_LOCK_PID_INFO=1 environment variable.\n>\n> Signed-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n\nSounds interesting. I think by the time I wish I knew what else was\nusing the lockfile, it's too late for me to alter my environment.\nPerhaps (in addition to allowing the environment opt-in) we could\nopt-in via configuration? Or is this really only useful, say, on the\nserver side where the environment is carefully controlled? I don't\nrelish putting this variable into my environment to take advantage of\nsomething that looks very useful.\n\nAre there downsides that make it necessary to be opt-in? I also\nimagine this could be a useful default; occasionally folks at work hit\nsomething similar and ask \"what's up with that?\"\n\nOnly other thing is: just because a process X is running doesn't mean\nit was the one holding the lock, right? Since PIDs can be reused.\n\n-- \nD. Ben Knoble\n"},{"id":"531633","messageId":"20251203194809.GA13919@tb-raspi4","threadId":"64568","inReplyTo":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Torsten Bögershausen","fromEmail":"tboegi@web.de","sentAt":"2025-12-03T19:48:09Z","receivedAt":"2025-12-03T19:48:15Z","isPatch":true,"sender":{"key":"tboegi@web.de","avatar":"https://avatars.githubusercontent.com/u/7138363?v=4"},"body":"On Tue, Dec 02, 2025 at 03:07:27PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> From: Paulo Casaretto <pcasaretto@gmail.com>\n> \n> When a lock file is held, it can be helpful to know which process owns\n> it, especially when debugging stale locks left behind by crashed\n> processes. Add an optional feature that creates a companion .lock.pid\n> file alongside each lock file, containing the PID of the lock holder.\n> \n> The .lock.pid file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n> \n> When a lock conflict occurs, the code checks if the PID from the .pid\n> file is still running using kill(pid, 0). This allows providing\n> context-aware error messages. With PID info enabled:\n> \n>   Lock is held by process 12345. Wait for it to finish, or remove\n>   the lock file to continue.\n> \n> Or for a stale lock:\n> \n>   Lock was held by process 12345, which is no longer running.\n>   Remove the stale lock file to continue.\n> \n> Without PID info (default):\n> \n>   Another git process seems to be running in this repository.\n>   Wait for it to finish, or remove the lock file to continue.\n> \n> The feature is opt-in via GIT_LOCK_PID_INFO=1 environment variable.\n[]\n\nI think that this makes sense.\nHowever, as a frequent user of Git repos hosted on an NFS server\n(without any problems in my setup):\n\nDoes it make sense to add the hostname here ?\nWe already have xgethostname() in Git, so that we can diagnose\nwho/which machine really left a lock.\n\n[]\n"},{"id":"531635","messageId":"20251203211610.GA64204@coredump.intra.peff.net","threadId":"64568","inReplyTo":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-12-03T21:16:10Z","receivedAt":"2025-12-03T21:16:12Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Dec 02, 2025 at 03:07:27PM +0000, Paulo Casaretto via GitGitGadget wrote:\n\n> The .lock.pid file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n\nI'm sympathetic to the goal of this series, and the implementation looks\ncleanly done. But I wonder if there might be some system-level side\neffects that make these .pid files awkward.\n\nTemporarily having an extra .git/index.lock.pid file is probably not a\nbig deal. But for other namespaces, like refs, we're colliding with\nnames that have other meanings. So if we want to update refs/heads/foo,\nfor example, we'll create refs/heads/foo.lock now. And after your patch,\nalso refs/heads/foo.lock.pid.\n\nThe \".lock\" suffix is special, in that we disallow it in a refname and\nknow to skip it when iterating over loose refs. But for the \".pid\"\nvariant, we run the risk of colliding with a real branch named\n\"foo.lock.pid\", both for reading and writing.\n\nOn the writing side, creating foo.lock.pid may accidentally overwrite an\nexisting branch. This can be mitigated by using O_EXCL when creating the\npid file.\n\nBut we can see the writes in the opposite order, which I think can also\nlead to data loss. Something like:\n\n  - process A wants to write branch \"foo\", so it holds\n    refs/heads/foo.lock and now also the matching foo.lock.pid\n\n  - process B wants to write branch \"foo.lock.pid\", so it holds\n    refs/heads/foo.lock.pid.lock (and the matching pid)\n\n  - process B finishes its write and atomically renames\n    foo.lock.pid.lock into foo.lock.pid. It's expected to overwrite\n    the existing file there, so now process A's pid file is gone.\n\n  - process A finishes its write and tries to clean up its pid file.  So\n    it deletes foo.lock.pid, which contains the actual data from process\n    B's write.\n\nAnd now process B's write has been lost (and maybe even the entire\nexistence of the foo.lock.pid branch, if it was not also present in the\npacked-refs file).\n\nOn the reading side, anybody iterating refs/heads/ may racily see the\nfoo.lock.pid file and think it is supposed to be an actual ref. So they\nopen it, find it contains garbage, and then flag it as an error.\n\nI think both could be mitigated if we disallowed \".lock.pid\" as a suffix\nin refnames, but that is a big user-facing change.\n\nIn the long run, alternate ref stores like reftable won't suffer from\nthis issue. It's possible there are other spots where we use lockfiles\nalongside arbitrarily-named files, though I couldn't think of any.\n\nSo I dunno what that means for your patch. I notice that the user has to\nenable the feature manually. But it feels more like it should be\nselective based on which subsystem is using the lockfile (so refs would\nnever want it, but other lockfiles/tempfiles might).\n\n-Peff\n"},{"id":"531637","messageId":"xmqqsedr5hrc.fsf@gitster.g","threadId":"64568","inReplyTo":"20251203211610.GA64204@coredump.intra.peff.net","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-03T22:21:11Z","receivedAt":"2025-12-03T22:21:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> So I dunno what that means for your patch. I notice that the user has to\n> enable the feature manually. But it feels more like it should be\n> selective based on which subsystem is using the lockfile (so refs would\n> never want it, but other lockfiles/tempfiles might).\n\nOr perhaps the way to opt into the feature is to create an empty\nfile $GIT_DIR/lockfile-audit, and the lockfile subsystem will append\nto it every time a lock is taken?  We need to ensure that a PID and\npathname formatted into a single record is small enough and O_APPEND\nwould relieve us from worrying about multi writer races, which may\nintroduce different kind of complications, though.\n\n\n"},{"id":"531638","messageId":"20251203223220.GA66584@coredump.intra.peff.net","threadId":"64568","inReplyTo":"xmqqsedr5hrc.fsf@gitster.g","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-12-03T22:32:20Z","receivedAt":"2025-12-03T22:32:22Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Dec 03, 2025 at 02:21:11PM -0800, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > So I dunno what that means for your patch. I notice that the user has to\n> > enable the feature manually. But it feels more like it should be\n> > selective based on which subsystem is using the lockfile (so refs would\n> > never want it, but other lockfiles/tempfiles might).\n> \n> Or perhaps the way to opt into the feature is to create an empty\n> file $GIT_DIR/lockfile-audit, and the lockfile subsystem will append\n> to it every time a lock is taken?  We need to ensure that a PID and\n> pathname formatted into a single record is small enough and O_APPEND\n> would relieve us from worrying about multi writer races, which may\n> introduce different kind of complications, though.\n\nI like a single log much better from a management perspective. I agree\nthat atomicity is a potential issue, though. I think that even if we\nkept it small, network filesystems like NFS do not provide great\nguarantees for atomic appends. Something like flock() can work there,\nbut that's not something we've relied on before.\n\nIt also raises questions about reading (do we find pid files in the log\nin order to provide more directed advice?) and maintenance (do we ever\nclean it up, or just let it grow without bound?).\n\n-Peff\n"},{"id":"531639","messageId":"aTDFks3RW57Ytwvq@nand.local","threadId":"64568","inReplyTo":"20251203211610.GA64204@coredump.intra.peff.net","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-12-03T23:19:46Z","receivedAt":"2025-12-03T23:19:49Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Wed, Dec 03, 2025 at 04:16:10PM -0500, Jeff King wrote:\n> On Tue, Dec 02, 2025 at 03:07:27PM +0000, Paulo Casaretto via GitGitGadget wrote:\n>\n> > The .lock.pid file is created when a lock is acquired (if enabled), and\n> > automatically cleaned up when the lock is released (via commit or\n> > rollback). The file is registered as a tempfile so it gets cleaned up\n> > by signal and atexit handlers if the process terminates abnormally.\n>\n> I'm sympathetic to the goal of this series, and the implementation looks\n> cleanly done. But I wonder if there might be some system-level side\n> effects that make these .pid files awkward.\n>\n> Temporarily having an extra .git/index.lock.pid file is probably not a\n> big deal. But for other namespaces, like refs, we're colliding with\n> names that have other meanings. So if we want to update refs/heads/foo,\n> for example, we'll create refs/heads/foo.lock now. And after your patch,\n> also refs/heads/foo.lock.pid.\n>\n> The \".lock\" suffix is special, in that we disallow it in a refname and\n> know to skip it when iterating over loose refs. But for the \".pid\"\n> variant, we run the risk of colliding with a real branch named\n> \"foo.lock.pid\", both for reading and writing.\n\nGood point. I don't have a strong opinion on whether or not we should\nuse an append-only log of which PIDs grabbed which lockfiles when versus\ntracking them on a per-lock basis. But I wonder if this would be\nmitigated by either:\n\n - Keeping the \".lock\" suffix as-is, so that holding a lockfile at path\n   \"$GIT_DIR/index.lock\" would create \"$GIT_DIR/index-pid.lock\" or\n   something similar.\n\n - Introducing a new reference name constraint that treats \".lock.pid\"\n   as a reserved in a manner identical to how we currently treat\n   \".lock\".\n\nBetween the two, I vastly prefer the former, but see below for more on\nwhy.\n\n> But we can see the writes in the opposite order, which I think can also\n> lead to data loss. Something like:\n>\n>   - process A wants to write branch \"foo\", so it holds\n>     refs/heads/foo.lock and now also the matching foo.lock.pid\n>\n>   - process B wants to write branch \"foo.lock.pid\", so it holds\n>     refs/heads/foo.lock.pid.lock (and the matching pid)\n\nChanging the naming scheme as above would cause us to hold\n\"foo.pid.lock\" in addition to \"foo.lock\". That would allow process B\nhere to write branch \"foo.lock.pid\" (as is the case today). But if the\nscenario were instead \"process B wants to write branch foo.pid.lock\", it\nwould fail immediately since the \".lock\" suffix is reserved.\n\n> I think both could be mitigated if we disallowed \".lock.pid\" as a suffix\n> in refnames, but that is a big user-facing change.\n\nYeah, I don't think that we should change the refname constraints here,\nespecially in a world where reftable deployments are more common. In\nthat world I think we should err on the side of removing constraints,\nnot adding them ;-).\n\n> So I dunno what that means for your patch. I notice that the user has to\n> enable the feature manually. But it feels more like it should be\n> selective based on which subsystem is using the lockfile (so refs would\n> never want it, but other lockfiles/tempfiles might).\n\nYeah, I think that something similar to the \"which files do we fsync()\nand how?\" configuration we have today would be a nice complement here.\n\n(As an aside, I wonder if that interface, too, could be slightly\nimproved. Right now we have a comma-separated list of values in the\n\"core.fsync\" configuration for listing different \"components\", and then\na global core.fsyncMethod to either issue a fsync(), or a pagecache\nwriteback, or writeout-only flushes in batches. It might be nice to have\nsomething like:\n\n  [fsync \"loose-object\"]\n    method = fsync\n  [fsync \"packfile\"]\n    method = writeout\n\n, so the analog here would be something like:\n\n  [lockfile \"refs\"]\n    pidfile = false\n  [lockfile \"index\"]\n    pidfile = true\n\nor similar. That could also be represented as core.lockfile=index,\nomitting \"refs\" to avoid tracking it. It may be that people don't really\ncare to ever use different fsync methods for different fsync-able\ncomponents, so perhaps the analogy doesn't hold up perfectly.)\n\nThanks,\nTaylor\n"},{"id":"531640","messageId":"aTDKK6Ibcx35q3Tz@nand.local","threadId":"64568","inReplyTo":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2025-12-03T23:39:23Z","receivedAt":"2025-12-03T23:39:26Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"Hi Paulo,\n\nOn Tue, Dec 02, 2025 at 03:07:27PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> The .lock.pid file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n\n(For the purposes of this review, I'll ignore the naming conventions\nthat are discussed elsewhere in the thread, which I think can be\nresolved separately of any technical concerns.)\n\n> diff --git a/Documentation/git.adoc b/Documentation/git.adoc\n> index ce099e78b8..6fdd509d34 100644\n> --- a/Documentation/git.adoc\n> +++ b/Documentation/git.adoc\n> @@ -1010,6 +1010,16 @@ be in the future).\n>  \tthe background which do not want to cause lock contention with\n>  \tother operations on the repository.  Defaults to `1`.\n>\n> +`GIT_LOCK_PID_INFO`::\n> +\tIf this Boolean environment variable is set to `1`, Git will create\n> +\ta `.lock.pid` file alongside each lock file containing the PID of the\n> +\tprocess that created the lock. This information is displayed in error\n> +\tmessages when a lock conflict occurs, making it easier to identify\n> +\tstale locks or debug locking issues. The PID files are automatically\n> +\tcleaned up via signal and atexit handlers; however, if a process is\n> +\tterminated abnormally (e.g., SIGKILL), the file may remain as a stale\n> +\tindicator. Disabled by default.\n\nRegardless of whether or not we expose this functionality behind an\nenvironment variable or configuration, I think it would be nice to be\nable to turn PID tracking on and off for different components (e.g., for\nscenarios where you care about who is holding open, say, $GIT_DIR/index,\nbut not who is creating a lock during ref creation).\n\nIf we determined this through an environment variable, I think it would\nbe reasonable to adopt the convention from the \"core.fsync\"\nconfiguration and use a comma-separated list. Alternatively, we could\nadopt that same convention for a configuration variable, say,\n\"core.lockfile\".\n\nBut I think that having this exposed as a per-component setting via\nconfiguration is preferable than a global switch, since callers don't\nhave to remember to set this variable in their environment to get the\ndesired effect. Callers that want to opt-in or out of this feature on a\none-off basis can still override the configuration via the top-level\n\"-c\" flag.\n\n>  `GIT_REDIRECT_STDIN`::\n>  `GIT_REDIRECT_STDOUT`::\n>  `GIT_REDIRECT_STDERR`::\n> diff --git a/lockfile.c b/lockfile.c\n> index 1d5ed01682..4a694b9c3d 100644\n> --- a/lockfile.c\n> +++ b/lockfile.c\n> @@ -6,6 +6,9 @@\n>  #include \"abspath.h\"\n>  #include \"gettext.h\"\n>  #include \"lockfile.h\"\n> +#include \"parse.h\"\n> +#include \"strbuf.h\"\n> +#include \"wrapper.h\"\n>\n>  /*\n>   * path = absolute or relative path name\n> @@ -71,6 +74,62 @@ static void resolve_symlink(struct strbuf *path)\n>  \tstrbuf_reset(&link);\n>  }\n>\n> +/*\n> + * Lock PID file functions - write PID to a .lock.pid file alongside\n> + * the lock file for debugging stale locks. The PID file is registered\n> + * as a tempfile so it gets cleaned up by signal/atexit handlers.\n> + */\n> +\n> +static int lock_pid_info_enabled(void)\n> +{\n\nWith the above suggestion, I think this function would get a little more\ncomplicated to instead take a component argument. That's not a huge deal\nin and of itself, but callers that *create* a lockfile will have to\nsomehow pass in the component name when acquiring the lock.\n\n> +\treturn git_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);\n> +}\n> +\n> +static struct tempfile *create_lock_pid_file(const char *lock_path, int mode)\n> +{\n> +\tstruct strbuf pid_path = STRBUF_INIT;\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tstruct tempfile *pid_tempfile = NULL;\n> +\tint fd;\n> +\n> +\tif (!lock_pid_info_enabled())\n> +\t\treturn NULL;\n> +\n> +\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n> +\tfd = open(pid_path.buf, O_WRONLY | O_CREAT | O_TRUNC, mode);\n\nI'm not sure whether or not we should pass O_EXCL here, but I think it\ndepends on the naming convention we pick.\n\n> +\tif (fd >= 0) {\n\nThis is a style preference, but I'd suggest handling the happy path\noutside of this conditional if possible by inverting it. Perhaps\nsomething like:\n\n    if (fd < 0)\n        goto out;\n\n    strbuf_addf(&content, ...);\n    if (write_in_full(fd, content.buf, content.len) < 0)\n        warning_errno(...);\n\n    close(fd);\n    pid_tempfile = register_tempfile(pid_path.buf);\n\n    out:\n        strbuf_release(...);\n        return pid_tempfile;\n\n> +static int read_lock_pid(const char *lock_path, uintmax_t *pid_out)\n> +{\n> +\tstruct strbuf pid_path = STRBUF_INIT;\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tint ret = -1;\n> +\n> +\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n> +\tif (strbuf_read_file(&content, pid_path.buf, LOCK_PID_MAXLEN) > 0) {\n> +\t\tchar *endptr;\n> +\t\t*pid_out = strtoumax(content.buf, &endptr, 10);\n> +\t\tif (*pid_out > 0 && (*endptr == '\\n' || *endptr == '\\0'))\n> +\t\t\tret = 0;\n\nSame note here. I'd suggest setting \"ret = 0\" during initialization, and\ninverting this conditional to:\n\n    if (*pid_out <= 0 || (*endptr != '\\n' && *endptr != '\\0')) {\n        warning(...);\n        ret = -1;\n        goto out;\n    }\n> +\t\telse\n> +\t\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path.buf);\n> +\t}\n> +\tstrbuf_release(&pid_path);\n> +\tstrbuf_release(&content);\n> +\treturn ret;\n> +}\n> +\n>  /* Make sure errno contains a meaningful value on error */\n>  static int lock_file(struct lock_file *lk, const char *path, int flags,\n>  \t\t     int mode)\n> @@ -80,9 +139,12 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,\n>  \tstrbuf_addstr(&filename, path);\n>  \tif (!(flags & LOCK_NO_DEREF))\n>  \t\tresolve_symlink(&filename);\n> -\n>  \tstrbuf_addstr(&filename, LOCK_SUFFIX);\n> +\n\nThese look like stray whitespace changes that were left behind from\ndevelopment.\n\n> +\t\tif (pid_status == 1)\n> +\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\"PRIuMAX\". \"\n> +\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"),\n> +\t\t\t    pid);\n> +\t\telse if (pid_status == -1)\n> +\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\"PRIuMAX\", \"\n> +\t\t\t    \"which is no longer running. Remove the stale lock file to continue\"),\n> +\t\t\t    pid);\n> +\t\telse\n> +\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n> +\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"));\n\nOn one hand I prefer the new \"Another git process\" message for when we\ndon't have a PID lockfile. But on the other hand, I think the \"If it\nstill fails, a git process may have crashed...\" message is useful for\nusers who may not be immediately aware of the consequences of simply\nremoving the lockfile to continue.\n\nI do think the original message is somewhat verbose, so maybe the change\nhere in the non-PID case is worth doing. What are your thoughts?\n\n> +\n> +\t\tstrbuf_release(&lock_path);\n>  \t} else\n>  \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n>  \t\t\t    absolute_path(path), strerror(err));\n> @@ -207,6 +292,8 @@ int commit_lock_file(struct lock_file *lk)\n>  {\n>  \tchar *result_path = get_locked_file_path(lk);\n>\n> +\tdelete_tempfile(&lk->pid_tempfile);\n> +\n\nDo we want to wait to delete the PID file until after we know that we\nsuccessfully committed the lockfile?\n\n> +/* Maximum length for PID file content */\n> +#define LOCK_PID_MAXLEN 32\n\nMakes sense ;-). This should be plenty of space, since on my system\nmaximum PID value is 2^22:\n\n    $ cat /proc/sys/kernel/pid_max\n    4194304\n\n> +test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n> +\tgit init repo4 &&\n> +\t(\n> +\t\tcd repo4 &&\n> +\t\techo content >file &&\n> +\t\tenv GIT_LOCK_PID_INFO=1 git add file &&\n> +\t\t# After successful add, no lock or PID files should exist\n> +\t\t! test -f .git/index.lock &&\n> +\t\t! test -f .git/index.lock.pid\n\nThese should be:\n\n    test_path_is_missing .git/index.lock &&\n    test_path_is_missing .git/index.lock.pid\n\ninstead of bare \"! test -f\"'s.\n\nThanks,\nTaylor\n"},{"id":"531688","messageId":"aTK77RFUvwpI-L13@pks.im","threadId":"64568","inReplyTo":"aTDFks3RW57Ytwvq@nand.local","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T11:03:09Z","receivedAt":"2025-12-05T11:03:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Dec 03, 2025 at 06:19:46PM -0500, Taylor Blau wrote:\n> On Wed, Dec 03, 2025 at 04:16:10PM -0500, Jeff King wrote:\n> > On Tue, Dec 02, 2025 at 03:07:27PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> >\n> > > The .lock.pid file is created when a lock is acquired (if enabled), and\n> > > automatically cleaned up when the lock is released (via commit or\n> > > rollback). The file is registered as a tempfile so it gets cleaned up\n> > > by signal and atexit handlers if the process terminates abnormally.\n> >\n> > I'm sympathetic to the goal of this series, and the implementation looks\n> > cleanly done. But I wonder if there might be some system-level side\n> > effects that make these .pid files awkward.\n> >\n> > Temporarily having an extra .git/index.lock.pid file is probably not a\n> > big deal. But for other namespaces, like refs, we're colliding with\n> > names that have other meanings. So if we want to update refs/heads/foo,\n> > for example, we'll create refs/heads/foo.lock now. And after your patch,\n> > also refs/heads/foo.lock.pid.\n> >\n> > The \".lock\" suffix is special, in that we disallow it in a refname and\n> > know to skip it when iterating over loose refs. But for the \".pid\"\n> > variant, we run the risk of colliding with a real branch named\n> > \"foo.lock.pid\", both for reading and writing.\n> \n> Good point. I don't have a strong opinion on whether or not we should\n> use an append-only log of which PIDs grabbed which lockfiles when versus\n> tracking them on a per-lock basis. But I wonder if this would be\n> mitigated by either:\n> \n>  - Keeping the \".lock\" suffix as-is, so that holding a lockfile at path\n>    \"$GIT_DIR/index.lock\" would create \"$GIT_DIR/index-pid.lock\" or\n>    something similar.\n> \n>  - Introducing a new reference name constraint that treats \".lock.pid\"\n>    as a reserved in a manner identical to how we currently treat\n>    \".lock\".\n> \n> Between the two, I vastly prefer the former, but see below for more on\n> why.\n\nYeah, I agree that this is not really a feasible change for the \"files\"\nreference backend. Besides all the issues mentioned in this thread, we\nalso have to consider alternative implementations of Git and old\nversions. Those wouldn't know that the \".lock.pid\" files are special, so\nthey will misbehave if Git started to write them now.\n\nWe could of course work around that issue by introducing a new\nrepository extension. But I doubt that this is something we want to\npursue in this context. The provided benefit just isn't high enough.\n\nThe overall idea still has merit though. So if we still want to pursue\nit we can likely work around the issue by introducing a toggle that\nallows specific callers to opt out of creating the PID files.\n\nThat'd raise the question though when we are most likely to need the PID\nfiles for debugging stuff. From my own experience I only ever had issues\nwith stale locks in the ref subsystem, so if we disable the mechanism in\nexactly that subsystem it may be way less useful.\n\nIf references are the main culprit one could also think about a slightly\nugly alternative approach: loose refs handle it just fine if they\ncontain additional lines. So in theory, we could write a second line for\neach lock file that contains the PID. We do have an fsck check that\nwarns about this, but in theory this should just work. Whether we want\nto go there is a different question though.\n\nPatrick\n"},{"id":"531724","messageId":"20251205184648.GC33447@coredump.intra.peff.net","threadId":"64568","inReplyTo":"aTDFks3RW57Ytwvq@nand.local","subject":"Re: [PATCH] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-12-05T18:46:48Z","receivedAt":"2025-12-05T18:46:50Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Dec 03, 2025 at 06:19:46PM -0500, Taylor Blau wrote:\n\n> Changing the naming scheme as above would cause us to hold\n> \"foo.pid.lock\" in addition to \"foo.lock\". That would allow process B\n> here to write branch \"foo.lock.pid\" (as is the case today). But if the\n> scenario were instead \"process B wants to write branch foo.pid.lock\", it\n> would fail immediately since the \".lock\" suffix is reserved.\n\nI agree that this gets rid of any corruption or race issues, since all\nversions understand how to handle \".lock\" specially (and assuming we\ncreate foo.pid.lock with O_EXCL). But there is still a namespace\ncollision; I cannot write refs \"foo\" and \"foo.pid\" at the same time.\n\n> > So I dunno what that means for your patch. I notice that the user has to\n> > enable the feature manually. But it feels more like it should be\n> > selective based on which subsystem is using the lockfile (so refs would\n> > never want it, but other lockfiles/tempfiles might).\n> \n> Yeah, I think that something similar to the \"which files do we fsync()\n> and how?\" configuration we have today would be a nice complement here.\n\nI'm not sure it makes sense for the user to configure this. I more meant\nthat the ref files-backend code would set a flag for \"no, do not create\na pid file for me ever\" (or inversely, other bits of the code would\nadd a flag for \"yes, it's OK to do so\").\n\n-Peff\n"},{"id":"532382","messageId":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":"pull.2011.git.1764688047077.gitgitgadget@gmail.com","subject":"[PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-17T18:59:26Z","receivedAt":"2025-12-17T18:59:29Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion PID file\nalongside each lock file, containing the PID of the lock holder.\n\nFor a lock file \"foo.lock\", the PID file is named \"foo.pid.lock\". The\nfile uses a simple key-value format (\"pid <value>\") following the same\npattern as Git object headers, making it extensible for future metadata.\n\nThe PID file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks for an existing PID file\nand, if found, uses kill(pid, 0) to determine if the process is still\nrunning. This allows providing context-aware error messages:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nThe feature is controlled via core.lockfilePid configuration, which\naccepts per-component values similar to core.fsync:\n\n  - none: Disable for all components (default)\n  - all: Enable for all components\n  - index, config, refs, commit-graph, midx, shallow, gc, other:\n    Enable for specific components\n\nMultiple components can be combined with commas:\n\n  git config core.lockfilePid index,config\n\nEach lock file call site specifies which component it belongs to,\nallowing users fine-grained control over which locks create PID files.\n\nExisting PID files are always read when displaying lock errors,\nregardless of the core.lockfilePid setting. This ensures helpful\ndiagnostics even when the feature was previously enabled and later\ndisabled.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n    \n    Changes in v2\n    \n     * Changed file naming: Using foo.pid.lock to keep the .lock suffix\n       (which is rejected by ref validation), while clearly indicating the\n       file's purpose\n    \n     * Extensible file format: Using pid <value> key-value format (same\n       pattern as Git object headers) instead of raw PID, allowing future\n       fields (hostname?)\n    \n     * Replaced environment variable with config: Now uses core.lockfilePid\n       configuration instead of GIT_LOCK_PID_INFO environment variable\n    \n     * Added per-component configuration: Similar to core.fsync, users can\n       enable PID files for specific components:\n       \n       * none (default), all\n       * index, config, refs, commit-graph, midx, shallow, gc, other\n    \n     * Caller-specified components: Each call site specifies which component\n       it belongs to, giving users fine-grained control\n    \n     * Always read existing PID files: When displaying lock errors, existing\n       PID files are read regardless of config setting, ensuring helpful\n       diagnostics\n    \n    I'm not very confident in my capacity to attribute the right component\n    name for each of the code paths. Specially the ones I marked as other.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nRange-diff vs v1:\n\n 1:  7068ec0041 ! 1:  d62edb0ca5 lockfile: add PID file for debugging stale locks\n     @@ Commit message\n      \n          When a lock file is held, it can be helpful to know which process owns\n          it, especially when debugging stale locks left behind by crashed\n     -    processes. Add an optional feature that creates a companion .lock.pid\n     -    file alongside each lock file, containing the PID of the lock holder.\n     +    processes. Add an optional feature that creates a companion PID file\n     +    alongside each lock file, containing the PID of the lock holder.\n      \n     -    The .lock.pid file is created when a lock is acquired (if enabled), and\n     +    For a lock file \"foo.lock\", the PID file is named \"foo.pid.lock\". The\n     +    file uses a simple key-value format (\"pid <value>\") following the same\n     +    pattern as Git object headers, making it extensible for future metadata.\n     +\n     +    The PID file is created when a lock is acquired (if enabled), and\n          automatically cleaned up when the lock is released (via commit or\n          rollback). The file is registered as a tempfile so it gets cleaned up\n          by signal and atexit handlers if the process terminates abnormally.\n      \n     -    When a lock conflict occurs, the code checks if the PID from the .pid\n     -    file is still running using kill(pid, 0). This allows providing\n     -    context-aware error messages. With PID info enabled:\n     +    When a lock conflict occurs, the code checks for an existing PID file\n     +    and, if found, uses kill(pid, 0) to determine if the process is still\n     +    running. This allows providing context-aware error messages:\n      \n            Lock is held by process 12345. Wait for it to finish, or remove\n            the lock file to continue.\n     @@ Commit message\n            Lock was held by process 12345, which is no longer running.\n            Remove the stale lock file to continue.\n      \n     -    Without PID info (default):\n     +    The feature is controlled via core.lockfilePid configuration, which\n     +    accepts per-component values similar to core.fsync:\n     +\n     +      - none: Disable for all components (default)\n     +      - all: Enable for all components\n     +      - index, config, refs, commit-graph, midx, shallow, gc, other:\n     +        Enable for specific components\n     +\n     +    Multiple components can be combined with commas:\n     +\n     +      git config core.lockfilePid index,config\n      \n     -      Another git process seems to be running in this repository.\n     -      Wait for it to finish, or remove the lock file to continue.\n     +    Each lock file call site specifies which component it belongs to,\n     +    allowing users fine-grained control over which locks create PID files.\n      \n     -    The feature is opt-in via GIT_LOCK_PID_INFO=1 environment variable.\n     +    Existing PID files are always read when displaying lock errors,\n     +    regardless of the core.lockfilePid setting. This ensures helpful\n     +    diagnostics even when the feature was previously enabled and later\n     +    disabled.\n      \n          Signed-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n      \n     - ## Documentation/git.adoc ##\n     -@@ Documentation/git.adoc: be in the future).\n     - \tthe background which do not want to cause lock contention with\n     - \tother operations on the repository.  Defaults to `1`.\n     - \n     -+`GIT_LOCK_PID_INFO`::\n     -+\tIf this Boolean environment variable is set to `1`, Git will create\n     -+\ta `.lock.pid` file alongside each lock file containing the PID of the\n     -+\tprocess that created the lock. This information is displayed in error\n     -+\tmessages when a lock conflict occurs, making it easier to identify\n     -+\tstale locks or debug locking issues. The PID files are automatically\n     -+\tcleaned up via signal and atexit handlers; however, if a process is\n     -+\tterminated abnormally (e.g., SIGKILL), the file may remain as a stale\n     -+\tindicator. Disabled by default.\n     -+\n     - `GIT_REDIRECT_STDIN`::\n     - `GIT_REDIRECT_STDOUT`::\n     - `GIT_REDIRECT_STDERR`::\n     + ## Documentation/config/core.adoc ##\n     +@@ Documentation/config/core.adoc: confusion unless you know what you are doing (e.g. you are creating a\n     + read-only snapshot of the same index to a location different from the\n     + repository's usual working tree).\n     + \n     ++core.lockfilePid::\n     ++\tA comma-separated list of components for which Git should create\n     ++\ta PID file alongside the lock file. When a lock acquisition fails\n     ++\tand a PID file exists, Git can provide additional diagnostic\n     ++\tinformation about the process holding the lock, including whether\n     ++\tit is still running.\n     +++\n     ++This feature is disabled by default. You can enable it for specific\n     ++components or use `all` to enable for all components.\n     +++\n     ++* `none` disables PID file creation for all components.\n     ++* `index` creates PID files for index lock operations.\n     ++* `config` creates PID files for config file lock operations.\n     ++* `refs` creates PID files for reference lock operations.\n     ++* `commit-graph` creates PID files for commit-graph lock operations.\n     ++* `midx` creates PID files for multi-pack-index lock operations.\n     ++* `shallow` creates PID files for shallow file lock operations.\n     ++* `gc` creates PID files for garbage collection lock operations.\n     ++* `other` creates PID files for other miscellaneous lock operations.\n     ++* `all` enables PID file creation for all components.\n     +++\n     ++The PID file is named by inserting `.pid` before the `.lock` suffix.\n     ++For example, if the lock file is `index.lock`, the PID file will be\n     ++`index.pid.lock`. The file contains `pid <value>` on a single line,\n     ++following the same key-value format used by Git object headers.\n     ++\n     + core.logAllRefUpdates::\n     + \tEnable the reflog. Updates to a ref <ref> is logged to the file\n     + \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\n     +\n     + ## apply.c ##\n     +@@ apply.c: static int build_fake_ancestor(struct apply_state *state, struct patch *list)\n     + \t\t}\n     + \t}\n     + \n     +-\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);\n     ++\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t  LOCKFILE_PID_OTHER);\n     + \tres = write_locked_index(&result, &lock, COMMIT_LOCK);\n     + \tdiscard_index(&result);\n     + \n     +@@ apply.c: static int apply_patch(struct apply_state *state,\n     + \t\tif (state->index_file)\n     + \t\t\thold_lock_file_for_update(&state->lock_file,\n     + \t\t\t\t\t\t  state->index_file,\n     +-\t\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t\t\t  LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t\t  LOCKFILE_PID_INDEX);\n     + \t\telse\n     + \t\t\trepo_hold_locked_index(state->repo, &state->lock_file,\n     + \t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     +\n     + ## builtin/commit.c ##\n     +@@ builtin/commit.c: static const char *prepare_index(const char **argv, const char *prefix,\n     + \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n     + \t\t\t     (uintmax_t) getpid());\n     + \thold_lock_file_for_update(&false_lock, path,\n     +-\t\t\t\t  LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_OTHER);\n     + \n     + \tcreate_base_index(current_head);\n     + \tadd_remove_files(&partial);\n     +\n     + ## builtin/credential-store.c ##\n     +@@ builtin/credential-store.c: static void rewrite_credential_file(const char *fn, struct credential *c,\n     + \tint timeout_ms = 1000;\n     + \n     + \trepo_config_get_int(the_repository, \"credentialstore.locktimeoutms\", &timeout_ms);\n     +-\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms) < 0)\n     ++\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms,\n     ++\t\t\t\t\t      LOCKFILE_PID_CONFIG) < 0)\n     + \t\tdie_errno(_(\"unable to get credential storage lock in %d ms\"), timeout_ms);\n     + \tif (extra)\n     + \t\tprint_line(extra);\n     +\n     + ## builtin/difftool.c ##\n     +@@ builtin/difftool.c: static int run_dir_diff(struct repository *repo,\n     + \t\t\tstruct lock_file lock = LOCK_INIT;\n     + \t\t\tstrbuf_reset(&buf);\n     + \t\t\tstrbuf_addf(&buf, \"%s/wtindex\", tmpdir.buf);\n     +-\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||\n     ++\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0,\n     ++\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0 ||\n     + \t\t\t    write_locked_index(&wtindex, &lock, COMMIT_LOCK)) {\n     + \t\t\t\tret = error(\"could not write %s\", buf.buf);\n     + \t\t\t\tgoto finish;\n     +\n     + ## builtin/fast-import.c ##\n     +@@ builtin/fast-import.c: static void dump_marks(void)\n     + \t\treturn;\n     + \t}\n     + \n     +-\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {\n     ++\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0,\n     ++\t\t\t\t      LOCKFILE_PID_OTHER) < 0) {\n     + \t\tfailure |= error_errno(_(\"unable to write marks file %s\"),\n     + \t\t\t\t       export_marks_file);\n     + \t\treturn;\n     +\n     + ## builtin/gc.c ##\n     +@@ builtin/gc.c: static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n     + \n     + \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n     + \tfd = hold_lock_file_for_update(&lock, pidfile_path,\n     +-\t\t\t\t       LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t       LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n     + \tif (!force) {\n     + \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n     + \t\tstatic char *scan_fmt;\n     +@@ builtin/gc.c: int cmd_gc(int argc,\n     + \tif (daemonized) {\n     + \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n     + \t\thold_lock_file_for_update(&log_lock, path,\n     +-\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n     + \t\tdup2(get_lock_file_fd(&log_lock), 2);\n     + \t\tatexit(process_log_file_at_exit);\n     + \t\tfree(path);\n     +@@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n     + \tstruct repository *r = the_repository;\n     + \tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->sources->path);\n     + \n     +-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n     ++\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n     ++\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n     + \t\t/*\n     + \t\t * Another maintenance command is running.\n     + \t\t *\n     +@@ builtin/gc.c: static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n     + \t\tlock_file_timeout_ms = 150;\n     + \n     + \tfd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,\n     +-\t\t\t\t\t       lock_file_timeout_ms);\n     ++\t\t\t\t\t       lock_file_timeout_ms,\n     ++\t\t\t\t\t       LOCKFILE_PID_GC);\n     + \n     + \t/*\n     + \t * Does this file already exist? With the intended contents? Is it\n     +@@ builtin/gc.c: static int update_background_schedule(const struct maintenance_start_opts *opts,\n     + \tstruct lock_file lk;\n     + \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->sources->path);\n     + \n     +-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n     ++\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n     ++\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n     + \t\tif (errno == EEXIST)\n     + \t\t\terror(_(\"unable to create '%s.lock': %s.\\n\\n\"\n     + \t\t\t    \"Another scheduled git-maintenance(1) process seems to be running in this\\n\"\n     +\n     + ## builtin/sparse-checkout.c ##\n     +@@ builtin/sparse-checkout.c: static int write_patterns_and_update(struct repository *repo,\n     + \tif (safe_create_leading_directories(repo, sparse_filename))\n     + \t\tdie(_(\"failed to create directory for sparse-checkout file\"));\n     + \n     +-\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);\n     ++\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t  LOCKFILE_PID_OTHER);\n     + \n     + \tresult = update_working_directory(repo, pl);\n     + \tif (result) {\n     +\n     + ## bundle.c ##\n     +@@ bundle.c: int create_bundle(struct repository *r, const char *path,\n     + \t\tbundle_fd = 1;\n     + \telse\n     + \t\tbundle_fd = hold_lock_file_for_update(&lock, path,\n     +-\t\t\t\t\t\t      LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t\t\t      LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t\t      LOCKFILE_PID_OTHER);\n     + \n     + \tif (version == -1)\n     + \t\tversion = min_version;\n     +\n     + ## cache-tree.c ##\n     +@@ cache-tree.c: int write_index_as_tree(struct object_id *oid, struct index_state *index_state,\n     + \tstruct lock_file lock_file = LOCK_INIT;\n     + \tint ret;\n     + \n     +-\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);\n     ++\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t  LOCKFILE_PID_INDEX);\n     + \n     + \tentries = read_index_from(index_state, index_path,\n     + \t\t\t\t  repo_get_git_dir(the_repository));\n     +\n     + ## commit-graph.c ##\n     +@@ commit-graph.c: static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n     + \t\tchar *lock_name = get_commit_graph_chain_filename(ctx->odb_source);\n     + \n     + \t\thold_lock_file_for_update_mode(&lk, lock_name,\n     +-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n     ++\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n     ++\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n     + \t\tfree(lock_name);\n     + \n     + \t\tgraph_layer = mks_tempfile_m(ctx->graph_name, 0444);\n     +@@ commit-graph.c: static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n     + \t\t\t   get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer));\n     + \t} else {\n     + \t\thold_lock_file_for_update_mode(&lk, ctx->graph_name,\n     +-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n     ++\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n     ++\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n     + \t\tf = hashfd(ctx->r->hash_algo,\n     + \t\t\t   get_lock_file_fd(&lk), get_lock_file_path(&lk));\n     + \t}\n     +\n     + ## compat/mingw.c ##\n     +@@ compat/mingw.c: int mingw_kill(pid_t pid, int sig)\n     + \t\t\tCloseHandle(h);\n     + \t\t\treturn 0;\n     + \t\t}\n     ++\t\t/*\n     ++\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n     ++\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n     ++\t\t * compatibility with kill(pid, 0).\n     ++\t\t */\n     ++\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n     ++\t\t\terrno = ESRCH;\n     ++\t\telse\n     ++\t\t\terrno = err_win_to_posix(GetLastError());\n     ++\t\treturn -1;\n     + \t}\n     + \n     + \terrno = EINVAL;\n     +\n     + ## config.c ##\n     +@@ config.c: int repo_config_set_multivar_in_file_gently(struct repository *r,\n     + \t * The lock serves a purpose in addition to locking: the new\n     + \t * contents of .git/config will be written into it.\n     + \t */\n     +-\tfd = hold_lock_file_for_update(&lock, config_filename, 0);\n     ++\tfd = hold_lock_file_for_update(&lock, config_filename, 0,\n     ++\t\t\t\t       LOCKFILE_PID_CONFIG);\n     + \tif (fd < 0) {\n     + \t\terror_errno(_(\"could not lock config file %s\"), config_filename);\n     + \t\tret = CONFIG_NO_LOCK;\n     +@@ config.c: static int repo_config_copy_or_rename_section_in_file(\n     + \tif (!config_filename)\n     + \t\tconfig_filename = filename_buf = repo_git_path(r, \"config\");\n     + \n     +-\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0);\n     ++\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0,\n     ++\t\t\t\t\t   LOCKFILE_PID_CONFIG);\n     + \tif (out_fd < 0) {\n     + \t\tret = error(_(\"could not lock config file %s\"), config_filename);\n     + \t\tgoto out;\n     +\n     + ## environment.c ##\n     +@@\n     + #include \"gettext.h\"\n     + #include \"git-zlib.h\"\n     + #include \"ident.h\"\n     ++#include \"lockfile.h\"\n     + #include \"mailmap.h\"\n     + #include \"object-name.h\"\n     + #include \"repository.h\"\n     +@@ environment.c: next_name:\n     + \treturn (current & ~negative) | positive;\n     + }\n     + \n     ++static const struct lockfile_pid_component_name {\n     ++\tconst char *name;\n     ++\tenum lockfile_pid_component component_bits;\n     ++} lockfile_pid_component_names[] = {\n     ++\t{ \"index\", LOCKFILE_PID_INDEX },\n     ++\t{ \"config\", LOCKFILE_PID_CONFIG },\n     ++\t{ \"refs\", LOCKFILE_PID_REFS },\n     ++\t{ \"commit-graph\", LOCKFILE_PID_COMMIT_GRAPH },\n     ++\t{ \"midx\", LOCKFILE_PID_MIDX },\n     ++\t{ \"shallow\", LOCKFILE_PID_SHALLOW },\n     ++\t{ \"gc\", LOCKFILE_PID_GC },\n     ++\t{ \"other\", LOCKFILE_PID_OTHER },\n     ++\t{ \"all\", LOCKFILE_PID_ALL },\n     ++};\n     ++\n     ++static enum lockfile_pid_component parse_lockfile_pid_components(const char *var,\n     ++\t\t\t\t\t\t\t\t const char *string)\n     ++{\n     ++\tenum lockfile_pid_component current = LOCKFILE_PID_DEFAULT;\n     ++\tenum lockfile_pid_component positive = 0, negative = 0;\n     ++\n     ++\twhile (string) {\n     ++\t\tsize_t len;\n     ++\t\tconst char *ep;\n     ++\t\tint negated = 0;\n     ++\t\tint found = 0;\n     ++\n     ++\t\tstring = string + strspn(string, \", \\t\\n\\r\");\n     ++\t\tep = strchrnul(string, ',');\n     ++\t\tlen = ep - string;\n     ++\t\tif (len == 4 && !strncmp(string, \"none\", 4)) {\n     ++\t\t\tcurrent = LOCKFILE_PID_NONE;\n     ++\t\t\tgoto next_name;\n     ++\t\t}\n     ++\n     ++\t\tif (*string == '-') {\n     ++\t\t\tnegated = 1;\n     ++\t\t\tstring++;\n     ++\t\t\tlen--;\n     ++\t\t\tif (!len)\n     ++\t\t\t\twarning(_(\"invalid value for variable %s\"), var);\n     ++\t\t}\n     ++\n     ++\t\tif (!len)\n     ++\t\t\tbreak;\n     ++\n     ++\t\tfor (size_t i = 0; i < ARRAY_SIZE(lockfile_pid_component_names); ++i) {\n     ++\t\t\tconst struct lockfile_pid_component_name *n = &lockfile_pid_component_names[i];\n     ++\n     ++\t\t\tif (strncmp(n->name, string, len) || strlen(n->name) != len)\n     ++\t\t\t\tcontinue;\n     ++\n     ++\t\t\tfound = 1;\n     ++\t\t\tif (negated)\n     ++\t\t\t\tnegative |= n->component_bits;\n     ++\t\t\telse\n     ++\t\t\t\tpositive |= n->component_bits;\n     ++\t\t}\n     ++\n     ++\t\tif (!found) {\n     ++\t\t\tchar *component = xstrndup(string, len);\n     ++\t\t\twarning(_(\"ignoring unknown core.lockfilePid component '%s'\"), component);\n     ++\t\t\tfree(component);\n     ++\t\t}\n     ++\n     ++next_name:\n     ++\t\tstring = ep;\n     ++\t}\n     ++\n     ++\treturn (current & ~negative) | positive;\n     ++}\n     ++\n     + static int git_default_core_config(const char *var, const char *value,\n     + \t\t\t\t   const struct config_context *ctx, void *cb)\n     + {\n     +@@ environment.c: static int git_default_core_config(const char *var, const char *value,\n     + \t\treturn 0;\n     + \t}\n     + \n     ++\tif (!strcmp(var, \"core.lockfilepid\")) {\n     ++\t\tif (!value)\n     ++\t\t\treturn config_error_nonbool(var);\n     ++\t\tlockfile_pid_components = parse_lockfile_pid_components(var, value);\n     ++\t\treturn 0;\n     ++\t}\n     ++\n     + \tif (!strcmp(var, \"core.createobject\")) {\n     + \t\tif (!value)\n     + \t\t\treturn config_error_nonbool(var);\n      \n       ## lockfile.c ##\n      @@\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n       }\n       \n      +/*\n     -+ * Lock PID file functions - write PID to a .lock.pid file alongside\n     ++ * Lock PID file functions - write PID to a foo-pid.lock file alongside\n      + * the lock file for debugging stale locks. The PID file is registered\n      + * as a tempfile so it gets cleaned up by signal/atexit handlers.\n     ++ *\n     ++ * Naming: For \"foo.lock\", the PID file is \"foo-pid.lock\" (not \"foo.lock.pid\").\n     ++ * This avoids collision with the refs namespace.\n     ++ */\n     ++\n     ++/* Global config variable, initialized from core.lockfilePid */\n     ++enum lockfile_pid_component lockfile_pid_components = LOCKFILE_PID_DEFAULT;\n     ++\n     ++/*\n     ++ * Path generation helpers.\n     ++ * Given base path \"foo\", generate:\n     ++ *   - lock path: \"foo.lock\"\n     ++ *   - pid path:  \"foo-pid.lock\"\n      + */\n     ++static void get_lock_path(struct strbuf *out, const char *path)\n     ++{\n     ++\tstrbuf_addstr(out, path);\n     ++\tstrbuf_addstr(out, LOCK_SUFFIX);\n     ++}\n      +\n     -+static int lock_pid_info_enabled(void)\n     ++static void get_pid_path(struct strbuf *out, const char *path)\n      +{\n     -+\treturn git_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);\n     ++\tstrbuf_addstr(out, path);\n     ++\tstrbuf_addstr(out, LOCK_PID_INFIX);\n     ++\tstrbuf_addstr(out, LOCK_SUFFIX);\n      +}\n      +\n     -+static struct tempfile *create_lock_pid_file(const char *lock_path, int mode)\n     ++static struct tempfile *create_lock_pid_file(const char *pid_path, int mode,\n     ++\t\t\t\t\t     enum lockfile_pid_component component)\n      +{\n     -+\tstruct strbuf pid_path = STRBUF_INIT;\n      +\tstruct strbuf content = STRBUF_INIT;\n      +\tstruct tempfile *pid_tempfile = NULL;\n     -+\tint fd;\n     -+\n     -+\tif (!lock_pid_info_enabled())\n     -+\t\treturn NULL;\n     -+\n     -+\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n     -+\tfd = open(pid_path.buf, O_WRONLY | O_CREAT | O_TRUNC, mode);\n     -+\tif (fd >= 0) {\n     -+\t\tstrbuf_addf(&content, \"%\"PRIuMAX\"\\n\", (uintmax_t)getpid());\n     -+\t\tif (write_in_full(fd, content.buf, content.len) < 0)\n     -+\t\t\twarning_errno(_(\"could not write lock pid file '%s'\"),\n     -+\t\t\t\t      pid_path.buf);\n     -+\t\tclose(fd);\n     -+\t\tpid_tempfile = register_tempfile(pid_path.buf);\n     ++\tint fd = -1;\n     ++\n     ++\tif (!(lockfile_pid_components & component))\n     ++\t\tgoto out;\n     ++\n     ++\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n     ++\tif (fd < 0)\n     ++\t\tgoto out;\n     ++\n     ++\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n     ++\tif (write_in_full(fd, content.buf, content.len) < 0) {\n     ++\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n     ++\t\tgoto out;\n      +\t}\n     ++\n     ++\tclose(fd);\n     ++\tfd = -1;\n     ++\tpid_tempfile = register_tempfile(pid_path);\n     ++\n     ++out:\n     ++\tif (fd >= 0)\n     ++\t\tclose(fd);\n      +\tstrbuf_release(&content);\n     -+\tstrbuf_release(&pid_path);\n      +\treturn pid_tempfile;\n      +}\n      +\n     -+static int read_lock_pid(const char *lock_path, uintmax_t *pid_out)\n     ++static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n      +{\n     -+\tstruct strbuf pid_path = STRBUF_INIT;\n      +\tstruct strbuf content = STRBUF_INIT;\n     ++\tconst char *val;\n      +\tint ret = -1;\n      +\n     -+\tstrbuf_addf(&pid_path, \"%s%s\", lock_path, LOCK_PID_SUFFIX);\n     -+\tif (strbuf_read_file(&content, pid_path.buf, LOCK_PID_MAXLEN) > 0) {\n     ++\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n     ++\t\tgoto out;\n     ++\n     ++\tstrbuf_rtrim(&content);\n     ++\n     ++\tif (skip_prefix(content.buf, \"pid \", &val)) {\n      +\t\tchar *endptr;\n     -+\t\t*pid_out = strtoumax(content.buf, &endptr, 10);\n     -+\t\tif (*pid_out > 0 && (*endptr == '\\n' || *endptr == '\\0'))\n     ++\t\t*pid_out = strtoumax(val, &endptr, 10);\n     ++\t\tif (*pid_out > 0 && !*endptr)\n      +\t\t\tret = 0;\n     -+\t\telse\n     -+\t\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path.buf);\n      +\t}\n     -+\tstrbuf_release(&pid_path);\n     ++\n     ++\tif (ret)\n     ++\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n     ++\n     ++out:\n      +\tstrbuf_release(&content);\n      +\treturn ret;\n      +}\n      +\n       /* Make sure errno contains a meaningful value on error */\n       static int lock_file(struct lock_file *lk, const char *path, int flags,\n     - \t\t     int mode)\n     -@@ lockfile.c: static int lock_file(struct lock_file *lk, const char *path, int flags,\n     - \tstrbuf_addstr(&filename, path);\n     +-\t\t     int mode)\n     ++\t\t     int mode, enum lockfile_pid_component component)\n     + {\n     +-\tstruct strbuf filename = STRBUF_INIT;\n     ++\tstruct strbuf base_path = STRBUF_INIT;\n     ++\tstruct strbuf lock_path = STRBUF_INIT;\n     ++\tstruct strbuf pid_path = STRBUF_INIT;\n     + \n     +-\tstrbuf_addstr(&filename, path);\n     ++\tstrbuf_addstr(&base_path, path);\n       \tif (!(flags & LOCK_NO_DEREF))\n     - \t\tresolve_symlink(&filename);\n     --\n     - \tstrbuf_addstr(&filename, LOCK_SUFFIX);\n     +-\t\tresolve_symlink(&filename);\n     ++\t\tresolve_symlink(&base_path);\n      +\n     - \tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n     -+\tif (lk->tempfile)\n     -+\t\tlk->pid_tempfile = create_lock_pid_file(filename.buf, mode);\n     ++\tget_lock_path(&lock_path, base_path.buf);\n     ++\tget_pid_path(&pid_path, base_path.buf);\n      +\n     - \tstrbuf_release(&filename);\n     ++\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n     ++\tif (lk->tempfile)\n     ++\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode,\n     ++\t\t\t\t\t\t\tcomponent);\n     + \n     +-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n     +-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n     +-\tstrbuf_release(&filename);\n     ++\tstrbuf_release(&base_path);\n     ++\tstrbuf_release(&lock_path);\n     ++\tstrbuf_release(&pid_path);\n       \treturn lk->tempfile ? lk->tempfile->fd : -1;\n       }\n     + \n     +@@ lockfile.c: static int lock_file(struct lock_file *lk, const char *path, int flags,\n     +  * exactly once. If timeout_ms is -1, try indefinitely.\n     +  */\n     + static int lock_file_timeout(struct lock_file *lk, const char *path,\n     +-\t\t\t     int flags, long timeout_ms, int mode)\n     ++\t\t\t     int flags, long timeout_ms, int mode,\n     ++\t\t\t     enum lockfile_pid_component component)\n     + {\n     + \tint n = 1;\n     + \tint multiplier = 1;\n     +@@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n     + \tstatic int random_initialized = 0;\n     + \n     + \tif (timeout_ms == 0)\n     +-\t\treturn lock_file(lk, path, flags, mode);\n     ++\t\treturn lock_file(lk, path, flags, mode, component);\n     + \n     + \tif (!random_initialized) {\n     + \t\tsrand((unsigned int)getpid());\n     +@@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n     + \t\tlong backoff_ms, wait_ms;\n     + \t\tint fd;\n     + \n     +-\t\tfd = lock_file(lk, path, flags, mode);\n     ++\t\tfd = lock_file(lk, path, flags, mode, component);\n     + \n     + \t\tif (fd >= 0)\n     + \t\t\treturn fd; /* success */\n      @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n       void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n       {\n     @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n      -\t\t    \"may have crashed in this repository earlier:\\n\"\n      -\t\t    \"remove the file manually to continue.\"),\n      -\t\t\t    absolute_path(path), strerror(err));\n     +-\t} else\n     ++\t\tconst char *abs_path = absolute_path(path);\n      +\t\tstruct strbuf lock_path = STRBUF_INIT;\n     ++\t\tstruct strbuf pid_path = STRBUF_INIT;\n      +\t\tuintmax_t pid;\n      +\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n      +\n     -+\t\tstrbuf_addf(&lock_path, \"%s%s\", absolute_path(path), LOCK_SUFFIX);\n     ++\t\tget_lock_path(&lock_path, abs_path);\n     ++\t\tget_pid_path(&pid_path, abs_path);\n      +\n      +\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n      +\t\t\t    lock_path.buf, strerror(err));\n      +\n     -+\t\tif (lock_pid_info_enabled() &&\n     -+\t\t    !read_lock_pid(lock_path.buf, &pid)) {\n     -+\t\t\tif (kill((pid_t)pid, 0) == 0)\n     -+\t\t\t\tpid_status = 1;\n     -+\t\t\telse\n     -+\t\t\t\tpid_status = -1;\n     ++\t\t/*\n     ++\t\t * Try to read PID file unconditionally - it may exist if\n     ++\t\t * core.lockfilePid was enabled for this component.\n     ++\t\t */\n     ++\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n     ++\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n     ++\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n     ++\t\t\telse if (errno == ESRCH)\n     ++\t\t\t\tpid_status = -1; /* no such process - stale lock */\n      +\t\t}\n      +\n      +\t\tif (pid_status == 1)\n     -+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\"PRIuMAX\". \"\n     -+\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"),\n     -+\t\t\t    pid);\n     ++\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n     ++\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n     ++\t\t\t\t    pid);\n      +\t\telse if (pid_status == -1)\n     -+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\"PRIuMAX\", \"\n     -+\t\t\t    \"which is no longer running. Remove the stale lock file to continue\"),\n     -+\t\t\t    pid);\n     ++\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n     ++\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n     ++\t\t\t\t    pid);\n      +\t\telse\n      +\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n     -+\t\t\t    \"Wait for it to finish, or remove the lock file to continue\"));\n     ++\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n      +\n      +\t\tstrbuf_release(&lock_path);\n     - \t} else\n     ++\t\tstrbuf_release(&pid_path);\n     ++\t} else {\n       \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n       \t\t\t    absolute_path(path), strerror(err));\n     ++\t}\n     + }\n     + \n     + NORETURN void unable_to_lock_die(const char *path, int err)\n     +@@ lockfile.c: NORETURN void unable_to_lock_die(const char *path, int err)\n     + /* This should return a meaningful errno on failure */\n     + int hold_lock_file_for_update_timeout_mode(struct lock_file *lk,\n     + \t\t\t\t\t   const char *path, int flags,\n     +-\t\t\t\t\t   long timeout_ms, int mode)\n     ++\t\t\t\t\t   long timeout_ms, int mode,\n     ++\t\t\t\t\t   enum lockfile_pid_component component)\n     + {\n     +-\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode);\n     ++\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode, component);\n     + \tif (fd < 0) {\n     + \t\tif (flags & LOCK_DIE_ON_ERROR)\n     + \t\t\tunable_to_lock_die(path, errno);\n      @@ lockfile.c: int commit_lock_file(struct lock_file *lk)\n       {\n       \tchar *result_path = get_locked_file_path(lk);\n     @@ lockfile.h: struct lock_file {\n       #define LOCK_SUFFIX \".lock\"\n       #define LOCK_SUFFIX_LEN 5\n       \n     -+/* Suffix for PID file that stores PID of lock holder: */\n     -+#define LOCK_PID_SUFFIX \".pid\"\n     -+#define LOCK_PID_SUFFIX_LEN 4\n     ++/*\n     ++ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo.pid.lock\".\n     ++ */\n     ++#define LOCK_PID_INFIX \".pid\"\n     ++#define LOCK_PID_INFIX_LEN 4\n      +\n      +/* Maximum length for PID file content */\n      +#define LOCK_PID_MAXLEN 32\n      +\n     -+/* Environment variable to enable lock PID info (default: disabled) */\n     -+#define GIT_LOCK_PID_INFO_ENVIRONMENT \"GIT_LOCK_PID_INFO\"\n     ++/*\n     ++ * Per-component lock PID file configuration, following core.fsync pattern.\n     ++ * Each component can be individually enabled via core.lockfilePid config.\n     ++ */\n     ++enum lockfile_pid_component {\n     ++\tLOCKFILE_PID_NONE = 0,\n     ++\tLOCKFILE_PID_INDEX = 1 << 0, /* .git/index.lock */\n     ++\tLOCKFILE_PID_CONFIG = 1 << 1, /* .git/config.lock */\n     ++\tLOCKFILE_PID_REFS = 1 << 2, /* refs locks */\n     ++\tLOCKFILE_PID_COMMIT_GRAPH = 1 << 3, /* commit-graph.lock */\n     ++\tLOCKFILE_PID_MIDX = 1 << 4, /* multi-pack-index.lock */\n     ++\tLOCKFILE_PID_SHALLOW = 1 << 5, /* shallow file */\n     ++\tLOCKFILE_PID_GC = 1 << 6, /* gc locks */\n     ++\tLOCKFILE_PID_OTHER = 1 << 7, /* other locks */\n     ++};\n     ++\n     ++#define LOCKFILE_PID_ALL (LOCKFILE_PID_INDEX | LOCKFILE_PID_CONFIG |      \\\n     ++\t\t\t  LOCKFILE_PID_REFS | LOCKFILE_PID_COMMIT_GRAPH | \\\n     ++\t\t\t  LOCKFILE_PID_MIDX | LOCKFILE_PID_SHALLOW |      \\\n     ++\t\t\t  LOCKFILE_PID_GC | LOCKFILE_PID_OTHER)\n     ++#define LOCKFILE_PID_DEFAULT LOCKFILE_PID_NONE\n     ++\n     ++/*\n     ++ * Bitmask indicating which components should create PID files.\n     ++ * Configured via core.lockfilePid.\n     ++ */\n     ++extern enum lockfile_pid_component lockfile_pid_components;\n       \n       /*\n        * Flags\n     +@@ lockfile.h: struct lock_file {\n     +  * timeout_ms milliseconds. If timeout_ms is 0, try exactly once; if\n     +  * timeout_ms is -1, retry indefinitely. The flags argument, error\n     +  * handling, and mode are described above.\n     ++ *\n     ++ * The `component` argument specifies which lock PID component this lock\n     ++ * belongs to, for selective PID file creation via core.lockfilePid config.\n     +  */\n     + int hold_lock_file_for_update_timeout_mode(\n     +-\t\tstruct lock_file *lk, const char *path,\n     +-\t\tint flags, long timeout_ms, int mode);\n     ++\tstruct lock_file *lk, const char *path,\n     ++\tint flags, long timeout_ms, int mode,\n     ++\tenum lockfile_pid_component component);\n     + \n     + static inline int hold_lock_file_for_update_timeout(\n     +-\t\tstruct lock_file *lk, const char *path,\n     +-\t\tint flags, long timeout_ms)\n     ++\tstruct lock_file *lk, const char *path,\n     ++\tint flags, long timeout_ms,\n     ++\tenum lockfile_pid_component component)\n     + {\n     + \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n     +-\t\t\t\t\t\t      timeout_ms, 0666);\n     ++\t\t\t\t\t\t      timeout_ms, 0666,\n     ++\t\t\t\t\t\t      component);\n     + }\n     + \n     + /*\n     +@@ lockfile.h: static inline int hold_lock_file_for_update_timeout(\n     +  * argument and error handling are described above.\n     +  */\n     + static inline int hold_lock_file_for_update(\n     +-\t\tstruct lock_file *lk, const char *path,\n     +-\t\tint flags)\n     ++\tstruct lock_file *lk, const char *path,\n     ++\tint flags, enum lockfile_pid_component component)\n     + {\n     +-\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n     ++\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0, component);\n     + }\n     + \n     + static inline int hold_lock_file_for_update_mode(\n     +-\t\tstruct lock_file *lk, const char *path,\n     +-\t\tint flags, int mode)\n     ++\tstruct lock_file *lk, const char *path,\n     ++\tint flags, int mode, enum lockfile_pid_component component)\n     + {\n     +-\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n     ++\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode,\n     ++\t\t\t\t\t\t      component);\n     + }\n     + \n     + /*\n      @@ lockfile.h: static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n       \n       /*\n     @@ lockfile.h: static inline int commit_lock_file_to(struct lock_file *lk, const ch\n       \n       #endif /* LOCKFILE_H */\n      \n     + ## loose.c ##\n     +@@ loose.c: int repo_write_loose_object_map(struct repository *repo)\n     + \t\treturn 0;\n     + \n     + \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n     +-\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n     ++\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n     ++\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     + \titer = kh_begin(map);\n     + \tif (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)\n     + \t\tgoto errout;\n     +@@ loose.c: static int write_one_object(struct odb_source *source,\n     + \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n     + \n     + \tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n     +-\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n     ++\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n     ++\t\t\t\t\t  LOCKFILE_PID_OTHER);\n     + \n     + \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n     + \tif (fd < 0)\n     +\n     + ## midx-write.c ##\n     +@@ midx-write.c: static int write_midx_internal(struct odb_source *source,\n     + \t\tstruct strbuf lock_name = STRBUF_INIT;\n     + \n     + \t\tget_midx_chain_filename(source, &lock_name);\n     +-\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);\n     ++\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t  LOCKFILE_PID_MIDX);\n     + \t\tstrbuf_release(&lock_name);\n     + \n     + \t\tincr = mks_tempfile_m(midx_name.buf, 0444);\n     +@@ midx-write.c: static int write_midx_internal(struct odb_source *source,\n     + \t\tf = hashfd(r->hash_algo, get_tempfile_fd(incr),\n     + \t\t\t   get_tempfile_path(incr));\n     + \t} else {\n     +-\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR);\n     ++\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t  LOCKFILE_PID_MIDX);\n     + \t\tf = hashfd(r->hash_algo, get_lock_file_fd(&lk),\n     + \t\t\t   get_lock_file_path(&lk));\n     + \t}\n     +\n     + ## odb.c ##\n     +@@ odb.c: void odb_add_to_alternates_file(struct object_database *odb,\n     + \tFILE *in, *out;\n     + \tint found = 0;\n     + \n     +-\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR);\n     ++\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t  LOCKFILE_PID_OTHER);\n     + \tout = fdopen_lock_file(&lock, \"w\");\n     + \tif (!out)\n     + \t\tdie_errno(_(\"unable to fdopen alternates lockfile\"));\n     +\n     + ## refs/files-backend.c ##\n     +@@ refs/files-backend.c: retry:\n     + \n     + \tif (hold_lock_file_for_update_timeout(\n     + \t\t\t    &lock->lk, ref_file.buf, LOCK_NO_DEREF,\n     +-\t\t\t    get_files_ref_lock_timeout_ms()) < 0) {\n     ++\t\t\t    get_files_ref_lock_timeout_ms(),\n     ++\t\t\t    LOCKFILE_PID_REFS) < 0) {\n     + \t\tint myerr = errno;\n     + \t\terrno = 0;\n     + \t\tif (myerr == ENOENT && --attempts_remaining > 0) {\n     +@@ refs/files-backend.c: static int create_reflock(const char *path, void *cb)\n     + \n     + \treturn hold_lock_file_for_update_timeout(\n     + \t\t\tlk, path, LOCK_NO_DEREF,\n     +-\t\t\tget_files_ref_lock_timeout_ms()) < 0 ? -1 : 0;\n     ++\t\t\tget_files_ref_lock_timeout_ms(),\n     ++\t\t\tLOCKFILE_PID_REFS) < 0 ? -1 : 0;\n     + }\n     + \n     + /*\n     +@@ refs/files-backend.c: static int files_reflog_expire(struct ref_store *ref_store,\n     + \t\t * work we need, including cleaning up if the program\n     + \t\t * exits unexpectedly.\n     + \t\t */\n     +-\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {\n     ++\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0,\n     ++\t\t\t\t\t      LOCKFILE_PID_REFS) < 0) {\n     + \t\t\tstruct strbuf err = STRBUF_INIT;\n     + \t\t\tunable_to_lock_message(log_file, errno, &err);\n     + \t\t\terror(\"%s\", err.buf);\n     +\n     + ## refs/packed-backend.c ##\n     +@@ refs/packed-backend.c: int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)\n     + \tif (hold_lock_file_for_update_timeout(\n     + \t\t\t    &refs->lock,\n     + \t\t\t    refs->path,\n     +-\t\t\t    flags, timeout_value) < 0) {\n     ++\t\t\t    flags, timeout_value,\n     ++\t\t\t    LOCKFILE_PID_REFS) < 0) {\n     + \t\tunable_to_lock_message(refs->path, errno, err);\n     + \t\treturn -1;\n     + \t}\n     +\n     + ## reftable/system.c ##\n     +@@ reftable/system.c: int flock_acquire(struct reftable_flock *l, const char *target_path,\n     + \t\treturn REFTABLE_OUT_OF_MEMORY_ERROR;\n     + \n     + \terr = hold_lock_file_for_update_timeout(lockfile, target_path, LOCK_NO_DEREF,\n     +-\t\t\t\t\t\ttimeout_ms);\n     ++\t\t\t\t\t\ttimeout_ms, LOCKFILE_PID_REFS);\n     + \tif (err < 0) {\n     + \t\treftable_free(lockfile);\n     + \t\tif (errno == EEXIST)\n     +\n     + ## repository.c ##\n     +@@ repository.c: int repo_hold_locked_index(struct repository *repo,\n     + {\n     + \tif (!repo->index_file)\n     + \t\tBUG(\"the repo hasn't been setup\");\n     +-\treturn hold_lock_file_for_update(lf, repo->index_file, flags);\n     ++\treturn hold_lock_file_for_update(lf, repo->index_file, flags,\n     ++\t\t\t\t\t LOCKFILE_PID_INDEX);\n     + }\n     +\n     + ## rerere.c ##\n     +@@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n     + \telse\n     + \t\tfd = hold_lock_file_for_update(&write_lock,\n     + \t\t\t\t\t       git_path_merge_rr(r),\n     +-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     + \tread_rr(r, merge_rr);\n     + \treturn fd;\n     + }\n     +\n     + ## sequencer.c ##\n     +@@ sequencer.c: static int write_message(const void *buf, size_t len, const char *filename,\n     + {\n     + \tstruct lock_file msg_file = LOCK_INIT;\n     + \n     +-\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0);\n     ++\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0,\n     ++\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     + \tif (msg_fd < 0)\n     + \t\treturn error_errno(_(\"could not lock '%s'\"), filename);\n     + \tif (write_in_full(msg_fd, buf, len) < 0) {\n     +@@ sequencer.c: static int save_todo(struct todo_list *todo_list, struct replay_opts *opts,\n     + \tif (is_rebase_i(opts) && !reschedule)\n     + \t\tnext++;\n     + \n     +-\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0);\n     ++\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0,\n     ++\t\t\t\t       LOCKFILE_PID_OTHER);\n     + \tif (fd < 0)\n     + \t\treturn error_errno(_(\"could not lock '%s'\"), todo_path);\n     + \toffset = get_item_line_offset(todo_list, next);\n     +@@ sequencer.c: static int safe_append(const char *filename, const char *fmt, ...)\n     + \tva_list ap;\n     + \tstruct lock_file lock = LOCK_INIT;\n     + \tint fd = hold_lock_file_for_update(&lock, filename,\n     +-\t\t\t\t\t   LOCK_REPORT_ON_ERROR);\n     ++\t\t\t\t\t   LOCK_REPORT_ON_ERROR,\n     ++\t\t\t\t\t   LOCKFILE_PID_OTHER);\n     + \tstruct strbuf buf = STRBUF_INIT;\n     + \n     + \tif (fd < 0)\n     +@@ sequencer.c: static int write_update_refs_state(struct string_list *refs_to_oids)\n     + \t\tgoto cleanup;\n     + \t}\n     + \n     +-\tif (hold_lock_file_for_update(&lock, path, 0) < 0) {\n     ++\tif (hold_lock_file_for_update(&lock, path, 0, LOCKFILE_PID_OTHER) < 0) {\n     + \t\tresult = error(_(\"another 'rebase' process appears to be running; \"\n     + \t\t\t\t \"'%s.lock' already exists\"),\n     + \t\t\t       path);\n     +\n     + ## shallow.c ##\n     +@@ shallow.c: void setup_alternate_shallow(struct shallow_lock *shallow_lock,\n     + \n     + \tfd = hold_lock_file_for_update(&shallow_lock->lock,\n     + \t\t\t\t       git_path_shallow(the_repository),\n     +-\t\t\t\t       LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t       LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t       LOCKFILE_PID_SHALLOW);\n     + \tcheck_shallow_file_for_update(the_repository);\n     + \tif (write_shallow_commits(&sb, 0, extra)) {\n     + \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n     +@@ shallow.c: void prune_shallow(unsigned options)\n     + \t}\n     + \tfd = hold_lock_file_for_update(&shallow_lock.lock,\n     + \t\t\t\t       git_path_shallow(the_repository),\n     +-\t\t\t\t       LOCK_DIE_ON_ERROR);\n     ++\t\t\t\t       LOCK_DIE_ON_ERROR,\n     ++\t\t\t\t       LOCKFILE_PID_SHALLOW);\n     + \tcheck_shallow_file_for_update(the_repository);\n     + \tif (write_shallow_commits_1(&sb, 0, NULL, flags)) {\n     + \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n     +\n       ## t/meson.build ##\n      @@ t/meson.build: integration_tests = [\n         't0028-working-tree-encoding.sh',\n     @@ t/t0031-lockfile-pid.sh (new)\n      +test_description='lock file PID info tests\n      +\n      +Tests for PID info file alongside lock files.\n     -+The feature is opt-in via GIT_LOCK_PID_INFO=1.\n     ++The feature is opt-in via core.lockfilePid config setting.\n      +'\n      +\n      +. ./test-lib.sh\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\techo \"99999\" >.git/index.lock.pid &&\n     -+\t\ttest_must_fail env GIT_LOCK_PID_INFO=1 git add . 2>err &&\n     ++\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\ttest_must_fail git -c core.lockfilePid=index add . 2>err &&\n      +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n      +\t\ttest_grep \"Remove the stale lock file\" err\n      +\t)\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo2 &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\techo \"99999\" >.git/index.lock.pid &&\n     ++\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n      +\t\ttest_must_fail git add . 2>err &&\n      +\t\t# Should not crash, just show normal error without PID\n      +\t\ttest_grep \"Unable to create\" err &&\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo3 &&\n      +\t\techo content >file &&\n     ++\t\t# Get the correct PID for this platform\n     ++\t\tshell_pid=$$ &&\n     ++\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n     ++\t\tthen\n     ++\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n     ++\t\t\t# uses Windows PIDs. Use the Windows PID.\n     ++\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n     ++\t\tfi &&\n      +\t\t# Create a lock and PID file with current shell PID (which is running)\n      +\t\ttouch .git/index.lock &&\n     -+\t\techo $$ >.git/index.lock.pid &&\n     ++\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index.pid.lock &&\n      +\t\t# Verify our PID is shown in the error message\n     -+\t\ttest_must_fail env GIT_LOCK_PID_INFO=1 git add file 2>err &&\n     -+\t\ttest_grep \"held by process $$\" err\n     ++\t\ttest_must_fail git -c core.lockfilePid=index add file 2>err &&\n     ++\t\ttest_grep \"held by process $shell_pid\" err\n      +\t)\n      +'\n      +\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo4 &&\n      +\t\techo content >file &&\n     -+\t\tenv GIT_LOCK_PID_INFO=1 git add file &&\n     ++\t\tgit -c core.lockfilePid=index add file &&\n      +\t\t# After successful add, no lock or PID files should exist\n     -+\t\t! test -f .git/index.lock &&\n     -+\t\t! test -f .git/index.lock.pid\n     ++\t\ttest_path_is_missing .git/index.lock &&\n     ++\t\ttest_path_is_missing .git/index.pid.lock\n      +\t)\n      +'\n      +\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\techo content >file &&\n      +\t\tgit add file &&\n      +\t\t# PID file should not be created when feature is disabled\n     -+\t\t! test -f .git/index.lock.pid\n     ++\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'core.lockfilePid=all enables for all components' '\n     ++\tgit init repo6 &&\n     ++\t(\n     ++\t\tcd repo6 &&\n     ++\t\ttouch .git/index.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\ttest_must_fail git -c core.lockfilePid=all add . 2>err &&\n     ++\t\ttest_grep \"process 99999\" err\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'multiple components can be specified' '\n     ++\tgit init repo8 &&\n     ++\t(\n     ++\t\tcd repo8 &&\n     ++\t\ttouch .git/index.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\ttest_must_fail git -c core.lockfilePid=index,config add . 2>err &&\n     ++\t\ttest_grep \"process 99999\" err\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'core.lockfilePid=none does not create PID file' '\n     ++\tgit init repo9 &&\n     ++\t(\n     ++\t\tcd repo9 &&\n     ++\t\techo content >file &&\n     ++\t\tgit -c core.lockfilePid=none add file &&\n     ++\t\t# PID file should not be created when feature is disabled\n     ++\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'existing PID files are read even when feature disabled' '\n     ++\tgit init repo10 &&\n     ++\t(\n     ++\t\tcd repo10 &&\n     ++\t\ttouch .git/index.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\t# Even with lockfilePid disabled, existing PID files are read\n     ++\t\t# to help diagnose stale locks\n     ++\t\ttest_must_fail git add . 2>err &&\n     ++\t\ttest_grep \"process 99999\" err\n     ++\t)\n     ++'\n     ++\n     ++test_expect_success 'negative component syntax excludes specific components' '\n     ++\tgit init repo11 &&\n     ++\t(\n     ++\t\tcd repo11 &&\n     ++\t\techo content >file &&\n     ++\t\t# Enable all components except index\n     ++\t\tgit -c core.lockfilePid=all,-index add file &&\n     ++\t\t# PID file should not be created for index when excluded\n     ++\t\ttest_path_is_missing .git/index.pid.lock\n      +\t)\n      +'\n      +\n      +test_done\n     +\n     + ## unix-stream-server.c ##\n     +@@ unix-stream-server.c: int unix_ss_create(const char *path,\n     + \t/*\n     + \t * Create a lock at \"<path>.lock\" if we can.\n     + \t */\n     +-\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms) < 0)\n     ++\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms,\n     ++\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0)\n     + \t\treturn -1;\n     + \n     + \t/*\n\n\n Documentation/config/core.adoc |  26 +++++\n apply.c                        |   6 +-\n builtin/commit.c               |   2 +-\n builtin/credential-store.c     |   3 +-\n builtin/difftool.c             |   3 +-\n builtin/fast-import.c          |   3 +-\n builtin/gc.c                   |  13 ++-\n builtin/sparse-checkout.c      |   3 +-\n bundle.c                       |   3 +-\n cache-tree.c                   |   3 +-\n commit-graph.c                 |   6 +-\n compat/mingw.c                 |  10 ++\n config.c                       |   6 +-\n environment.c                  |  80 +++++++++++++++\n lockfile.c                     | 182 +++++++++++++++++++++++++++++----\n lockfile.h                     |  77 ++++++++++----\n loose.c                        |   6 +-\n midx-write.c                   |   6 +-\n odb.c                          |   3 +-\n refs/files-backend.c           |   9 +-\n refs/packed-backend.c          |   3 +-\n reftable/system.c              |   2 +-\n repository.c                   |   3 +-\n rerere.c                       |   3 +-\n sequencer.c                    |  11 +-\n shallow.c                      |   6 +-\n t/meson.build                  |   1 +\n t/t0031-lockfile-pid.sh        | 139 +++++++++++++++++++++++++\n unix-stream-server.c           |   3 +-\n 29 files changed, 546 insertions(+), 75 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex 11efad189e..45597b9cea 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -348,6 +348,32 @@ confusion unless you know what you are doing (e.g. you are creating a\n read-only snapshot of the same index to a location different from the\n repository's usual working tree).\n \n+core.lockfilePid::\n+\tA comma-separated list of components for which Git should create\n+\ta PID file alongside the lock file. When a lock acquisition fails\n+\tand a PID file exists, Git can provide additional diagnostic\n+\tinformation about the process holding the lock, including whether\n+\tit is still running.\n++\n+This feature is disabled by default. You can enable it for specific\n+components or use `all` to enable for all components.\n++\n+* `none` disables PID file creation for all components.\n+* `index` creates PID files for index lock operations.\n+* `config` creates PID files for config file lock operations.\n+* `refs` creates PID files for reference lock operations.\n+* `commit-graph` creates PID files for commit-graph lock operations.\n+* `midx` creates PID files for multi-pack-index lock operations.\n+* `shallow` creates PID files for shallow file lock operations.\n+* `gc` creates PID files for garbage collection lock operations.\n+* `other` creates PID files for other miscellaneous lock operations.\n+* `all` enables PID file creation for all components.\n++\n+The PID file is named by inserting `.pid` before the `.lock` suffix.\n+For example, if the lock file is `index.lock`, the PID file will be\n+`index.pid.lock`. The file contains `pid <value>` on a single line,\n+following the same key-value format used by Git object headers.\n+\n core.logAllRefUpdates::\n \tEnable the reflog. Updates to a ref <ref> is logged to the file\n \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\ndiff --git a/apply.c b/apply.c\nindex a2ceb3fb40..95e3bafabd 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -4173,7 +4173,8 @@ static int build_fake_ancestor(struct apply_state *state, struct patch *list)\n \t\t}\n \t}\n \n-\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \tres = write_locked_index(&result, &lock, COMMIT_LOCK);\n \tdiscard_index(&result);\n \n@@ -4830,7 +4831,8 @@ static int apply_patch(struct apply_state *state,\n \t\tif (state->index_file)\n \t\t\thold_lock_file_for_update(&state->lock_file,\n \t\t\t\t\t\t  state->index_file,\n-\t\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t\t  LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t  LOCKFILE_PID_INDEX);\n \t\telse\n \t\t\trepo_hold_locked_index(state->repo, &state->lock_file,\n \t\t\t\t\t       LOCK_DIE_ON_ERROR);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 0243f17d53..30db6409a1 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -540,7 +540,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n \t\t\t     (uintmax_t) getpid());\n \thold_lock_file_for_update(&false_lock, path,\n-\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_OTHER);\n \n \tcreate_base_index(current_head);\n \tadd_remove_files(&partial);\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..e1d1be5cd1 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -67,7 +67,8 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \tint timeout_ms = 1000;\n \n \trepo_config_get_int(the_repository, \"credentialstore.locktimeoutms\", &timeout_ms);\n-\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms) < 0)\n+\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms,\n+\t\t\t\t\t      LOCKFILE_PID_CONFIG) < 0)\n \t\tdie_errno(_(\"unable to get credential storage lock in %d ms\"), timeout_ms);\n \tif (extra)\n \t\tprint_line(extra);\ndiff --git a/builtin/difftool.c b/builtin/difftool.c\nindex e4bc1f8316..15d0af0031 100644\n--- a/builtin/difftool.c\n+++ b/builtin/difftool.c\n@@ -636,7 +636,8 @@ static int run_dir_diff(struct repository *repo,\n \t\t\tstruct lock_file lock = LOCK_INIT;\n \t\t\tstrbuf_reset(&buf);\n \t\t\tstrbuf_addf(&buf, \"%s/wtindex\", tmpdir.buf);\n-\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||\n+\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0,\n+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0 ||\n \t\t\t    write_locked_index(&wtindex, &lock, COMMIT_LOCK)) {\n \t\t\t\tret = error(\"could not write %s\", buf.buf);\n \t\t\t\tgoto finish;\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 4cd0b079b6..9715f04d2d 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -1734,7 +1734,8 @@ static void dump_marks(void)\n \t\treturn;\n \t}\n \n-\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {\n+\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0,\n+\t\t\t\t      LOCKFILE_PID_OTHER) < 0) {\n \t\tfailure |= error_errno(_(\"unable to write marks file %s\"),\n \t\t\t\t       export_marks_file);\n \t\treturn;\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e9a76243aa..6ba56d784d 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -749,7 +749,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n \n \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n \tfd = hold_lock_file_for_update(&lock, pidfile_path,\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n \tif (!force) {\n \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n \t\tstatic char *scan_fmt;\n@@ -1017,7 +1017,7 @@ int cmd_gc(int argc,\n \tif (daemonized) {\n \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n \t\thold_lock_file_for_update(&log_lock, path,\n-\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n \t\tdup2(get_lock_file_fd(&log_lock), 2);\n \t\tatexit(process_log_file_at_exit);\n \t\tfree(path);\n@@ -1797,7 +1797,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \tstruct repository *r = the_repository;\n \tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->sources->path);\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n \t\t/*\n \t\t * Another maintenance command is running.\n \t\t *\n@@ -2561,7 +2562,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \t\tlock_file_timeout_ms = 150;\n \n \tfd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,\n-\t\t\t\t\t       lock_file_timeout_ms);\n+\t\t\t\t\t       lock_file_timeout_ms,\n+\t\t\t\t\t       LOCKFILE_PID_GC);\n \n \t/*\n \t * Does this file already exist? With the intended contents? Is it\n@@ -3372,7 +3374,8 @@ static int update_background_schedule(const struct maintenance_start_opts *opts,\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->sources->path);\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n \t\tif (errno == EEXIST)\n \t\t\terror(_(\"unable to create '%s.lock': %s.\\n\\n\"\n \t\t\t    \"Another scheduled git-maintenance(1) process seems to be running in this\\n\"\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex 15d51e60a8..566df7d229 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -341,7 +341,8 @@ static int write_patterns_and_update(struct repository *repo,\n \tif (safe_create_leading_directories(repo, sparse_filename))\n \t\tdie(_(\"failed to create directory for sparse-checkout file\"));\n \n-\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \n \tresult = update_working_directory(repo, pl);\n \tif (result) {\ndiff --git a/bundle.c b/bundle.c\nindex 42327f9739..25728278fc 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -520,7 +520,8 @@ int create_bundle(struct repository *r, const char *path,\n \t\tbundle_fd = 1;\n \telse\n \t\tbundle_fd = hold_lock_file_for_update(&lock, path,\n-\t\t\t\t\t\t      LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t\t      LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t      LOCKFILE_PID_OTHER);\n \n \tif (version == -1)\n \t\tversion = min_version;\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 2aba47060e..b370fa94d0 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -730,7 +730,8 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state,\n \tstruct lock_file lock_file = LOCK_INIT;\n \tint ret;\n \n-\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_INDEX);\n \n \tentries = read_index_from(index_state, index_path,\n \t\t\t\t  repo_get_git_dir(the_repository));\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 80be2ff2c3..b18249fb73 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -2075,7 +2075,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tchar *lock_name = get_commit_graph_chain_filename(ctx->odb_source);\n \n \t\thold_lock_file_for_update_mode(&lk, lock_name,\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n \t\tfree(lock_name);\n \n \t\tgraph_layer = mks_tempfile_m(ctx->graph_name, 0444);\n@@ -2094,7 +2095,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\t   get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer));\n \t} else {\n \t\thold_lock_file_for_update_mode(&lk, ctx->graph_name,\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n \t\tf = hashfd(ctx->r->hash_algo,\n \t\t\t   get_lock_file_fd(&lk), get_lock_file_path(&lk));\n \t}\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 736a07a028..cbd102fa0e 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1982,6 +1982,16 @@ int mingw_kill(pid_t pid, int sig)\n \t\t\tCloseHandle(h);\n \t\t\treturn 0;\n \t\t}\n+\t\t/*\n+\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n+\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n+\t\t * compatibility with kill(pid, 0).\n+\t\t */\n+\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n+\t\t\terrno = ESRCH;\n+\t\telse\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n \t}\n \n \terrno = EINVAL;\ndiff --git a/config.c b/config.c\nindex f1def0dcfb..ac35427523 100644\n--- a/config.c\n+++ b/config.c\n@@ -2985,7 +2985,8 @@ int repo_config_set_multivar_in_file_gently(struct repository *r,\n \t * The lock serves a purpose in addition to locking: the new\n \t * contents of .git/config will be written into it.\n \t */\n-\tfd = hold_lock_file_for_update(&lock, config_filename, 0);\n+\tfd = hold_lock_file_for_update(&lock, config_filename, 0,\n+\t\t\t\t       LOCKFILE_PID_CONFIG);\n \tif (fd < 0) {\n \t\terror_errno(_(\"could not lock config file %s\"), config_filename);\n \t\tret = CONFIG_NO_LOCK;\n@@ -3330,7 +3331,8 @@ static int repo_config_copy_or_rename_section_in_file(\n \tif (!config_filename)\n \t\tconfig_filename = filename_buf = repo_git_path(r, \"config\");\n \n-\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0);\n+\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0,\n+\t\t\t\t\t   LOCKFILE_PID_CONFIG);\n \tif (out_fd < 0) {\n \t\tret = error(_(\"could not lock config file %s\"), config_filename);\n \t\tgoto out;\ndiff --git a/environment.c b/environment.c\nindex a770b5921d..0d1dfb525b 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -21,6 +21,7 @@\n #include \"gettext.h\"\n #include \"git-zlib.h\"\n #include \"ident.h\"\n+#include \"lockfile.h\"\n #include \"mailmap.h\"\n #include \"object-name.h\"\n #include \"repository.h\"\n@@ -324,6 +325,78 @@ next_name:\n \treturn (current & ~negative) | positive;\n }\n \n+static const struct lockfile_pid_component_name {\n+\tconst char *name;\n+\tenum lockfile_pid_component component_bits;\n+} lockfile_pid_component_names[] = {\n+\t{ \"index\", LOCKFILE_PID_INDEX },\n+\t{ \"config\", LOCKFILE_PID_CONFIG },\n+\t{ \"refs\", LOCKFILE_PID_REFS },\n+\t{ \"commit-graph\", LOCKFILE_PID_COMMIT_GRAPH },\n+\t{ \"midx\", LOCKFILE_PID_MIDX },\n+\t{ \"shallow\", LOCKFILE_PID_SHALLOW },\n+\t{ \"gc\", LOCKFILE_PID_GC },\n+\t{ \"other\", LOCKFILE_PID_OTHER },\n+\t{ \"all\", LOCKFILE_PID_ALL },\n+};\n+\n+static enum lockfile_pid_component parse_lockfile_pid_components(const char *var,\n+\t\t\t\t\t\t\t\t const char *string)\n+{\n+\tenum lockfile_pid_component current = LOCKFILE_PID_DEFAULT;\n+\tenum lockfile_pid_component positive = 0, negative = 0;\n+\n+\twhile (string) {\n+\t\tsize_t len;\n+\t\tconst char *ep;\n+\t\tint negated = 0;\n+\t\tint found = 0;\n+\n+\t\tstring = string + strspn(string, \", \\t\\n\\r\");\n+\t\tep = strchrnul(string, ',');\n+\t\tlen = ep - string;\n+\t\tif (len == 4 && !strncmp(string, \"none\", 4)) {\n+\t\t\tcurrent = LOCKFILE_PID_NONE;\n+\t\t\tgoto next_name;\n+\t\t}\n+\n+\t\tif (*string == '-') {\n+\t\t\tnegated = 1;\n+\t\t\tstring++;\n+\t\t\tlen--;\n+\t\t\tif (!len)\n+\t\t\t\twarning(_(\"invalid value for variable %s\"), var);\n+\t\t}\n+\n+\t\tif (!len)\n+\t\t\tbreak;\n+\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(lockfile_pid_component_names); ++i) {\n+\t\t\tconst struct lockfile_pid_component_name *n = &lockfile_pid_component_names[i];\n+\n+\t\t\tif (strncmp(n->name, string, len) || strlen(n->name) != len)\n+\t\t\t\tcontinue;\n+\n+\t\t\tfound = 1;\n+\t\t\tif (negated)\n+\t\t\t\tnegative |= n->component_bits;\n+\t\t\telse\n+\t\t\t\tpositive |= n->component_bits;\n+\t\t}\n+\n+\t\tif (!found) {\n+\t\t\tchar *component = xstrndup(string, len);\n+\t\t\twarning(_(\"ignoring unknown core.lockfilePid component '%s'\"), component);\n+\t\t\tfree(component);\n+\t\t}\n+\n+next_name:\n+\t\tstring = ep;\n+\t}\n+\n+\treturn (current & ~negative) | positive;\n+}\n+\n static int git_default_core_config(const char *var, const char *value,\n \t\t\t\t   const struct config_context *ctx, void *cb)\n {\n@@ -532,6 +605,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"core.lockfilepid\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tlockfile_pid_components = parse_lockfile_pid_components(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"core.createobject\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..4ee215374a 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,19 +74,117 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a foo-pid.lock file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ *\n+ * Naming: For \"foo.lock\", the PID file is \"foo-pid.lock\" (not \"foo.lock.pid\").\n+ * This avoids collision with the refs namespace.\n+ */\n+\n+/* Global config variable, initialized from core.lockfilePid */\n+enum lockfile_pid_component lockfile_pid_components = LOCKFILE_PID_DEFAULT;\n+\n+/*\n+ * Path generation helpers.\n+ * Given base path \"foo\", generate:\n+ *   - lock path: \"foo.lock\"\n+ *   - pid path:  \"foo-pid.lock\"\n+ */\n+static void get_lock_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static void get_pid_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_PID_INFIX);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode,\n+\t\t\t\t\t     enum lockfile_pid_component component)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd = -1;\n+\n+\tif (!(lockfile_pid_components & component))\n+\t\tgoto out;\n+\n+\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n+\tif (fd < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n+\tif (write_in_full(fd, content.buf, content.len) < 0) {\n+\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n+\t\tgoto out;\n+\t}\n+\n+\tclose(fd);\n+\tfd = -1;\n+\tpid_tempfile = register_tempfile(pid_path);\n+\n+out:\n+\tif (fd >= 0)\n+\t\tclose(fd);\n+\tstrbuf_release(&content);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tconst char *val;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n+\t\tgoto out;\n+\n+\tstrbuf_rtrim(&content);\n+\n+\tif (skip_prefix(content.buf, \"pid \", &val)) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(val, &endptr, 10);\n+\t\tif (*pid_out > 0 && !*endptr)\n+\t\t\tret = 0;\n+\t}\n+\n+\tif (ret)\n+\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n-\t\t     int mode)\n+\t\t     int mode, enum lockfile_pid_component component)\n {\n-\tstruct strbuf filename = STRBUF_INIT;\n+\tstruct strbuf base_path = STRBUF_INIT;\n+\tstruct strbuf lock_path = STRBUF_INIT;\n+\tstruct strbuf pid_path = STRBUF_INIT;\n \n-\tstrbuf_addstr(&filename, path);\n+\tstrbuf_addstr(&base_path, path);\n \tif (!(flags & LOCK_NO_DEREF))\n-\t\tresolve_symlink(&filename);\n+\t\tresolve_symlink(&base_path);\n+\n+\tget_lock_path(&lock_path, base_path.buf);\n+\tget_pid_path(&pid_path, base_path.buf);\n+\n+\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode,\n+\t\t\t\t\t\t\tcomponent);\n \n-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n-\tstrbuf_release(&filename);\n+\tstrbuf_release(&base_path);\n+\tstrbuf_release(&lock_path);\n+\tstrbuf_release(&pid_path);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n \n@@ -102,7 +203,8 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,\n  * exactly once. If timeout_ms is -1, try indefinitely.\n  */\n static int lock_file_timeout(struct lock_file *lk, const char *path,\n-\t\t\t     int flags, long timeout_ms, int mode)\n+\t\t\t     int flags, long timeout_ms, int mode,\n+\t\t\t     enum lockfile_pid_component component)\n {\n \tint n = 1;\n \tint multiplier = 1;\n@@ -110,7 +212,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n \tstatic int random_initialized = 0;\n \n \tif (timeout_ms == 0)\n-\t\treturn lock_file(lk, path, flags, mode);\n+\t\treturn lock_file(lk, path, flags, mode, component);\n \n \tif (!random_initialized) {\n \t\tsrand((unsigned int)getpid());\n@@ -124,7 +226,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n \t\tlong backoff_ms, wait_ms;\n \t\tint fd;\n \n-\t\tfd = lock_file(lk, path, flags, mode);\n+\t\tfd = lock_file(lk, path, flags, mode, component);\n \n \t\tif (fd >= 0)\n \t\t\treturn fd; /* success */\n@@ -151,16 +253,47 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n-\t} else\n+\t\tconst char *abs_path = absolute_path(path);\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tstruct strbuf pid_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tget_lock_path(&lock_path, abs_path);\n+\t\tget_pid_path(&pid_path, abs_path);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\t/*\n+\t\t * Try to read PID file unconditionally - it may exist if\n+\t\t * core.lockfilePid was enabled for this component.\n+\t\t */\n+\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n+\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n+\t\t\telse if (errno == ESRCH)\n+\t\t\t\tpid_status = -1; /* no such process - stale lock */\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n+\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n+\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n+\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n+\n+\t\tstrbuf_release(&lock_path);\n+\t\tstrbuf_release(&pid_path);\n+\t} else {\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n+\t}\n }\n \n NORETURN void unable_to_lock_die(const char *path, int err)\n@@ -174,9 +307,10 @@ NORETURN void unable_to_lock_die(const char *path, int err)\n /* This should return a meaningful errno on failure */\n int hold_lock_file_for_update_timeout_mode(struct lock_file *lk,\n \t\t\t\t\t   const char *path, int flags,\n-\t\t\t\t\t   long timeout_ms, int mode)\n+\t\t\t\t\t   long timeout_ms, int mode,\n+\t\t\t\t\t   enum lockfile_pid_component component)\n {\n-\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode);\n+\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode, component);\n \tif (fd < 0) {\n \t\tif (flags & LOCK_DIE_ON_ERROR)\n \t\t\tunable_to_lock_die(path, errno);\n@@ -207,6 +341,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +352,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..7e472cb5af 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,42 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/*\n+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo.pid.lock\".\n+ */\n+#define LOCK_PID_INFIX \".pid\"\n+#define LOCK_PID_INFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/*\n+ * Per-component lock PID file configuration, following core.fsync pattern.\n+ * Each component can be individually enabled via core.lockfilePid config.\n+ */\n+enum lockfile_pid_component {\n+\tLOCKFILE_PID_NONE = 0,\n+\tLOCKFILE_PID_INDEX = 1 << 0, /* .git/index.lock */\n+\tLOCKFILE_PID_CONFIG = 1 << 1, /* .git/config.lock */\n+\tLOCKFILE_PID_REFS = 1 << 2, /* refs locks */\n+\tLOCKFILE_PID_COMMIT_GRAPH = 1 << 3, /* commit-graph.lock */\n+\tLOCKFILE_PID_MIDX = 1 << 4, /* multi-pack-index.lock */\n+\tLOCKFILE_PID_SHALLOW = 1 << 5, /* shallow file */\n+\tLOCKFILE_PID_GC = 1 << 6, /* gc locks */\n+\tLOCKFILE_PID_OTHER = 1 << 7, /* other locks */\n+};\n+\n+#define LOCKFILE_PID_ALL (LOCKFILE_PID_INDEX | LOCKFILE_PID_CONFIG |      \\\n+\t\t\t  LOCKFILE_PID_REFS | LOCKFILE_PID_COMMIT_GRAPH | \\\n+\t\t\t  LOCKFILE_PID_MIDX | LOCKFILE_PID_SHALLOW |      \\\n+\t\t\t  LOCKFILE_PID_GC | LOCKFILE_PID_OTHER)\n+#define LOCKFILE_PID_DEFAULT LOCKFILE_PID_NONE\n+\n+/*\n+ * Bitmask indicating which components should create PID files.\n+ * Configured via core.lockfilePid.\n+ */\n+extern enum lockfile_pid_component lockfile_pid_components;\n \n /*\n  * Flags\n@@ -167,17 +204,23 @@ struct lock_file {\n  * timeout_ms milliseconds. If timeout_ms is 0, try exactly once; if\n  * timeout_ms is -1, retry indefinitely. The flags argument, error\n  * handling, and mode are described above.\n+ *\n+ * The `component` argument specifies which lock PID component this lock\n+ * belongs to, for selective PID file creation via core.lockfilePid config.\n  */\n int hold_lock_file_for_update_timeout_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms, int mode);\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms, int mode,\n+\tenum lockfile_pid_component component);\n \n static inline int hold_lock_file_for_update_timeout(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms,\n+\tenum lockfile_pid_component component)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n-\t\t\t\t\t\t      timeout_ms, 0666);\n+\t\t\t\t\t\t      timeout_ms, 0666,\n+\t\t\t\t\t\t      component);\n }\n \n /*\n@@ -186,17 +229,18 @@ static inline int hold_lock_file_for_update_timeout(\n  * argument and error handling are described above.\n  */\n static inline int hold_lock_file_for_update(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, enum lockfile_pid_component component)\n {\n-\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n+\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0, component);\n }\n \n static inline int hold_lock_file_for_update_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, int mode)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, int mode, enum lockfile_pid_component component)\n {\n-\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n+\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode,\n+\t\t\t\t\t\t      component);\n }\n \n /*\n@@ -319,13 +363,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/loose.c b/loose.c\nindex 56cf64b648..c0d2f0416f 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -135,7 +135,8 @@ int repo_write_loose_object_map(struct repository *repo)\n \t\treturn 0;\n \n \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n-\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n+\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \titer = kh_begin(map);\n \tif (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)\n \t\tgoto errout;\n@@ -177,7 +178,8 @@ static int write_one_object(struct odb_source *source,\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \n \tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n-\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n+\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n+\t\t\t\t\t  LOCKFILE_PID_OTHER);\n \n \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n \tif (fd < 0)\ndiff --git a/midx-write.c b/midx-write.c\nindex 23e61cb000..6196dcd7cd 100644\n--- a/midx-write.c\n+++ b/midx-write.c\n@@ -1309,7 +1309,8 @@ static int write_midx_internal(struct odb_source *source,\n \t\tstruct strbuf lock_name = STRBUF_INIT;\n \n \t\tget_midx_chain_filename(source, &lock_name);\n-\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n \t\tstrbuf_release(&lock_name);\n \n \t\tincr = mks_tempfile_m(midx_name.buf, 0444);\n@@ -1327,7 +1328,8 @@ static int write_midx_internal(struct odb_source *source,\n \t\tf = hashfd(r->hash_algo, get_tempfile_fd(incr),\n \t\t\t   get_tempfile_path(incr));\n \t} else {\n-\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n \t\tf = hashfd(r->hash_algo, get_lock_file_fd(&lk),\n \t\t\t   get_lock_file_path(&lk));\n \t}\ndiff --git a/odb.c b/odb.c\nindex 3ec21ef24e..570cea3029 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -292,7 +292,8 @@ void odb_add_to_alternates_file(struct object_database *odb,\n \tFILE *in, *out;\n \tint found = 0;\n \n-\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \tout = fdopen_lock_file(&lock, \"w\");\n \tif (!out)\n \t\tdie_errno(_(\"unable to fdopen alternates lockfile\"));\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 6f6f76a8d8..e296154fe7 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -790,7 +790,8 @@ retry:\n \n \tif (hold_lock_file_for_update_timeout(\n \t\t\t    &lock->lk, ref_file.buf, LOCK_NO_DEREF,\n-\t\t\t    get_files_ref_lock_timeout_ms()) < 0) {\n+\t\t\t    get_files_ref_lock_timeout_ms(),\n+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n \t\tint myerr = errno;\n \t\terrno = 0;\n \t\tif (myerr == ENOENT && --attempts_remaining > 0) {\n@@ -1194,7 +1195,8 @@ static int create_reflock(const char *path, void *cb)\n \n \treturn hold_lock_file_for_update_timeout(\n \t\t\tlk, path, LOCK_NO_DEREF,\n-\t\t\tget_files_ref_lock_timeout_ms()) < 0 ? -1 : 0;\n+\t\t\tget_files_ref_lock_timeout_ms(),\n+\t\t\tLOCKFILE_PID_REFS) < 0 ? -1 : 0;\n }\n \n /*\n@@ -3536,7 +3538,8 @@ static int files_reflog_expire(struct ref_store *ref_store,\n \t\t * work we need, including cleaning up if the program\n \t\t * exits unexpectedly.\n \t\t */\n-\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {\n+\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0,\n+\t\t\t\t\t      LOCKFILE_PID_REFS) < 0) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n \t\t\tunable_to_lock_message(log_file, errno, &err);\n \t\t\terror(\"%s\", err.buf);\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 4ea0c12299..f832c49eae 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1230,7 +1230,8 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)\n \tif (hold_lock_file_for_update_timeout(\n \t\t\t    &refs->lock,\n \t\t\t    refs->path,\n-\t\t\t    flags, timeout_value) < 0) {\n+\t\t\t    flags, timeout_value,\n+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n \t\tunable_to_lock_message(refs->path, errno, err);\n \t\treturn -1;\n \t}\ndiff --git a/reftable/system.c b/reftable/system.c\nindex 725a25844e..5462111a04 100644\n--- a/reftable/system.c\n+++ b/reftable/system.c\n@@ -67,7 +67,7 @@ int flock_acquire(struct reftable_flock *l, const char *target_path,\n \t\treturn REFTABLE_OUT_OF_MEMORY_ERROR;\n \n \terr = hold_lock_file_for_update_timeout(lockfile, target_path, LOCK_NO_DEREF,\n-\t\t\t\t\t\ttimeout_ms);\n+\t\t\t\t\t\ttimeout_ms, LOCKFILE_PID_REFS);\n \tif (err < 0) {\n \t\treftable_free(lockfile);\n \t\tif (errno == EEXIST)\ndiff --git a/repository.c b/repository.c\nindex 6aaa7ba008..30dde7076b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -460,5 +460,6 @@ int repo_hold_locked_index(struct repository *repo,\n {\n \tif (!repo->index_file)\n \t\tBUG(\"the repo hasn't been setup\");\n-\treturn hold_lock_file_for_update(lf, repo->index_file, flags);\n+\treturn hold_lock_file_for_update(lf, repo->index_file, flags,\n+\t\t\t\t\t LOCKFILE_PID_INDEX);\n }\ndiff --git a/rerere.c b/rerere.c\nindex 6ec55964e2..71f1d5b176 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -917,7 +917,8 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \telse\n \t\tfd = hold_lock_file_for_update(&write_lock,\n \t\t\t\t\t       git_path_merge_rr(r),\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\ndiff --git a/sequencer.c b/sequencer.c\nindex 5476d39ba9..337f82e02c 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -566,7 +566,8 @@ static int write_message(const void *buf, size_t len, const char *filename,\n {\n \tstruct lock_file msg_file = LOCK_INIT;\n \n-\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0);\n+\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \tif (msg_fd < 0)\n \t\treturn error_errno(_(\"could not lock '%s'\"), filename);\n \tif (write_in_full(msg_fd, buf, len) < 0) {\n@@ -3605,7 +3606,8 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts,\n \tif (is_rebase_i(opts) && !reschedule)\n \t\tnext++;\n \n-\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0);\n+\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0,\n+\t\t\t\t       LOCKFILE_PID_OTHER);\n \tif (fd < 0)\n \t\treturn error_errno(_(\"could not lock '%s'\"), todo_path);\n \toffset = get_item_line_offset(todo_list, next);\n@@ -3877,7 +3879,8 @@ static int safe_append(const char *filename, const char *fmt, ...)\n \tva_list ap;\n \tstruct lock_file lock = LOCK_INIT;\n \tint fd = hold_lock_file_for_update(&lock, filename,\n-\t\t\t\t\t   LOCK_REPORT_ON_ERROR);\n+\t\t\t\t\t   LOCK_REPORT_ON_ERROR,\n+\t\t\t\t\t   LOCKFILE_PID_OTHER);\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tif (fd < 0)\n@@ -4400,7 +4403,7 @@ static int write_update_refs_state(struct string_list *refs_to_oids)\n \t\tgoto cleanup;\n \t}\n \n-\tif (hold_lock_file_for_update(&lock, path, 0) < 0) {\n+\tif (hold_lock_file_for_update(&lock, path, 0, LOCKFILE_PID_OTHER) < 0) {\n \t\tresult = error(_(\"another 'rebase' process appears to be running; \"\n \t\t\t\t \"'%s.lock' already exists\"),\n \t\t\t       path);\ndiff --git a/shallow.c b/shallow.c\nindex 55b9cd9d3f..412c37c96a 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -392,7 +392,8 @@ void setup_alternate_shallow(struct shallow_lock *shallow_lock,\n \n \tfd = hold_lock_file_for_update(&shallow_lock->lock,\n \t\t\t\t       git_path_shallow(the_repository),\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n \tcheck_shallow_file_for_update(the_repository);\n \tif (write_shallow_commits(&sb, 0, extra)) {\n \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n@@ -447,7 +448,8 @@ void prune_shallow(unsigned options)\n \t}\n \tfd = hold_lock_file_for_update(&shallow_lock.lock,\n \t\t\t\t       git_path_shallow(the_repository),\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n \tcheck_shallow_file_for_update(the_repository);\n \tif (write_shallow_commits_1(&sb, 0, NULL, flags)) {\n \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\ndiff --git a/t/meson.build b/t/meson.build\nindex dc43d69636..9a880db57c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..d99f738cb3\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,139 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via core.lockfilePid config setting.\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=index add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"Remove the stale lock file\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Get the correct PID for this platform\n+\t\tshell_pid=$$ &&\n+\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n+\t\tthen\n+\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n+\t\t\t# uses Windows PIDs. Use the Windows PID.\n+\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n+\t\tfi &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index.pid.lock &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail git -c core.lockfilePid=index add file 2>err &&\n+\t\ttest_grep \"held by process $shell_pid\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=index add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\ttest_path_is_missing .git/index.lock &&\n+\t\ttest_path_is_missing .git/index.pid.lock\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index.pid.lock\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=all enables for all components' '\n+\tgit init repo6 &&\n+\t(\n+\t\tcd repo6 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=all add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'multiple components can be specified' '\n+\tgit init repo8 &&\n+\t(\n+\t\tcd repo8 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=index,config add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=none does not create PID file' '\n+\tgit init repo9 &&\n+\t(\n+\t\tcd repo9 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=none add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index.pid.lock\n+\t)\n+'\n+\n+test_expect_success 'existing PID files are read even when feature disabled' '\n+\tgit init repo10 &&\n+\t(\n+\t\tcd repo10 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n+\t\t# Even with lockfilePid disabled, existing PID files are read\n+\t\t# to help diagnose stale locks\n+\t\ttest_must_fail git add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'negative component syntax excludes specific components' '\n+\tgit init repo11 &&\n+\t(\n+\t\tcd repo11 &&\n+\t\techo content >file &&\n+\t\t# Enable all components except index\n+\t\tgit -c core.lockfilePid=all,-index add file &&\n+\t\t# PID file should not be created for index when excluded\n+\t\ttest_path_is_missing .git/index.pid.lock\n+\t)\n+'\n+\n+test_done\ndiff --git a/unix-stream-server.c b/unix-stream-server.c\nindex 22ac2373e0..d8c79cd569 100644\n--- a/unix-stream-server.c\n+++ b/unix-stream-server.c\n@@ -47,7 +47,8 @@ int unix_ss_create(const char *path,\n \t/*\n \t * Create a lock at \"<path>.lock\" if we can.\n \t */\n-\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms) < 0)\n+\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms,\n+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0)\n \t\treturn -1;\n \n \t/*\n\nbase-commit: 6ab38b7e9cc7adafc304f3204616a4debd49c6e9\n-- \ngitgitgadget\n"},{"id":"532395","messageId":"xmqqh5tozl1i.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-18T00:32:57Z","receivedAt":"2025-12-18T00:33:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  /* Make sure errno contains a meaningful value on error */\n>  static int lock_file(struct lock_file *lk, const char *path, int flags,\n> -\t\t     int mode)\n> +\t\t     int mode, enum lockfile_pid_component component)\n> ...\n>  }\n>  \n> @@ -102,7 +203,8 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,\n>   * exactly once. If timeout_ms is -1, try indefinitely.\n>   */\n>  static int lock_file_timeout(struct lock_file *lk, const char *path,\n> -\t\t\t     int flags, long timeout_ms, int mode)\n> +\t\t\t     int flags, long timeout_ms, int mode,\n> +\t\t\t     enum lockfile_pid_component component)\n>  {\n> ...\n>  \tif (timeout_ms == 0)\n> -\t\treturn lock_file(lk, path, flags, mode);\n> +\t\treturn lock_file(lk, path, flags, mode, component);\n> -\t\tfd = lock_file(lk, path, flags, mode);\n> +\t\tfd = lock_file(lk, path, flags, mode, component);\n\nThese are OK, but I expected these are rolled into an \"unsigned\nflags\" word, so that ...\n\n>  int hold_lock_file_for_update_timeout_mode(\n> -\t\tstruct lock_file *lk, const char *path,\n> -\t\tint flags, long timeout_ms, int mode);\n> +\tstruct lock_file *lk, const char *path,\n> +\tint flags, long timeout_ms, int mode,\n> +\tenum lockfile_pid_component component);\n\n... things like this can be done without adding an extra parameter.\nCompared to \"what should we do when we see an error?\", ...\n\n> -\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n> +\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n> +\t\t\t\t\t       LOCKFILE_PID_OTHER);\n\n... \"how would we name the lockfile for this action?\" is *not* all\nthat special and should not occupy a separate parameter on its own.\n\nExisting \"flags\" argument being \"int\" not \"unsigned int\" is a\nhistorical mistake, by the way.\n\nBut maybe it is just me?  I dunno.\n\n"},{"id":"532396","messageId":"xmqqbjjwzkd4.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-18T00:47:35Z","receivedAt":"2025-12-18T00:47:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> The feature is controlled via core.lockfilePid configuration, which\n> accepts per-component values similar to core.fsync:\n>\n>   - none: Disable for all components (default)\n>   - all: Enable for all components\n>   - index, config, refs, commit-graph, midx, shallow, gc, other:\n>     Enable for specific components\n\nAs this is about lockfile, you need to decide what should happen\nwhen an older version of Git, which is unaware of this new world\norder where .git/index.pid.lock declares \".git/index is being\nupdated; you should not touch it!\", comes to the repository.  You,\nas a user of the updated Git, do want them to stop interferring with\nthe operation on the repository your new Git is making, and it means\nyou should have some way to telling them \"do not touch---you do not\neven understand what is in this repository!\".\n\nThe established way to do so is with a repository extension.  Having\ncore.lockfilePid that is a plain vanilla configuration variable\nwould not affect Git that is not aware of the variable, but unknown\nrepository extension will cause setup.c::verify_repository_format()\nto stop Git from making damage.  Perhaps use extensions.lockfilePID\ninstead?\n\nBy the way, we name multi-word configuration variables in CamelCase,\nbut PID (not \"process identifier\") is an already all-caps word, so\ncore.lockfilePid looks awkward.  Call it core.lockfilePID instead?\nOh, with \"core.lockfilePID\" -> \"extensions.lockfilePID\", perhaps.\n\n"},{"id":"532400","messageId":"xmqq7bukzi8j.fsf@gitster.g","threadId":"64568","inReplyTo":"xmqqbjjwzkd4.fsf@gitster.g","subject":"Re: [PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-18T01:33:32Z","receivedAt":"2025-12-18T01:33:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> As this is about lockfile, you need to decide what should happen\n> when an older version of Git, which is unaware of this new world\n> order where .git/index.pid.lock declares \".git/index is being\n> updated; you should not touch it!\", comes to the repository.  You,\n> as a user of the updated Git, do want them to stop interferring with\n> the operation on the repository your new Git is making, and it means\n> you should have some way to telling them \"do not touch---you do not\n> even understand what is in this repository!\".\n\nAh, sorry, I take this back.  We are not replacing index.lock with\nsomething else; we are adding index.pid.lock in addition; older\nversions of Git would simply ignore the new file, still notice\nindex.lock and stop.  So no need for extensions, either.\n\n"},{"id":"532408","messageId":"211B172F-303C-48F6-9B2E-ABF6DDBCA662@gmail.com","threadId":"64568","inReplyTo":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-18T03:38:24Z","receivedAt":"2025-12-18T03:38:36Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 17 déc. 2025 à 13:59, Paulo Casaretto via GitGitGadget <gitgitgadget@gmail.com> a écrit :\n> \n> ﻿From: Paulo Casaretto <pcasaretto@gmail.com>\n> \n> When a lock file is held, it can be helpful to know which process owns\n> it, especially when debugging stale locks left behind by crashed\n> processes. Add an optional feature that creates a companion PID file\n> alongside each lock file, containing the PID of the lock holder.\n> \n> For a lock file \"foo.lock\", the PID file is named \"foo.pid.lock\". The\n> file uses a simple key-value format (\"pid <value>\") following the same\n> pattern as Git object headers, making it extensible for future metadata.\n> \n> The PID file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n\nDo we also release locks when the process terminates abnormally? If not, this info would be lost during a crash, which I assume is when it would be very useful 🤔 I haven’t looked, just curiously wondering. "},{"id":"532443","messageId":"aUO2Pb5JQ678ELCf@pks.im","threadId":"64568","inReplyTo":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] lockfile: add PID file for debugging stale locks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-18T08:07:25Z","receivedAt":"2025-12-18T08:07:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Dec 17, 2025 at 06:59:26PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> diff --git a/lockfile.c b/lockfile.c\n> index 1d5ed01682..4ee215374a 100644\n> --- a/lockfile.c\n> +++ b/lockfile.c\n> @@ -71,19 +74,117 @@ static void resolve_symlink(struct strbuf *path)\n>  \tstrbuf_reset(&link);\n>  }\n>  \n> +/*\n> + * Lock PID file functions - write PID to a foo-pid.lock file alongside\n> + * the lock file for debugging stale locks. The PID file is registered\n> + * as a tempfile so it gets cleaned up by signal/atexit handlers.\n> + *\n> + * Naming: For \"foo.lock\", the PID file is \"foo-pid.lock\" (not \"foo.lock.pid\").\n> + * This avoids collision with the refs namespace.\n> + */\n\nThis neatly solves the issue that alternative implementations of Git\nwouldn't know to handle this new PID file. They already ignore every\nfile that ends with \".lock\", so of course they would also ignore these\nnew PID files.\n\nBut unfortunately this doesn't solve the other issue, which is that we\nnow have a new restriction: you now cannot have two references \"foo\" and\n\"foo-pid\" and modify them in the same transaction. Granted, this is a\nvery specific situation, and I doubt that 99.9% of all users would ever\nhit this restriction. But regardless of that I'm worried about the 0.1%\nthat _do_ hit this restriction now. So I'm afraid that this proposed\nnaming schema isn't going to work, which raises the question whether\nthere are any other alternative naming schemas that could.\n\nWe could (I'm not saying we should!) do some gross stuff here. For\nexample, refnames are not allowed to contain some specific characters in\ntheir names: \":?[\\\\^~ \\t*\" are all characters that are forbidden to\nexist in a refname. So in theory, we could call the lockfile for example\n\"foo:pid.lock\":\n\n  - We know that no reference \"foo:pid\" should exist because it contains\n    a forbidden character.\n\n  - We know that all alternative implementations should ignore it due to\n    the \".lock\" suffix.\n\nNote the \"should\" in both cases. I'd consider any implementation that\ndoesn't honor these \"shoulds\" to be buggy, but that doesn't mean that\nthere are no buggy implementations.\n\nIn any case though, this may be a possible way forward if we really want\nto also cover loose references. Whether we should is a different\nquestion though, and I'm not too sure about it myself:\n\n  - It will regress performance of the \"files\" reference backend as\n    every ref update now needs to write two files instead of one.\n    Writing many references is already the worst-case scenario for this\n    backend because each reference requires a separate file, and we make\n    it even worse by making it two files.\n\n  - We now have not only one file that would block future updates, but\n    two. This increases the likelihood that a crash or forced shutdown\n    will leave behind stale garbage.\n\nThat being said, I also see the potential upside, which is that it now\nbecomes a bit easier to recover from crashes/forced shutdowns. But in\nany case I think that this needs to be opt-in, not opt-out.\n\nPatrick\n"},{"id":"532707","messageId":"pull.2011.v3.git.1766579053136.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":"pull.2011.v2.git.1765997966593.gitgitgadget@gmail.com","subject":"[PATCH v3] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-12-24T12:24:13Z","receivedAt":"2025-12-24T12:24:16Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion PID file\nalongside each lock file, containing the PID of the lock holder.\n\nFor a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\ntilde character is forbidden in refnames and allowed in Windows\nfilenames, which guarantees no collision with the refs namespace\n(e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file uses a\nsimple key-value format (\"pid <value>\") following the same pattern as\nGit object headers, making it extensible for future metadata.\n\nThe PID file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks for an existing PID file\nand, if found, uses kill(pid, 0) to determine if the process is still\nrunning. This allows providing context-aware error messages:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nThe feature is controlled via core.lockfilePid configuration, which\naccepts per-component values similar to core.fsync:\n\n  - none: Disable for all components (default)\n  - all: Enable for all components\n  - index, config, refs, commit-graph, midx, shallow, gc, other:\n    Enable for specific components\n\nMultiple components can be combined with commas:\n\n  git config core.lockfilePid index,config\n\nEach lock file call site specifies which component it belongs to,\nallowing users fine-grained control over which locks create PID files.\n\nExisting PID files are always read when displaying lock errors,\nregardless of the core.lockfilePid setting. This ensures helpful\ndiagnostics even when the feature was previously enabled and later\ndisabled.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n    \n    Merry Christmas!\n    \n    Changes in v3:\n    \n     * The PID file now uses a tilde instead of a hyphen in the filename:\n       foo~pid.lock rather than foo-pid.lock. The tilde is forbidden in\n       refnames, guaranteeing no collision with the refs namespace, and is\n       allowed in Windows filenames.\n     * The feature remains opt-in: the default is core.lockfilePid=none, so\n       no PID files are created unless explicitly configured.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nRange-diff vs v2:\n\n 1:  d62edb0ca5 ! 1:  29e5903b21 lockfile: add PID file for debugging stale locks\n     @@ Commit message\n          processes. Add an optional feature that creates a companion PID file\n          alongside each lock file, containing the PID of the lock holder.\n      \n     -    For a lock file \"foo.lock\", the PID file is named \"foo.pid.lock\". The\n     -    file uses a simple key-value format (\"pid <value>\") following the same\n     -    pattern as Git object headers, making it extensible for future metadata.\n     +    For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n     +    tilde character is forbidden in refnames and allowed in Windows\n     +    filenames, which guarantees no collision with the refs namespace\n     +    (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file uses a\n     +    simple key-value format (\"pid <value>\") following the same pattern as\n     +    Git object headers, making it extensible for future metadata.\n      \n          The PID file is created when a lock is acquired (if enabled), and\n          automatically cleaned up when the lock is released (via commit or\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n       }\n       \n      +/*\n     -+ * Lock PID file functions - write PID to a foo-pid.lock file alongside\n     ++ * Lock PID file functions - write PID to a foo~pid.lock file alongside\n      + * the lock file for debugging stale locks. The PID file is registered\n      + * as a tempfile so it gets cleaned up by signal/atexit handlers.\n      + *\n     -+ * Naming: For \"foo.lock\", the PID file is \"foo-pid.lock\" (not \"foo.lock.pid\").\n     -+ * This avoids collision with the refs namespace.\n     ++ * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n     ++ * forbidden in refnames and allowed in Windows filenames, guaranteeing\n     ++ * no collision with the refs namespace.\n      + */\n      +\n      +/* Global config variable, initialized from core.lockfilePid */\n     @@ lockfile.h: struct lock_file {\n       #define LOCK_SUFFIX_LEN 5\n       \n      +/*\n     -+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo.pid.lock\".\n     ++ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n     ++ * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n     ++ * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n      + */\n     -+#define LOCK_PID_INFIX \".pid\"\n     ++#define LOCK_PID_INFIX \"~pid\"\n      +#define LOCK_PID_INFIX_LEN 4\n      +\n      +/* Maximum length for PID file content */\n     @@ midx-write.c: static int write_midx_internal(struct odb_source *source,\n       \t}\n      \n       ## odb.c ##\n     -@@ odb.c: void odb_add_to_alternates_file(struct object_database *odb,\n     - \tFILE *in, *out;\n     +@@ odb.c: static int odb_source_write_alternate(struct odb_source *source,\n       \tint found = 0;\n     + \tint ret;\n       \n     --\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&lock, alts, LOCK_DIE_ON_ERROR,\n     +-\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR);\n     ++\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR,\n      +\t\t\t\t  LOCKFILE_PID_OTHER);\n       \tout = fdopen_lock_file(&lock, \"w\");\n     - \tif (!out)\n     - \t\tdie_errno(_(\"unable to fdopen alternates lockfile\"));\n     + \tif (!out) {\n     + \t\tret = error_errno(_(\"unable to fdopen alternates lockfile\"));\n      \n       ## refs/files-backend.c ##\n      @@ refs/files-backend.c: retry:\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\ttest_must_fail git -c core.lockfilePid=index add . 2>err &&\n      +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n      +\t\ttest_grep \"Remove the stale lock file\" err\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo2 &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\ttest_must_fail git add . 2>err &&\n      +\t\t# Should not crash, just show normal error without PID\n      +\t\ttest_grep \"Unable to create\" err &&\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\tfi &&\n      +\t\t# Create a lock and PID file with current shell PID (which is running)\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n      +\t\t# Verify our PID is shown in the error message\n      +\t\ttest_must_fail git -c core.lockfilePid=index add file 2>err &&\n      +\t\ttest_grep \"held by process $shell_pid\" err\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\tgit -c core.lockfilePid=index add file &&\n      +\t\t# After successful add, no lock or PID files should exist\n      +\t\ttest_path_is_missing .git/index.lock &&\n     -+\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t\ttest_path_is_missing .git/index~pid.lock\n      +\t)\n      +'\n      +\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\techo content >file &&\n      +\t\tgit add file &&\n      +\t\t# PID file should not be created when feature is disabled\n     -+\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t\ttest_path_is_missing .git/index~pid.lock\n      +\t)\n      +'\n      +\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo6 &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\ttest_must_fail git -c core.lockfilePid=all add . 2>err &&\n      +\t\ttest_grep \"process 99999\" err\n      +\t)\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo8 &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\ttest_must_fail git -c core.lockfilePid=index,config add . 2>err &&\n      +\t\ttest_grep \"process 99999\" err\n      +\t)\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\techo content >file &&\n      +\t\tgit -c core.lockfilePid=none add file &&\n      +\t\t# PID file should not be created when feature is disabled\n     -+\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t\ttest_path_is_missing .git/index~pid.lock\n      +\t)\n      +'\n      +\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo10 &&\n      +\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index.pid.lock &&\n     ++\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\t# Even with lockfilePid disabled, existing PID files are read\n      +\t\t# to help diagnose stale locks\n      +\t\ttest_must_fail git add . 2>err &&\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\t# Enable all components except index\n      +\t\tgit -c core.lockfilePid=all,-index add file &&\n      +\t\t# PID file should not be created for index when excluded\n     -+\t\ttest_path_is_missing .git/index.pid.lock\n     ++\t\ttest_path_is_missing .git/index~pid.lock\n      +\t)\n      +'\n      +\n\n\n Documentation/config/core.adoc |  26 +++++\n apply.c                        |   6 +-\n builtin/commit.c               |   2 +-\n builtin/credential-store.c     |   3 +-\n builtin/difftool.c             |   3 +-\n builtin/fast-import.c          |   3 +-\n builtin/gc.c                   |  13 ++-\n builtin/sparse-checkout.c      |   3 +-\n bundle.c                       |   3 +-\n cache-tree.c                   |   3 +-\n commit-graph.c                 |   6 +-\n compat/mingw.c                 |  10 ++\n config.c                       |   6 +-\n environment.c                  |  80 ++++++++++++++\n lockfile.c                     | 183 +++++++++++++++++++++++++++++----\n lockfile.h                     |  79 ++++++++++----\n loose.c                        |   6 +-\n midx-write.c                   |   6 +-\n odb.c                          |   3 +-\n refs/files-backend.c           |   9 +-\n refs/packed-backend.c          |   3 +-\n reftable/system.c              |   2 +-\n repository.c                   |   3 +-\n rerere.c                       |   3 +-\n sequencer.c                    |  11 +-\n shallow.c                      |   6 +-\n t/meson.build                  |   1 +\n t/t0031-lockfile-pid.sh        | 139 +++++++++++++++++++++++++\n unix-stream-server.c           |   3 +-\n 29 files changed, 549 insertions(+), 75 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex 01202da7cd..4edea55872 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -348,6 +348,32 @@ confusion unless you know what you are doing (e.g. you are creating a\n read-only snapshot of the same index to a location different from the\n repository's usual working tree).\n \n+core.lockfilePid::\n+\tA comma-separated list of components for which Git should create\n+\ta PID file alongside the lock file. When a lock acquisition fails\n+\tand a PID file exists, Git can provide additional diagnostic\n+\tinformation about the process holding the lock, including whether\n+\tit is still running.\n++\n+This feature is disabled by default. You can enable it for specific\n+components or use `all` to enable for all components.\n++\n+* `none` disables PID file creation for all components.\n+* `index` creates PID files for index lock operations.\n+* `config` creates PID files for config file lock operations.\n+* `refs` creates PID files for reference lock operations.\n+* `commit-graph` creates PID files for commit-graph lock operations.\n+* `midx` creates PID files for multi-pack-index lock operations.\n+* `shallow` creates PID files for shallow file lock operations.\n+* `gc` creates PID files for garbage collection lock operations.\n+* `other` creates PID files for other miscellaneous lock operations.\n+* `all` enables PID file creation for all components.\n++\n+The PID file is named by inserting `.pid` before the `.lock` suffix.\n+For example, if the lock file is `index.lock`, the PID file will be\n+`index.pid.lock`. The file contains `pid <value>` on a single line,\n+following the same key-value format used by Git object headers.\n+\n core.logAllRefUpdates::\n \tEnable the reflog. Updates to a ref <ref> is logged to the file\n \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\ndiff --git a/apply.c b/apply.c\nindex c9fb45247d..6d24f1d9f8 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -4212,7 +4212,8 @@ static int build_fake_ancestor(struct apply_state *state, struct patch *list)\n \t\t}\n \t}\n \n-\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \tres = write_locked_index(&result, &lock, COMMIT_LOCK);\n \tdiscard_index(&result);\n \n@@ -4869,7 +4870,8 @@ static int apply_patch(struct apply_state *state,\n \t\tif (state->index_file)\n \t\t\thold_lock_file_for_update(&state->lock_file,\n \t\t\t\t\t\t  state->index_file,\n-\t\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t\t  LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t  LOCKFILE_PID_INDEX);\n \t\telse\n \t\t\trepo_hold_locked_index(state->repo, &state->lock_file,\n \t\t\t\t\t       LOCK_DIE_ON_ERROR);\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 0243f17d53..30db6409a1 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -540,7 +540,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n \t\t\t     (uintmax_t) getpid());\n \thold_lock_file_for_update(&false_lock, path,\n-\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_OTHER);\n \n \tcreate_base_index(current_head);\n \tadd_remove_files(&partial);\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..e1d1be5cd1 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -67,7 +67,8 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \tint timeout_ms = 1000;\n \n \trepo_config_get_int(the_repository, \"credentialstore.locktimeoutms\", &timeout_ms);\n-\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms) < 0)\n+\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms,\n+\t\t\t\t\t      LOCKFILE_PID_CONFIG) < 0)\n \t\tdie_errno(_(\"unable to get credential storage lock in %d ms\"), timeout_ms);\n \tif (extra)\n \t\tprint_line(extra);\ndiff --git a/builtin/difftool.c b/builtin/difftool.c\nindex e4bc1f8316..15d0af0031 100644\n--- a/builtin/difftool.c\n+++ b/builtin/difftool.c\n@@ -636,7 +636,8 @@ static int run_dir_diff(struct repository *repo,\n \t\t\tstruct lock_file lock = LOCK_INIT;\n \t\t\tstrbuf_reset(&buf);\n \t\t\tstrbuf_addf(&buf, \"%s/wtindex\", tmpdir.buf);\n-\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||\n+\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0,\n+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0 ||\n \t\t\t    write_locked_index(&wtindex, &lock, COMMIT_LOCK)) {\n \t\t\t\tret = error(\"could not write %s\", buf.buf);\n \t\t\t\tgoto finish;\ndiff --git a/builtin/fast-import.c b/builtin/fast-import.c\nindex 7849005ccb..89fd42db65 100644\n--- a/builtin/fast-import.c\n+++ b/builtin/fast-import.c\n@@ -1734,7 +1734,8 @@ static void dump_marks(void)\n \t\treturn;\n \t}\n \n-\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {\n+\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0,\n+\t\t\t\t      LOCKFILE_PID_OTHER) < 0) {\n \t\tfailure |= error_errno(_(\"unable to write marks file %s\"),\n \t\t\t\t       export_marks_file);\n \t\treturn;\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 92c6e7b954..cb142afa0b 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -749,7 +749,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n \n \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n \tfd = hold_lock_file_for_update(&lock, pidfile_path,\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n \tif (!force) {\n \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n \t\tstatic char *scan_fmt;\n@@ -1017,7 +1017,7 @@ int cmd_gc(int argc,\n \tif (daemonized) {\n \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n \t\thold_lock_file_for_update(&log_lock, path,\n-\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n \t\tdup2(get_lock_file_fd(&log_lock), 2);\n \t\tatexit(process_log_file_at_exit);\n \t\tfree(path);\n@@ -1797,7 +1797,8 @@ static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n \tstruct repository *r = the_repository;\n \tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->sources->path);\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n \t\t/*\n \t\t * Another maintenance command is running.\n \t\t *\n@@ -2561,7 +2562,8 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n \t\tlock_file_timeout_ms = 150;\n \n \tfd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,\n-\t\t\t\t\t       lock_file_timeout_ms);\n+\t\t\t\t\t       lock_file_timeout_ms,\n+\t\t\t\t\t       LOCKFILE_PID_GC);\n \n \t/*\n \t * Does this file already exist? With the intended contents? Is it\n@@ -3372,7 +3374,8 @@ static int update_background_schedule(const struct maintenance_start_opts *opts,\n \tstruct lock_file lk;\n \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->sources->path);\n \n-\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n \t\tif (errno == EEXIST)\n \t\t\terror(_(\"unable to create '%s.lock': %s.\\n\\n\"\n \t\t\t    \"Another scheduled git-maintenance(1) process seems to be running in this\\n\"\ndiff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c\nindex 15d51e60a8..566df7d229 100644\n--- a/builtin/sparse-checkout.c\n+++ b/builtin/sparse-checkout.c\n@@ -341,7 +341,8 @@ static int write_patterns_and_update(struct repository *repo,\n \tif (safe_create_leading_directories(repo, sparse_filename))\n \t\tdie(_(\"failed to create directory for sparse-checkout file\"));\n \n-\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \n \tresult = update_working_directory(repo, pl);\n \tif (result) {\ndiff --git a/bundle.c b/bundle.c\nindex 42327f9739..25728278fc 100644\n--- a/bundle.c\n+++ b/bundle.c\n@@ -520,7 +520,8 @@ int create_bundle(struct repository *r, const char *path,\n \t\tbundle_fd = 1;\n \telse\n \t\tbundle_fd = hold_lock_file_for_update(&lock, path,\n-\t\t\t\t\t\t      LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t\t      LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t\t      LOCKFILE_PID_OTHER);\n \n \tif (version == -1)\n \t\tversion = min_version;\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 2d8947b518..bbed306e78 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -754,7 +754,8 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state,\n \tstruct lock_file lock_file = LOCK_INIT;\n \tint ret;\n \n-\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_INDEX);\n \n \tentries = read_index_from(index_state, index_path,\n \t\t\t\t  repo_get_git_dir(the_repository));\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 80be2ff2c3..b18249fb73 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -2075,7 +2075,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tchar *lock_name = get_commit_graph_chain_filename(ctx->odb_source);\n \n \t\thold_lock_file_for_update_mode(&lk, lock_name,\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n \t\tfree(lock_name);\n \n \t\tgraph_layer = mks_tempfile_m(ctx->graph_name, 0444);\n@@ -2094,7 +2095,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\t   get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer));\n \t} else {\n \t\thold_lock_file_for_update_mode(&lk, ctx->graph_name,\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n \t\tf = hashfd(ctx->r->hash_algo,\n \t\t\t   get_lock_file_fd(&lk), get_lock_file_path(&lk));\n \t}\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 939f938fe2..146b2585ce 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)\n \t\t\tCloseHandle(h);\n \t\t\treturn 0;\n \t\t}\n+\t\t/*\n+\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n+\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n+\t\t * compatibility with kill(pid, 0).\n+\t\t */\n+\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n+\t\t\terrno = ESRCH;\n+\t\telse\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n \t}\n \n \terrno = EINVAL;\ndiff --git a/config.c b/config.c\nindex 1738c0cb0d..6658f59834 100644\n--- a/config.c\n+++ b/config.c\n@@ -2986,7 +2986,8 @@ int repo_config_set_multivar_in_file_gently(struct repository *r,\n \t * The lock serves a purpose in addition to locking: the new\n \t * contents of .git/config will be written into it.\n \t */\n-\tfd = hold_lock_file_for_update(&lock, config_filename, 0);\n+\tfd = hold_lock_file_for_update(&lock, config_filename, 0,\n+\t\t\t\t       LOCKFILE_PID_CONFIG);\n \tif (fd < 0) {\n \t\terror_errno(_(\"could not lock config file %s\"), config_filename);\n \t\tret = CONFIG_NO_LOCK;\n@@ -3331,7 +3332,8 @@ static int repo_config_copy_or_rename_section_in_file(\n \tif (!config_filename)\n \t\tconfig_filename = filename_buf = repo_git_path(r, \"config\");\n \n-\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0);\n+\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0,\n+\t\t\t\t\t   LOCKFILE_PID_CONFIG);\n \tif (out_fd < 0) {\n \t\tret = error(_(\"could not lock config file %s\"), config_filename);\n \t\tgoto out;\ndiff --git a/environment.c b/environment.c\nindex a770b5921d..0d1dfb525b 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -21,6 +21,7 @@\n #include \"gettext.h\"\n #include \"git-zlib.h\"\n #include \"ident.h\"\n+#include \"lockfile.h\"\n #include \"mailmap.h\"\n #include \"object-name.h\"\n #include \"repository.h\"\n@@ -324,6 +325,78 @@ next_name:\n \treturn (current & ~negative) | positive;\n }\n \n+static const struct lockfile_pid_component_name {\n+\tconst char *name;\n+\tenum lockfile_pid_component component_bits;\n+} lockfile_pid_component_names[] = {\n+\t{ \"index\", LOCKFILE_PID_INDEX },\n+\t{ \"config\", LOCKFILE_PID_CONFIG },\n+\t{ \"refs\", LOCKFILE_PID_REFS },\n+\t{ \"commit-graph\", LOCKFILE_PID_COMMIT_GRAPH },\n+\t{ \"midx\", LOCKFILE_PID_MIDX },\n+\t{ \"shallow\", LOCKFILE_PID_SHALLOW },\n+\t{ \"gc\", LOCKFILE_PID_GC },\n+\t{ \"other\", LOCKFILE_PID_OTHER },\n+\t{ \"all\", LOCKFILE_PID_ALL },\n+};\n+\n+static enum lockfile_pid_component parse_lockfile_pid_components(const char *var,\n+\t\t\t\t\t\t\t\t const char *string)\n+{\n+\tenum lockfile_pid_component current = LOCKFILE_PID_DEFAULT;\n+\tenum lockfile_pid_component positive = 0, negative = 0;\n+\n+\twhile (string) {\n+\t\tsize_t len;\n+\t\tconst char *ep;\n+\t\tint negated = 0;\n+\t\tint found = 0;\n+\n+\t\tstring = string + strspn(string, \", \\t\\n\\r\");\n+\t\tep = strchrnul(string, ',');\n+\t\tlen = ep - string;\n+\t\tif (len == 4 && !strncmp(string, \"none\", 4)) {\n+\t\t\tcurrent = LOCKFILE_PID_NONE;\n+\t\t\tgoto next_name;\n+\t\t}\n+\n+\t\tif (*string == '-') {\n+\t\t\tnegated = 1;\n+\t\t\tstring++;\n+\t\t\tlen--;\n+\t\t\tif (!len)\n+\t\t\t\twarning(_(\"invalid value for variable %s\"), var);\n+\t\t}\n+\n+\t\tif (!len)\n+\t\t\tbreak;\n+\n+\t\tfor (size_t i = 0; i < ARRAY_SIZE(lockfile_pid_component_names); ++i) {\n+\t\t\tconst struct lockfile_pid_component_name *n = &lockfile_pid_component_names[i];\n+\n+\t\t\tif (strncmp(n->name, string, len) || strlen(n->name) != len)\n+\t\t\t\tcontinue;\n+\n+\t\t\tfound = 1;\n+\t\t\tif (negated)\n+\t\t\t\tnegative |= n->component_bits;\n+\t\t\telse\n+\t\t\t\tpositive |= n->component_bits;\n+\t\t}\n+\n+\t\tif (!found) {\n+\t\t\tchar *component = xstrndup(string, len);\n+\t\t\twarning(_(\"ignoring unknown core.lockfilePid component '%s'\"), component);\n+\t\t\tfree(component);\n+\t\t}\n+\n+next_name:\n+\t\tstring = ep;\n+\t}\n+\n+\treturn (current & ~negative) | positive;\n+}\n+\n static int git_default_core_config(const char *var, const char *value,\n \t\t\t\t   const struct config_context *ctx, void *cb)\n {\n@@ -532,6 +605,13 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"core.lockfilepid\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tlockfile_pid_components = parse_lockfile_pid_components(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"core.createobject\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..e9a86f64b8 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,19 +74,118 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a foo~pid.lock file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ *\n+ * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n+ * forbidden in refnames and allowed in Windows filenames, guaranteeing\n+ * no collision with the refs namespace.\n+ */\n+\n+/* Global config variable, initialized from core.lockfilePid */\n+enum lockfile_pid_component lockfile_pid_components = LOCKFILE_PID_DEFAULT;\n+\n+/*\n+ * Path generation helpers.\n+ * Given base path \"foo\", generate:\n+ *   - lock path: \"foo.lock\"\n+ *   - pid path:  \"foo-pid.lock\"\n+ */\n+static void get_lock_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static void get_pid_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_PID_INFIX);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode,\n+\t\t\t\t\t     enum lockfile_pid_component component)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd = -1;\n+\n+\tif (!(lockfile_pid_components & component))\n+\t\tgoto out;\n+\n+\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n+\tif (fd < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n+\tif (write_in_full(fd, content.buf, content.len) < 0) {\n+\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n+\t\tgoto out;\n+\t}\n+\n+\tclose(fd);\n+\tfd = -1;\n+\tpid_tempfile = register_tempfile(pid_path);\n+\n+out:\n+\tif (fd >= 0)\n+\t\tclose(fd);\n+\tstrbuf_release(&content);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tconst char *val;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n+\t\tgoto out;\n+\n+\tstrbuf_rtrim(&content);\n+\n+\tif (skip_prefix(content.buf, \"pid \", &val)) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(val, &endptr, 10);\n+\t\tif (*pid_out > 0 && !*endptr)\n+\t\t\tret = 0;\n+\t}\n+\n+\tif (ret)\n+\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n-\t\t     int mode)\n+\t\t     int mode, enum lockfile_pid_component component)\n {\n-\tstruct strbuf filename = STRBUF_INIT;\n+\tstruct strbuf base_path = STRBUF_INIT;\n+\tstruct strbuf lock_path = STRBUF_INIT;\n+\tstruct strbuf pid_path = STRBUF_INIT;\n \n-\tstrbuf_addstr(&filename, path);\n+\tstrbuf_addstr(&base_path, path);\n \tif (!(flags & LOCK_NO_DEREF))\n-\t\tresolve_symlink(&filename);\n+\t\tresolve_symlink(&base_path);\n+\n+\tget_lock_path(&lock_path, base_path.buf);\n+\tget_pid_path(&pid_path, base_path.buf);\n+\n+\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode,\n+\t\t\t\t\t\t\tcomponent);\n \n-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n-\tstrbuf_release(&filename);\n+\tstrbuf_release(&base_path);\n+\tstrbuf_release(&lock_path);\n+\tstrbuf_release(&pid_path);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n \n@@ -102,7 +204,8 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,\n  * exactly once. If timeout_ms is -1, try indefinitely.\n  */\n static int lock_file_timeout(struct lock_file *lk, const char *path,\n-\t\t\t     int flags, long timeout_ms, int mode)\n+\t\t\t     int flags, long timeout_ms, int mode,\n+\t\t\t     enum lockfile_pid_component component)\n {\n \tint n = 1;\n \tint multiplier = 1;\n@@ -110,7 +213,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n \tstatic int random_initialized = 0;\n \n \tif (timeout_ms == 0)\n-\t\treturn lock_file(lk, path, flags, mode);\n+\t\treturn lock_file(lk, path, flags, mode, component);\n \n \tif (!random_initialized) {\n \t\tsrand((unsigned int)getpid());\n@@ -124,7 +227,7 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n \t\tlong backoff_ms, wait_ms;\n \t\tint fd;\n \n-\t\tfd = lock_file(lk, path, flags, mode);\n+\t\tfd = lock_file(lk, path, flags, mode, component);\n \n \t\tif (fd >= 0)\n \t\t\treturn fd; /* success */\n@@ -151,16 +254,47 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n-\t} else\n+\t\tconst char *abs_path = absolute_path(path);\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tstruct strbuf pid_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tget_lock_path(&lock_path, abs_path);\n+\t\tget_pid_path(&pid_path, abs_path);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\t/*\n+\t\t * Try to read PID file unconditionally - it may exist if\n+\t\t * core.lockfilePid was enabled for this component.\n+\t\t */\n+\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n+\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n+\t\t\telse if (errno == ESRCH)\n+\t\t\t\tpid_status = -1; /* no such process - stale lock */\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n+\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n+\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n+\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n+\n+\t\tstrbuf_release(&lock_path);\n+\t\tstrbuf_release(&pid_path);\n+\t} else {\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n+\t}\n }\n \n NORETURN void unable_to_lock_die(const char *path, int err)\n@@ -174,9 +308,10 @@ NORETURN void unable_to_lock_die(const char *path, int err)\n /* This should return a meaningful errno on failure */\n int hold_lock_file_for_update_timeout_mode(struct lock_file *lk,\n \t\t\t\t\t   const char *path, int flags,\n-\t\t\t\t\t   long timeout_ms, int mode)\n+\t\t\t\t\t   long timeout_ms, int mode,\n+\t\t\t\t\t   enum lockfile_pid_component component)\n {\n-\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode);\n+\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode, component);\n \tif (fd < 0) {\n \t\tif (flags & LOCK_DIE_ON_ERROR)\n \t\t\tunable_to_lock_die(path, errno);\n@@ -207,6 +342,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +353,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..147ea6ec23 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,44 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/*\n+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n+ * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n+ * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n+ */\n+#define LOCK_PID_INFIX \"~pid\"\n+#define LOCK_PID_INFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/*\n+ * Per-component lock PID file configuration, following core.fsync pattern.\n+ * Each component can be individually enabled via core.lockfilePid config.\n+ */\n+enum lockfile_pid_component {\n+\tLOCKFILE_PID_NONE = 0,\n+\tLOCKFILE_PID_INDEX = 1 << 0, /* .git/index.lock */\n+\tLOCKFILE_PID_CONFIG = 1 << 1, /* .git/config.lock */\n+\tLOCKFILE_PID_REFS = 1 << 2, /* refs locks */\n+\tLOCKFILE_PID_COMMIT_GRAPH = 1 << 3, /* commit-graph.lock */\n+\tLOCKFILE_PID_MIDX = 1 << 4, /* multi-pack-index.lock */\n+\tLOCKFILE_PID_SHALLOW = 1 << 5, /* shallow file */\n+\tLOCKFILE_PID_GC = 1 << 6, /* gc locks */\n+\tLOCKFILE_PID_OTHER = 1 << 7, /* other locks */\n+};\n+\n+#define LOCKFILE_PID_ALL (LOCKFILE_PID_INDEX | LOCKFILE_PID_CONFIG |      \\\n+\t\t\t  LOCKFILE_PID_REFS | LOCKFILE_PID_COMMIT_GRAPH | \\\n+\t\t\t  LOCKFILE_PID_MIDX | LOCKFILE_PID_SHALLOW |      \\\n+\t\t\t  LOCKFILE_PID_GC | LOCKFILE_PID_OTHER)\n+#define LOCKFILE_PID_DEFAULT LOCKFILE_PID_NONE\n+\n+/*\n+ * Bitmask indicating which components should create PID files.\n+ * Configured via core.lockfilePid.\n+ */\n+extern enum lockfile_pid_component lockfile_pid_components;\n \n /*\n  * Flags\n@@ -167,17 +206,23 @@ struct lock_file {\n  * timeout_ms milliseconds. If timeout_ms is 0, try exactly once; if\n  * timeout_ms is -1, retry indefinitely. The flags argument, error\n  * handling, and mode are described above.\n+ *\n+ * The `component` argument specifies which lock PID component this lock\n+ * belongs to, for selective PID file creation via core.lockfilePid config.\n  */\n int hold_lock_file_for_update_timeout_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms, int mode);\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms, int mode,\n+\tenum lockfile_pid_component component);\n \n static inline int hold_lock_file_for_update_timeout(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms,\n+\tenum lockfile_pid_component component)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n-\t\t\t\t\t\t      timeout_ms, 0666);\n+\t\t\t\t\t\t      timeout_ms, 0666,\n+\t\t\t\t\t\t      component);\n }\n \n /*\n@@ -186,17 +231,18 @@ static inline int hold_lock_file_for_update_timeout(\n  * argument and error handling are described above.\n  */\n static inline int hold_lock_file_for_update(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, enum lockfile_pid_component component)\n {\n-\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n+\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0, component);\n }\n \n static inline int hold_lock_file_for_update_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, int mode)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, int mode, enum lockfile_pid_component component)\n {\n-\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n+\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode,\n+\t\t\t\t\t\t      component);\n }\n \n /*\n@@ -319,13 +365,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/loose.c b/loose.c\nindex 56cf64b648..c0d2f0416f 100644\n--- a/loose.c\n+++ b/loose.c\n@@ -135,7 +135,8 @@ int repo_write_loose_object_map(struct repository *repo)\n \t\treturn 0;\n \n \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n-\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n+\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \titer = kh_begin(map);\n \tif (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)\n \t\tgoto errout;\n@@ -177,7 +178,8 @@ static int write_one_object(struct odb_source *source,\n \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n \n \tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n-\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n+\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n+\t\t\t\t\t  LOCKFILE_PID_OTHER);\n \n \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n \tif (fd < 0)\ndiff --git a/midx-write.c b/midx-write.c\nindex e3e9be6d03..8180334703 100644\n--- a/midx-write.c\n+++ b/midx-write.c\n@@ -1309,7 +1309,8 @@ static int write_midx_internal(struct odb_source *source,\n \t\tstruct strbuf lock_name = STRBUF_INIT;\n \n \t\tget_midx_chain_filename(source, &lock_name);\n-\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n \t\tstrbuf_release(&lock_name);\n \n \t\tincr = mks_tempfile_m(midx_name.buf, 0444);\n@@ -1327,7 +1328,8 @@ static int write_midx_internal(struct odb_source *source,\n \t\tf = hashfd(r->hash_algo, get_tempfile_fd(incr),\n \t\t\t   get_tempfile_path(incr));\n \t} else {\n-\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n \t\tf = hashfd(r->hash_algo, get_lock_file_fd(&lk),\n \t\t\t   get_lock_file_path(&lk));\n \t}\ndiff --git a/odb.c b/odb.c\nindex 7b5da2de32..7017f2d215 100644\n--- a/odb.c\n+++ b/odb.c\n@@ -280,7 +280,8 @@ static int odb_source_write_alternate(struct odb_source *source,\n \tint found = 0;\n \tint ret;\n \n-\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR,\n+\t\t\t\t  LOCKFILE_PID_OTHER);\n \tout = fdopen_lock_file(&lock, \"w\");\n \tif (!out) {\n \t\tret = error_errno(_(\"unable to fdopen alternates lockfile\"));\ndiff --git a/refs/files-backend.c b/refs/files-backend.c\nindex 6f6f76a8d8..e296154fe7 100644\n--- a/refs/files-backend.c\n+++ b/refs/files-backend.c\n@@ -790,7 +790,8 @@ retry:\n \n \tif (hold_lock_file_for_update_timeout(\n \t\t\t    &lock->lk, ref_file.buf, LOCK_NO_DEREF,\n-\t\t\t    get_files_ref_lock_timeout_ms()) < 0) {\n+\t\t\t    get_files_ref_lock_timeout_ms(),\n+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n \t\tint myerr = errno;\n \t\terrno = 0;\n \t\tif (myerr == ENOENT && --attempts_remaining > 0) {\n@@ -1194,7 +1195,8 @@ static int create_reflock(const char *path, void *cb)\n \n \treturn hold_lock_file_for_update_timeout(\n \t\t\tlk, path, LOCK_NO_DEREF,\n-\t\t\tget_files_ref_lock_timeout_ms()) < 0 ? -1 : 0;\n+\t\t\tget_files_ref_lock_timeout_ms(),\n+\t\t\tLOCKFILE_PID_REFS) < 0 ? -1 : 0;\n }\n \n /*\n@@ -3536,7 +3538,8 @@ static int files_reflog_expire(struct ref_store *ref_store,\n \t\t * work we need, including cleaning up if the program\n \t\t * exits unexpectedly.\n \t\t */\n-\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {\n+\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0,\n+\t\t\t\t\t      LOCKFILE_PID_REFS) < 0) {\n \t\t\tstruct strbuf err = STRBUF_INIT;\n \t\t\tunable_to_lock_message(log_file, errno, &err);\n \t\t\terror(\"%s\", err.buf);\ndiff --git a/refs/packed-backend.c b/refs/packed-backend.c\nindex 4ea0c12299..f832c49eae 100644\n--- a/refs/packed-backend.c\n+++ b/refs/packed-backend.c\n@@ -1230,7 +1230,8 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)\n \tif (hold_lock_file_for_update_timeout(\n \t\t\t    &refs->lock,\n \t\t\t    refs->path,\n-\t\t\t    flags, timeout_value) < 0) {\n+\t\t\t    flags, timeout_value,\n+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n \t\tunable_to_lock_message(refs->path, errno, err);\n \t\treturn -1;\n \t}\ndiff --git a/reftable/system.c b/reftable/system.c\nindex 725a25844e..5462111a04 100644\n--- a/reftable/system.c\n+++ b/reftable/system.c\n@@ -67,7 +67,7 @@ int flock_acquire(struct reftable_flock *l, const char *target_path,\n \t\treturn REFTABLE_OUT_OF_MEMORY_ERROR;\n \n \terr = hold_lock_file_for_update_timeout(lockfile, target_path, LOCK_NO_DEREF,\n-\t\t\t\t\t\ttimeout_ms);\n+\t\t\t\t\t\ttimeout_ms, LOCKFILE_PID_REFS);\n \tif (err < 0) {\n \t\treftable_free(lockfile);\n \t\tif (errno == EEXIST)\ndiff --git a/repository.c b/repository.c\nindex 863f24411b..c010098fda 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -449,5 +449,6 @@ int repo_hold_locked_index(struct repository *repo,\n {\n \tif (!repo->index_file)\n \t\tBUG(\"the repo hasn't been setup\");\n-\treturn hold_lock_file_for_update(lf, repo->index_file, flags);\n+\treturn hold_lock_file_for_update(lf, repo->index_file, flags,\n+\t\t\t\t\t LOCKFILE_PID_INDEX);\n }\ndiff --git a/rerere.c b/rerere.c\nindex 6ec55964e2..71f1d5b176 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -917,7 +917,8 @@ int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n \telse\n \t\tfd = hold_lock_file_for_update(&write_lock,\n \t\t\t\t\t       git_path_merge_rr(r),\n-\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \tread_rr(r, merge_rr);\n \treturn fd;\n }\ndiff --git a/sequencer.c b/sequencer.c\nindex 5476d39ba9..337f82e02c 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -566,7 +566,8 @@ static int write_message(const void *buf, size_t len, const char *filename,\n {\n \tstruct lock_file msg_file = LOCK_INIT;\n \n-\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0);\n+\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0,\n+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n \tif (msg_fd < 0)\n \t\treturn error_errno(_(\"could not lock '%s'\"), filename);\n \tif (write_in_full(msg_fd, buf, len) < 0) {\n@@ -3605,7 +3606,8 @@ static int save_todo(struct todo_list *todo_list, struct replay_opts *opts,\n \tif (is_rebase_i(opts) && !reschedule)\n \t\tnext++;\n \n-\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0);\n+\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0,\n+\t\t\t\t       LOCKFILE_PID_OTHER);\n \tif (fd < 0)\n \t\treturn error_errno(_(\"could not lock '%s'\"), todo_path);\n \toffset = get_item_line_offset(todo_list, next);\n@@ -3877,7 +3879,8 @@ static int safe_append(const char *filename, const char *fmt, ...)\n \tva_list ap;\n \tstruct lock_file lock = LOCK_INIT;\n \tint fd = hold_lock_file_for_update(&lock, filename,\n-\t\t\t\t\t   LOCK_REPORT_ON_ERROR);\n+\t\t\t\t\t   LOCK_REPORT_ON_ERROR,\n+\t\t\t\t\t   LOCKFILE_PID_OTHER);\n \tstruct strbuf buf = STRBUF_INIT;\n \n \tif (fd < 0)\n@@ -4400,7 +4403,7 @@ static int write_update_refs_state(struct string_list *refs_to_oids)\n \t\tgoto cleanup;\n \t}\n \n-\tif (hold_lock_file_for_update(&lock, path, 0) < 0) {\n+\tif (hold_lock_file_for_update(&lock, path, 0, LOCKFILE_PID_OTHER) < 0) {\n \t\tresult = error(_(\"another 'rebase' process appears to be running; \"\n \t\t\t\t \"'%s.lock' already exists\"),\n \t\t\t       path);\ndiff --git a/shallow.c b/shallow.c\nindex 186e9178f3..01dc152a1c 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -392,7 +392,8 @@ void setup_alternate_shallow(struct shallow_lock *shallow_lock,\n \n \tfd = hold_lock_file_for_update(&shallow_lock->lock,\n \t\t\t\t       git_path_shallow(the_repository),\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n \tcheck_shallow_file_for_update(the_repository);\n \tif (write_shallow_commits(&sb, 0, extra)) {\n \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n@@ -447,7 +448,8 @@ void prune_shallow(unsigned options)\n \t}\n \tfd = hold_lock_file_for_update(&shallow_lock.lock,\n \t\t\t\t       git_path_shallow(the_repository),\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\t\t\t\t       LOCK_DIE_ON_ERROR,\n+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n \tcheck_shallow_file_for_update(the_repository);\n \tif (write_shallow_commits_1(&sb, 0, NULL, flags)) {\n \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..2aec2c011e 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..51bb6d4be1\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,139 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via core.lockfilePid config setting.\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=index add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"Remove the stale lock file\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Get the correct PID for this platform\n+\t\tshell_pid=$$ &&\n+\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n+\t\tthen\n+\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n+\t\t\t# uses Windows PIDs. Use the Windows PID.\n+\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n+\t\tfi &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail git -c core.lockfilePid=index add file 2>err &&\n+\t\ttest_grep \"held by process $shell_pid\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=index add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\ttest_path_is_missing .git/index.lock &&\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=all enables for all components' '\n+\tgit init repo6 &&\n+\t(\n+\t\tcd repo6 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=all add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'multiple components can be specified' '\n+\tgit init repo8 &&\n+\t(\n+\t\tcd repo8 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=index,config add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=none does not create PID file' '\n+\tgit init repo9 &&\n+\t(\n+\t\tcd repo9 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=none add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'existing PID files are read even when feature disabled' '\n+\tgit init repo10 &&\n+\t(\n+\t\tcd repo10 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\t# Even with lockfilePid disabled, existing PID files are read\n+\t\t# to help diagnose stale locks\n+\t\ttest_must_fail git add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_expect_success 'negative component syntax excludes specific components' '\n+\tgit init repo11 &&\n+\t(\n+\t\tcd repo11 &&\n+\t\techo content >file &&\n+\t\t# Enable all components except index\n+\t\tgit -c core.lockfilePid=all,-index add file &&\n+\t\t# PID file should not be created for index when excluded\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_done\ndiff --git a/unix-stream-server.c b/unix-stream-server.c\nindex 22ac2373e0..d8c79cd569 100644\n--- a/unix-stream-server.c\n+++ b/unix-stream-server.c\n@@ -47,7 +47,8 @@ int unix_ss_create(const char *path,\n \t/*\n \t * Create a lock at \"<path>.lock\" if we can.\n \t */\n-\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms) < 0)\n+\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms,\n+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0)\n \t\treturn -1;\n \n \t/*\n\nbase-commit: 66ce5f8e8872f0183bb137911c52b07f1f242d13\n-- \ngitgitgadget\n"},{"id":"532734","messageId":"xmqqwm2bmnug.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v3.git.1766579053136.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-25T00:01:11Z","receivedAt":"2025-12-25T00:01:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n> tilde character is forbidden in refnames and allowed in Windows\n> filenames, which guarantees no collision with the refs namespace\n> (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n\nLucky we had this wiggle room for us ;-)\n\n> The file uses a\n> simple key-value format (\"pid <value>\") following the same pattern as\n> Git object headers, making it extensible for future metadata.\n\nIt may be just me, but \"a Git object header\" to me means the \"<type>\n<length> <NUL>\".  I suspect that you modelled this more after\ncommit headers (in which case you may want to say \"pid <value> LF\"\nto stress the fact that a record is newline terminated).\n\n> +The PID file is named by inserting `.pid` before the `.lock` suffix.\n\nDon't we use tilde somewhere???\n\n> +For example, if the lock file is `index.lock`, the PID file will be\n> +`index.pid.lock`. The file contains `pid <value>` on a single line,\n> +following the same key-value format used by Git object headers.\n\nSame comment as the one for the proposed log message applies here.\n\n> diff --git a/apply.c b/apply.c\n> index c9fb45247d..6d24f1d9f8 100644\n> --- a/apply.c\n> +++ b/apply.c\n> @@ -4212,7 +4212,8 @@ static int build_fake_ancestor(struct apply_state *state, struct patch *list)\n>  \t\t}\n>  \t}\n>  \n> -\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);\n> +\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR,\n> +\t\t\t\t  LOCKFILE_PID_OTHER);\n\nHmph, I thought I suggested not to send this as a separate\nparameter, and instead use a set of bits in an existing flag word\nthat currently carries only \"what to do upon error\" bits?\n\n>  \t\t\thold_lock_file_for_update(&state->lock_file,\n>  \t\t\t\t\t\t  state->index_file,\n> -\t\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n> +\t\t\t\t\t\t  LOCK_DIE_ON_ERROR,\n> +\t\t\t\t\t\t  LOCKFILE_PID_INDEX);\n\nDitto.\n\n> +static const struct lockfile_pid_component_name {\n> +\tconst char *name;\n> +\tenum lockfile_pid_component component_bits;\n> +} lockfile_pid_component_names[] = {\n> +\t{ \"index\", LOCKFILE_PID_INDEX },\n> +\t{ \"config\", LOCKFILE_PID_CONFIG },\n> +\t{ \"refs\", LOCKFILE_PID_REFS },\n> +\t{ \"commit-graph\", LOCKFILE_PID_COMMIT_GRAPH },\n> +\t{ \"midx\", LOCKFILE_PID_MIDX },\n> +\t{ \"shallow\", LOCKFILE_PID_SHALLOW },\n> +\t{ \"gc\", LOCKFILE_PID_GC },\n> +\t{ \"other\", LOCKFILE_PID_OTHER },\n> +\t{ \"all\", LOCKFILE_PID_ALL },\n> +};\n\nI wonder if we want to sort the array entries here, even if we look\nit up linearly (as the table is small enough)?\n\n> +\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n> +\tif (write_in_full(fd, content.buf, content.len) < 0) {\n> +\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> +\t\tgoto out;\n\nThe caller still will get a non-NULL pid_tempfile?  If you failed to\nwrite the contents, wouldn't it allow the caller to handle the\nfailure more smoothly if you allowed the caller to know about the\nfailure, perhaps by removing the temporary file and returning NULL?\n\n> +\t}\n> +\n> +\tclose(fd);\n> +\tfd = -1;\n> +\tpid_tempfile = register_tempfile(pid_path);\n> +\n> +out:\n> +\tif (fd >= 0)\n> +\t\tclose(fd);\n> +\tstrbuf_release(&content);\n> +\treturn pid_tempfile;\n> +}\n\n> +static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n> +{\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tconst char *val;\n> +\tint ret = -1;\n> +\n> +\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n> +\t\tgoto out;\n\nAny and all failure to read the pid_path is a silent, including an\nempty one?\n\nI'll stop here for now.\n\nThanks.\n"},{"id":"532773","messageId":"20251227075025.GC2071715@coredump.intra.peff.net","threadId":"64568","inReplyTo":"pull.2011.v3.git.1766579053136.gitgitgadget@gmail.com","subject":"Re: [PATCH v3] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-12-27T07:50:25Z","receivedAt":"2025-12-27T07:50:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Dec 24, 2025 at 12:24:13PM +0000, Paulo Casaretto via GitGitGadget wrote:\n\n> For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n> tilde character is forbidden in refnames and allowed in Windows\n> filenames, which guarantees no collision with the refs namespace\n> (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file uses a\n> simple key-value format (\"pid <value>\") following the same pattern as\n> Git object headers, making it extensible for future metadata.\n\nFWIW, I like this approach, as the earlier collision possibilities in\nearlier iterations made me a bit uncomfortable.\n\nBut then I wondered...\n\n> The feature is controlled via core.lockfilePid configuration, which\n> accepts per-component values similar to core.fsync:\n> \n>   - none: Disable for all components (default)\n>   - all: Enable for all components\n>   - index, config, refs, commit-graph, midx, shallow, gc, other:\n>     Enable for specific components\n\nDo we really need this complexity now? I can see reasons why a user\nmight want to switch the feature on (because they want the extra pid\ndebugging info) or off (because they do not want the extra filesystem\noperations or potential cleanup hassle of extra stale files).\n\nBut if the collision problems in the ref namespace are now solved, does\nanybody really care about turning it on just for particular subsystems?\nI'd think they would want all or nothing.\n\n\nTrying to devil's advocate myself: maybe somebody is concerned about\nfilesystem overhead for frequently-written files like refs but not for\nothers, like config? That feels unlikely to me, but at least possible.\n\nBut if we were to ditch the subsystem-granularity for config, then all\nof the extra LOCKFILE_* arguments here could just go away.\n\n-Peff\n"},{"id":"533033","messageId":"aVutN543nceW0f8W@pks.im","threadId":"64568","inReplyTo":"20251227075025.GC2071715@coredump.intra.peff.net","subject":"Re: [PATCH v3] lockfile: add PID file for debugging stale locks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-05T12:23:19Z","receivedAt":"2026-01-05T12:23:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 27, 2025 at 02:50:25AM -0500, Jeff King wrote:\n> On Wed, Dec 24, 2025 at 12:24:13PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> \n> > For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n> > tilde character is forbidden in refnames and allowed in Windows\n> > filenames, which guarantees no collision with the refs namespace\n> > (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file uses a\n> > simple key-value format (\"pid <value>\") following the same pattern as\n> > Git object headers, making it extensible for future metadata.\n> \n> FWIW, I like this approach, as the earlier collision possibilities in\n> earlier iterations made me a bit uncomfortable.\n> \n> But then I wondered...\n> \n> > The feature is controlled via core.lockfilePid configuration, which\n> > accepts per-component values similar to core.fsync:\n> > \n> >   - none: Disable for all components (default)\n> >   - all: Enable for all components\n> >   - index, config, refs, commit-graph, midx, shallow, gc, other:\n> >     Enable for specific components\n> \n> Do we really need this complexity now? I can see reasons why a user\n> might want to switch the feature on (because they want the extra pid\n> debugging info) or off (because they do not want the extra filesystem\n> operations or potential cleanup hassle of extra stale files).\n> \n> But if the collision problems in the ref namespace are now solved, does\n> anybody really care about turning it on just for particular subsystems?\n> I'd think they would want all or nothing.\n> \n> \n> Trying to devil's advocate myself: maybe somebody is concerned about\n> filesystem overhead for frequently-written files like refs but not for\n> others, like config? That feels unlikely to me, but at least possible.\n> \n> But if we were to ditch the subsystem-granularity for config, then all\n> of the extra LOCKFILE_* arguments here could just go away.\n\nI tend to agree. Based on my own experience, references are by far the\nmost likely subsystem where one may hit stale lockfiles, so the feature\nis most useful there. But at the same time, it's also the most expensive\nsubsystem to enable this feature for because we write so many small\nfiles there.\n\nConsequently, if anybody cares, they would most likely want to enable\nthis feature for refs. And, if so, the impact on other subsystems would\nvery likely be dwarfed by the refs overhead.\n\nSo my approach would be to enable this setting with only \"true\" and\n\"false\" as possible values initially. And if we ever get a use case\nwhere somebody would be helped by configuring this per subsystem we can\nextend the config to learn about subsystems.\n\nBut ultimately I don't care too strongly about this. If the author wants\nto keep the current approach I'm fine with that, too.\n\nPatrick\n"},{"id":"533231","messageId":"pull.2011.v4.git.1767804355831.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":"pull.2011.v3.git.1766579053136.gitgitgadget@gmail.com","subject":"[PATCH v4] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-07T16:45:55Z","receivedAt":"2026-01-07T16:46:01Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion PID file\nalongside each lock file, containing the PID of the lock holder.\n\nFor a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\ntilde character is forbidden in refnames and allowed in Windows\nfilenames, which guarantees no collision with the refs namespace\n(e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file contains\na single line in the format \"pid <value>\" followed by a newline.\n\nThe PID file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks for an existing PID file\nand, if found, uses kill(pid, 0) to determine if the process is still\nrunning. This allows providing context-aware error messages:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nThe feature is controlled via core.lockfilePid configuration (boolean).\nDefaults to false. When enabled, PID files are created for all lock\noperations.\n\nExisting PID files are always read when displaying lock errors,\nregardless of the core.lockfilePid setting. This ensures helpful\ndiagnostics even when the feature was previously enabled and later\ndisabled.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n    \n    Changes in v4:\n    \n     * Simplified core.lockfilePid from per-component configuration to a\n       simple boolean (true/false), as suggested by Jeff and Patrick. The\n       per-subsystem granularity added complexity without a clear use case -\n       if someone wants PID files for debugging, they likely want them\n       everywhere.\n     * Removed the enum lockfile_pid_component and all LOCKFILE_PID_*\n       constants\n     * Removed the component parameter from hold_lock_file_for_update*()\n       functions, restoring their original signatures\n     * Simplified config parsing to use git_config_bool() instead of the\n       fsync-style component parser\n     * Fixed error handling in create_lock_pid_file() to unlink the file on\n       write failure, as noted by Junio\n     * Clarified documentation to describe the file format directly (\"pid \"\n       followed by a newline) rather than referencing \"Git object headers\"\n     * Updated tests and documentation to reflect the boolean config\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v4\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nRange-diff vs v3:\n\n 1:  29e5903b21 ! 1:  c26bc77e30 lockfile: add PID file for debugging stale locks\n     @@ Commit message\n          For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n          tilde character is forbidden in refnames and allowed in Windows\n          filenames, which guarantees no collision with the refs namespace\n     -    (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file uses a\n     -    simple key-value format (\"pid <value>\") following the same pattern as\n     -    Git object headers, making it extensible for future metadata.\n     +    (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file contains\n     +    a single line in the format \"pid <value>\" followed by a newline.\n      \n          The PID file is created when a lock is acquired (if enabled), and\n          automatically cleaned up when the lock is released (via commit or\n     @@ Commit message\n            Lock was held by process 12345, which is no longer running.\n            Remove the stale lock file to continue.\n      \n     -    The feature is controlled via core.lockfilePid configuration, which\n     -    accepts per-component values similar to core.fsync:\n     -\n     -      - none: Disable for all components (default)\n     -      - all: Enable for all components\n     -      - index, config, refs, commit-graph, midx, shallow, gc, other:\n     -        Enable for specific components\n     -\n     -    Multiple components can be combined with commas:\n     -\n     -      git config core.lockfilePid index,config\n     -\n     -    Each lock file call site specifies which component it belongs to,\n     -    allowing users fine-grained control over which locks create PID files.\n     +    The feature is controlled via core.lockfilePid configuration (boolean).\n     +    Defaults to false. When enabled, PID files are created for all lock\n     +    operations.\n      \n          Existing PID files are always read when displaying lock errors,\n          regardless of the core.lockfilePid setting. This ensures helpful\n     @@ Documentation/config/core.adoc: confusion unless you know what you are doing (e.\n       repository's usual working tree).\n       \n      +core.lockfilePid::\n     -+\tA comma-separated list of components for which Git should create\n     -+\ta PID file alongside the lock file. When a lock acquisition fails\n     -+\tand a PID file exists, Git can provide additional diagnostic\n     -+\tinformation about the process holding the lock, including whether\n     -+\tit is still running.\n     -++\n     -+This feature is disabled by default. You can enable it for specific\n     -+components or use `all` to enable for all components.\n     ++\tIf true, Git will create a PID file alongside lock files. When a\n     ++\tlock acquisition fails and a PID file exists, Git can provide\n     ++\tadditional diagnostic information about the process holding the\n     ++\tlock, including whether it is still running. Defaults to `false`.\n      ++\n     -+* `none` disables PID file creation for all components.\n     -+* `index` creates PID files for index lock operations.\n     -+* `config` creates PID files for config file lock operations.\n     -+* `refs` creates PID files for reference lock operations.\n     -+* `commit-graph` creates PID files for commit-graph lock operations.\n     -+* `midx` creates PID files for multi-pack-index lock operations.\n     -+* `shallow` creates PID files for shallow file lock operations.\n     -+* `gc` creates PID files for garbage collection lock operations.\n     -+* `other` creates PID files for other miscellaneous lock operations.\n     -+* `all` enables PID file creation for all components.\n     -++\n     -+The PID file is named by inserting `.pid` before the `.lock` suffix.\n     ++The PID file is named by inserting `~pid` before the `.lock` suffix.\n      +For example, if the lock file is `index.lock`, the PID file will be\n     -+`index.pid.lock`. The file contains `pid <value>` on a single line,\n     -+following the same key-value format used by Git object headers.\n     ++`index~pid.lock`. The file contains a single line in the format\n     ++`pid <value>` followed by a newline.\n      +\n       core.logAllRefUpdates::\n       \tEnable the reflog. Updates to a ref <ref> is logged to the file\n       \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\n      \n     - ## apply.c ##\n     -@@ apply.c: static int build_fake_ancestor(struct apply_state *state, struct patch *list)\n     - \t\t}\n     - \t}\n     - \n     --\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&lock, state->fake_ancestor, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t  LOCKFILE_PID_OTHER);\n     - \tres = write_locked_index(&result, &lock, COMMIT_LOCK);\n     - \tdiscard_index(&result);\n     - \n     -@@ apply.c: static int apply_patch(struct apply_state *state,\n     - \t\tif (state->index_file)\n     - \t\t\thold_lock_file_for_update(&state->lock_file,\n     - \t\t\t\t\t\t  state->index_file,\n     --\t\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t\t\t  LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t\t  LOCKFILE_PID_INDEX);\n     - \t\telse\n     - \t\t\trepo_hold_locked_index(state->repo, &state->lock_file,\n     - \t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -\n       ## builtin/commit.c ##\n      @@ builtin/commit.c: static const char *prepare_index(const char **argv, const char *prefix,\n     + \n       \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n       \t\t\t     (uintmax_t) getpid());\n     - \thold_lock_file_for_update(&false_lock, path,\n     +-\thold_lock_file_for_update(&false_lock, path,\n      -\t\t\t\t  LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_OTHER);\n     ++\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n       \n       \tcreate_base_index(current_head);\n       \tadd_remove_files(&partial);\n      \n     - ## builtin/credential-store.c ##\n     -@@ builtin/credential-store.c: static void rewrite_credential_file(const char *fn, struct credential *c,\n     - \tint timeout_ms = 1000;\n     - \n     - \trepo_config_get_int(the_repository, \"credentialstore.locktimeoutms\", &timeout_ms);\n     --\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms) < 0)\n     -+\tif (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms,\n     -+\t\t\t\t\t      LOCKFILE_PID_CONFIG) < 0)\n     - \t\tdie_errno(_(\"unable to get credential storage lock in %d ms\"), timeout_ms);\n     - \tif (extra)\n     - \t\tprint_line(extra);\n     -\n     - ## builtin/difftool.c ##\n     -@@ builtin/difftool.c: static int run_dir_diff(struct repository *repo,\n     - \t\t\tstruct lock_file lock = LOCK_INIT;\n     - \t\t\tstrbuf_reset(&buf);\n     - \t\t\tstrbuf_addf(&buf, \"%s/wtindex\", tmpdir.buf);\n     --\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||\n     -+\t\t\tif (hold_lock_file_for_update(&lock, buf.buf, 0,\n     -+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0 ||\n     - \t\t\t    write_locked_index(&wtindex, &lock, COMMIT_LOCK)) {\n     - \t\t\t\tret = error(\"could not write %s\", buf.buf);\n     - \t\t\t\tgoto finish;\n     -\n     - ## builtin/fast-import.c ##\n     -@@ builtin/fast-import.c: static void dump_marks(void)\n     - \t\treturn;\n     - \t}\n     - \n     --\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) {\n     -+\tif (hold_lock_file_for_update(&mark_lock, export_marks_file, 0,\n     -+\t\t\t\t      LOCKFILE_PID_OTHER) < 0) {\n     - \t\tfailure |= error_errno(_(\"unable to write marks file %s\"),\n     - \t\t\t\t       export_marks_file);\n     - \t\treturn;\n     -\n       ## builtin/gc.c ##\n      @@ builtin/gc.c: static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n     + \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n       \n       \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n     - \tfd = hold_lock_file_for_update(&lock, pidfile_path,\n     +-\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n      -\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t       LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n     ++\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n       \tif (!force) {\n       \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n       \t\tstatic char *scan_fmt;\n      @@ builtin/gc.c: int cmd_gc(int argc,\n     + \n       \tif (daemonized) {\n       \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n     - \t\thold_lock_file_for_update(&log_lock, path,\n     +-\t\thold_lock_file_for_update(&log_lock, path,\n      -\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t\t  LOCK_DIE_ON_ERROR, LOCKFILE_PID_GC);\n     ++\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n       \t\tdup2(get_lock_file_fd(&log_lock), 2);\n       \t\tatexit(process_log_file_at_exit);\n       \t\tfree(path);\n     -@@ builtin/gc.c: static int maintenance_run_tasks(struct maintenance_run_opts *opts,\n     - \tstruct repository *r = the_repository;\n     - \tchar *lock_path = xstrfmt(\"%s/maintenance\", r->objects->sources->path);\n     - \n     --\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n     -+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n     -+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n     - \t\t/*\n     - \t\t * Another maintenance command is running.\n     - \t\t *\n     -@@ builtin/gc.c: static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit\n     - \t\tlock_file_timeout_ms = 150;\n     - \n     - \tfd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR,\n     --\t\t\t\t\t       lock_file_timeout_ms);\n     -+\t\t\t\t\t       lock_file_timeout_ms,\n     -+\t\t\t\t\t       LOCKFILE_PID_GC);\n     - \n     - \t/*\n     - \t * Does this file already exist? With the intended contents? Is it\n     -@@ builtin/gc.c: static int update_background_schedule(const struct maintenance_start_opts *opts,\n     - \tstruct lock_file lk;\n     - \tchar *lock_path = xstrfmt(\"%s/schedule\", the_repository->objects->sources->path);\n     - \n     --\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) {\n     -+\tif (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF,\n     -+\t\t\t\t      LOCKFILE_PID_GC) < 0) {\n     - \t\tif (errno == EEXIST)\n     - \t\t\terror(_(\"unable to create '%s.lock': %s.\\n\\n\"\n     - \t\t\t    \"Another scheduled git-maintenance(1) process seems to be running in this\\n\"\n     -\n     - ## builtin/sparse-checkout.c ##\n     -@@ builtin/sparse-checkout.c: static int write_patterns_and_update(struct repository *repo,\n     - \tif (safe_create_leading_directories(repo, sparse_filename))\n     - \t\tdie(_(\"failed to create directory for sparse-checkout file\"));\n     - \n     --\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&lk, sparse_filename, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t  LOCKFILE_PID_OTHER);\n     - \n     - \tresult = update_working_directory(repo, pl);\n     - \tif (result) {\n     -\n     - ## bundle.c ##\n     -@@ bundle.c: int create_bundle(struct repository *r, const char *path,\n     - \t\tbundle_fd = 1;\n     - \telse\n     - \t\tbundle_fd = hold_lock_file_for_update(&lock, path,\n     --\t\t\t\t\t\t      LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t\t\t      LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t\t      LOCKFILE_PID_OTHER);\n     - \n     - \tif (version == -1)\n     - \t\tversion = min_version;\n     -\n     - ## cache-tree.c ##\n     -@@ cache-tree.c: int write_index_as_tree(struct object_id *oid, struct index_state *index_state,\n     - \tstruct lock_file lock_file = LOCK_INIT;\n     - \tint ret;\n     - \n     --\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&lock_file, index_path, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t  LOCKFILE_PID_INDEX);\n     - \n     - \tentries = read_index_from(index_state, index_path,\n     - \t\t\t\t  repo_get_git_dir(the_repository));\n     -\n     - ## commit-graph.c ##\n     -@@ commit-graph.c: static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n     - \t\tchar *lock_name = get_commit_graph_chain_filename(ctx->odb_source);\n     - \n     - \t\thold_lock_file_for_update_mode(&lk, lock_name,\n     --\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n     -+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n     -+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n     - \t\tfree(lock_name);\n     - \n     - \t\tgraph_layer = mks_tempfile_m(ctx->graph_name, 0444);\n     -@@ commit-graph.c: static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n     - \t\t\t   get_tempfile_fd(graph_layer), get_tempfile_path(graph_layer));\n     - \t} else {\n     - \t\thold_lock_file_for_update_mode(&lk, ctx->graph_name,\n     --\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444);\n     -+\t\t\t\t\t       LOCK_DIE_ON_ERROR, 0444,\n     -+\t\t\t\t\t       LOCKFILE_PID_COMMIT_GRAPH);\n     - \t\tf = hashfd(ctx->r->hash_algo,\n     - \t\t\t   get_lock_file_fd(&lk), get_lock_file_path(&lk));\n     - \t}\n      \n       ## compat/mingw.c ##\n      @@ compat/mingw.c: int mingw_kill(pid_t pid, int sig)\n     @@ compat/mingw.c: int mingw_kill(pid_t pid, int sig)\n       \n       \terrno = EINVAL;\n      \n     - ## config.c ##\n     -@@ config.c: int repo_config_set_multivar_in_file_gently(struct repository *r,\n     - \t * The lock serves a purpose in addition to locking: the new\n     - \t * contents of .git/config will be written into it.\n     - \t */\n     --\tfd = hold_lock_file_for_update(&lock, config_filename, 0);\n     -+\tfd = hold_lock_file_for_update(&lock, config_filename, 0,\n     -+\t\t\t\t       LOCKFILE_PID_CONFIG);\n     - \tif (fd < 0) {\n     - \t\terror_errno(_(\"could not lock config file %s\"), config_filename);\n     - \t\tret = CONFIG_NO_LOCK;\n     -@@ config.c: static int repo_config_copy_or_rename_section_in_file(\n     - \tif (!config_filename)\n     - \t\tconfig_filename = filename_buf = repo_git_path(r, \"config\");\n     - \n     --\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0);\n     -+\tout_fd = hold_lock_file_for_update(&lock, config_filename, 0,\n     -+\t\t\t\t\t   LOCKFILE_PID_CONFIG);\n     - \tif (out_fd < 0) {\n     - \t\tret = error(_(\"could not lock config file %s\"), config_filename);\n     - \t\tgoto out;\n     -\n       ## environment.c ##\n      @@\n       #include \"gettext.h\"\n     @@ environment.c\n       #include \"mailmap.h\"\n       #include \"object-name.h\"\n       #include \"repository.h\"\n     -@@ environment.c: next_name:\n     - \treturn (current & ~negative) | positive;\n     - }\n     - \n     -+static const struct lockfile_pid_component_name {\n     -+\tconst char *name;\n     -+\tenum lockfile_pid_component component_bits;\n     -+} lockfile_pid_component_names[] = {\n     -+\t{ \"index\", LOCKFILE_PID_INDEX },\n     -+\t{ \"config\", LOCKFILE_PID_CONFIG },\n     -+\t{ \"refs\", LOCKFILE_PID_REFS },\n     -+\t{ \"commit-graph\", LOCKFILE_PID_COMMIT_GRAPH },\n     -+\t{ \"midx\", LOCKFILE_PID_MIDX },\n     -+\t{ \"shallow\", LOCKFILE_PID_SHALLOW },\n     -+\t{ \"gc\", LOCKFILE_PID_GC },\n     -+\t{ \"other\", LOCKFILE_PID_OTHER },\n     -+\t{ \"all\", LOCKFILE_PID_ALL },\n     -+};\n     -+\n     -+static enum lockfile_pid_component parse_lockfile_pid_components(const char *var,\n     -+\t\t\t\t\t\t\t\t const char *string)\n     -+{\n     -+\tenum lockfile_pid_component current = LOCKFILE_PID_DEFAULT;\n     -+\tenum lockfile_pid_component positive = 0, negative = 0;\n     -+\n     -+\twhile (string) {\n     -+\t\tsize_t len;\n     -+\t\tconst char *ep;\n     -+\t\tint negated = 0;\n     -+\t\tint found = 0;\n     -+\n     -+\t\tstring = string + strspn(string, \", \\t\\n\\r\");\n     -+\t\tep = strchrnul(string, ',');\n     -+\t\tlen = ep - string;\n     -+\t\tif (len == 4 && !strncmp(string, \"none\", 4)) {\n     -+\t\t\tcurrent = LOCKFILE_PID_NONE;\n     -+\t\t\tgoto next_name;\n     -+\t\t}\n     -+\n     -+\t\tif (*string == '-') {\n     -+\t\t\tnegated = 1;\n     -+\t\t\tstring++;\n     -+\t\t\tlen--;\n     -+\t\t\tif (!len)\n     -+\t\t\t\twarning(_(\"invalid value for variable %s\"), var);\n     -+\t\t}\n     -+\n     -+\t\tif (!len)\n     -+\t\t\tbreak;\n     -+\n     -+\t\tfor (size_t i = 0; i < ARRAY_SIZE(lockfile_pid_component_names); ++i) {\n     -+\t\t\tconst struct lockfile_pid_component_name *n = &lockfile_pid_component_names[i];\n     -+\n     -+\t\t\tif (strncmp(n->name, string, len) || strlen(n->name) != len)\n     -+\t\t\t\tcontinue;\n     -+\n     -+\t\t\tfound = 1;\n     -+\t\t\tif (negated)\n     -+\t\t\t\tnegative |= n->component_bits;\n     -+\t\t\telse\n     -+\t\t\t\tpositive |= n->component_bits;\n     -+\t\t}\n     -+\n     -+\t\tif (!found) {\n     -+\t\t\tchar *component = xstrndup(string, len);\n     -+\t\t\twarning(_(\"ignoring unknown core.lockfilePid component '%s'\"), component);\n     -+\t\t\tfree(component);\n     -+\t\t}\n     -+\n     -+next_name:\n     -+\t\tstring = ep;\n     -+\t}\n     -+\n     -+\treturn (current & ~negative) | positive;\n     -+}\n     -+\n     - static int git_default_core_config(const char *var, const char *value,\n     - \t\t\t\t   const struct config_context *ctx, void *cb)\n     - {\n      @@ environment.c: static int git_default_core_config(const char *var, const char *value,\n       \t\treturn 0;\n       \t}\n       \n      +\tif (!strcmp(var, \"core.lockfilepid\")) {\n     -+\t\tif (!value)\n     -+\t\t\treturn config_error_nonbool(var);\n     -+\t\tlockfile_pid_components = parse_lockfile_pid_components(var, value);\n     ++\t\tlockfile_pid_enabled = git_config_bool(var, value);\n      +\t\treturn 0;\n      +\t}\n      +\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      + */\n      +\n      +/* Global config variable, initialized from core.lockfilePid */\n     -+enum lockfile_pid_component lockfile_pid_components = LOCKFILE_PID_DEFAULT;\n     ++int lockfile_pid_enabled;\n      +\n      +/*\n      + * Path generation helpers.\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +\tstrbuf_addstr(out, LOCK_SUFFIX);\n      +}\n      +\n     -+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode,\n     -+\t\t\t\t\t     enum lockfile_pid_component component)\n     ++static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n      +{\n      +\tstruct strbuf content = STRBUF_INIT;\n      +\tstruct tempfile *pid_tempfile = NULL;\n      +\tint fd = -1;\n      +\n     -+\tif (!(lockfile_pid_components & component))\n     ++\tif (!lockfile_pid_enabled)\n      +\t\tgoto out;\n      +\n      +\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n      +\tif (write_in_full(fd, content.buf, content.len) < 0) {\n      +\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n     ++\t\tclose(fd);\n     ++\t\tfd = -1;\n     ++\t\tunlink(pid_path);\n      +\t\tgoto out;\n      +\t}\n      +\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +\n       /* Make sure errno contains a meaningful value on error */\n       static int lock_file(struct lock_file *lk, const char *path, int flags,\n     --\t\t     int mode)\n     -+\t\t     int mode, enum lockfile_pid_component component)\n     + \t\t     int mode)\n       {\n      -\tstruct strbuf filename = STRBUF_INIT;\n      +\tstruct strbuf base_path = STRBUF_INIT;\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +\n      +\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n      +\tif (lk->tempfile)\n     -+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode,\n     -+\t\t\t\t\t\t\tcomponent);\n     ++\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);\n       \n      -\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n      -\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n       \treturn lk->tempfile ? lk->tempfile->fd : -1;\n       }\n       \n     -@@ lockfile.c: static int lock_file(struct lock_file *lk, const char *path, int flags,\n     -  * exactly once. If timeout_ms is -1, try indefinitely.\n     -  */\n     - static int lock_file_timeout(struct lock_file *lk, const char *path,\n     --\t\t\t     int flags, long timeout_ms, int mode)\n     -+\t\t\t     int flags, long timeout_ms, int mode,\n     -+\t\t\t     enum lockfile_pid_component component)\n     - {\n     - \tint n = 1;\n     - \tint multiplier = 1;\n     -@@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n     - \tstatic int random_initialized = 0;\n     - \n     - \tif (timeout_ms == 0)\n     --\t\treturn lock_file(lk, path, flags, mode);\n     -+\t\treturn lock_file(lk, path, flags, mode, component);\n     - \n     - \tif (!random_initialized) {\n     - \t\tsrand((unsigned int)getpid());\n     -@@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n     - \t\tlong backoff_ms, wait_ms;\n     - \t\tint fd;\n     - \n     --\t\tfd = lock_file(lk, path, flags, mode);\n     -+\t\tfd = lock_file(lk, path, flags, mode, component);\n     - \n     - \t\tif (fd >= 0)\n     - \t\t\treturn fd; /* success */\n      @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n       void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n       {\n     @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n      +\n      +\t\t/*\n      +\t\t * Try to read PID file unconditionally - it may exist if\n     -+\t\t * core.lockfilePid was enabled for this component.\n     ++\t\t * core.lockfilePid was enabled.\n      +\t\t */\n      +\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n      +\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n     @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n       }\n       \n       NORETURN void unable_to_lock_die(const char *path, int err)\n     -@@ lockfile.c: NORETURN void unable_to_lock_die(const char *path, int err)\n     - /* This should return a meaningful errno on failure */\n     - int hold_lock_file_for_update_timeout_mode(struct lock_file *lk,\n     - \t\t\t\t\t   const char *path, int flags,\n     --\t\t\t\t\t   long timeout_ms, int mode)\n     -+\t\t\t\t\t   long timeout_ms, int mode,\n     -+\t\t\t\t\t   enum lockfile_pid_component component)\n     - {\n     --\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode);\n     -+\tint fd = lock_file_timeout(lk, path, flags, timeout_ms, mode, component);\n     - \tif (fd < 0) {\n     - \t\tif (flags & LOCK_DIE_ON_ERROR)\n     - \t\t\tunable_to_lock_die(path, errno);\n      @@ lockfile.c: int commit_lock_file(struct lock_file *lk)\n       {\n       \tchar *result_path = get_locked_file_path(lk);\n     @@ lockfile.h: struct lock_file {\n      +#define LOCK_PID_MAXLEN 32\n      +\n      +/*\n     -+ * Per-component lock PID file configuration, following core.fsync pattern.\n     -+ * Each component can be individually enabled via core.lockfilePid config.\n     -+ */\n     -+enum lockfile_pid_component {\n     -+\tLOCKFILE_PID_NONE = 0,\n     -+\tLOCKFILE_PID_INDEX = 1 << 0, /* .git/index.lock */\n     -+\tLOCKFILE_PID_CONFIG = 1 << 1, /* .git/config.lock */\n     -+\tLOCKFILE_PID_REFS = 1 << 2, /* refs locks */\n     -+\tLOCKFILE_PID_COMMIT_GRAPH = 1 << 3, /* commit-graph.lock */\n     -+\tLOCKFILE_PID_MIDX = 1 << 4, /* multi-pack-index.lock */\n     -+\tLOCKFILE_PID_SHALLOW = 1 << 5, /* shallow file */\n     -+\tLOCKFILE_PID_GC = 1 << 6, /* gc locks */\n     -+\tLOCKFILE_PID_OTHER = 1 << 7, /* other locks */\n     -+};\n     -+\n     -+#define LOCKFILE_PID_ALL (LOCKFILE_PID_INDEX | LOCKFILE_PID_CONFIG |      \\\n     -+\t\t\t  LOCKFILE_PID_REFS | LOCKFILE_PID_COMMIT_GRAPH | \\\n     -+\t\t\t  LOCKFILE_PID_MIDX | LOCKFILE_PID_SHALLOW |      \\\n     -+\t\t\t  LOCKFILE_PID_GC | LOCKFILE_PID_OTHER)\n     -+#define LOCKFILE_PID_DEFAULT LOCKFILE_PID_NONE\n     -+\n     -+/*\n     -+ * Bitmask indicating which components should create PID files.\n     -+ * Configured via core.lockfilePid.\n     ++ * Whether to create PID files alongside lock files.\n     ++ * Configured via core.lockfilePid (boolean).\n      + */\n     -+extern enum lockfile_pid_component lockfile_pid_components;\n     ++extern int lockfile_pid_enabled;\n       \n       /*\n        * Flags\n      @@ lockfile.h: struct lock_file {\n     -  * timeout_ms milliseconds. If timeout_ms is 0, try exactly once; if\n     -  * timeout_ms is -1, retry indefinitely. The flags argument, error\n        * handling, and mode are described above.\n     -+ *\n     -+ * The `component` argument specifies which lock PID component this lock\n     -+ * belongs to, for selective PID file creation via core.lockfilePid config.\n        */\n       int hold_lock_file_for_update_timeout_mode(\n      -\t\tstruct lock_file *lk, const char *path,\n      -\t\tint flags, long timeout_ms, int mode);\n      +\tstruct lock_file *lk, const char *path,\n     -+\tint flags, long timeout_ms, int mode,\n     -+\tenum lockfile_pid_component component);\n     ++\tint flags, long timeout_ms, int mode);\n       \n       static inline int hold_lock_file_for_update_timeout(\n      -\t\tstruct lock_file *lk, const char *path,\n      -\t\tint flags, long timeout_ms)\n      +\tstruct lock_file *lk, const char *path,\n     -+\tint flags, long timeout_ms,\n     -+\tenum lockfile_pid_component component)\n     ++\tint flags, long timeout_ms)\n       {\n       \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n     --\t\t\t\t\t\t      timeout_ms, 0666);\n     -+\t\t\t\t\t\t      timeout_ms, 0666,\n     -+\t\t\t\t\t\t      component);\n     - }\n     - \n     - /*\n     + \t\t\t\t\t\t      timeout_ms, 0666);\n      @@ lockfile.h: static inline int hold_lock_file_for_update_timeout(\n        * argument and error handling are described above.\n        */\n       static inline int hold_lock_file_for_update(\n      -\t\tstruct lock_file *lk, const char *path,\n      -\t\tint flags)\n     -+\tstruct lock_file *lk, const char *path,\n     -+\tint flags, enum lockfile_pid_component component)\n     ++\tstruct lock_file *lk, const char *path, int flags)\n       {\n     --\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n     -+\treturn hold_lock_file_for_update_timeout(lk, path, flags, 0, component);\n     + \treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n       }\n       \n       static inline int hold_lock_file_for_update_mode(\n      -\t\tstruct lock_file *lk, const char *path,\n      -\t\tint flags, int mode)\n      +\tstruct lock_file *lk, const char *path,\n     -+\tint flags, int mode, enum lockfile_pid_component component)\n     ++\tint flags, int mode)\n       {\n     --\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n     -+\treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode,\n     -+\t\t\t\t\t\t      component);\n     + \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n       }\n     - \n     - /*\n      @@ lockfile.h: static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n       \n       /*\n     @@ lockfile.h: static inline int commit_lock_file_to(struct lock_file *lk, const ch\n       \n       #endif /* LOCKFILE_H */\n      \n     - ## loose.c ##\n     -@@ loose.c: int repo_write_loose_object_map(struct repository *repo)\n     - \t\treturn 0;\n     - \n     - \trepo_common_path_replace(repo, &path, \"objects/loose-object-idx\");\n     --\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n     -+\tfd = hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n     -+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     - \titer = kh_begin(map);\n     - \tif (write_in_full(fd, loose_object_header, strlen(loose_object_header)) < 0)\n     - \t\tgoto errout;\n     -@@ loose.c: static int write_one_object(struct odb_source *source,\n     - \tstruct strbuf buf = STRBUF_INIT, path = STRBUF_INIT;\n     - \n     - \tstrbuf_addf(&path, \"%s/loose-object-idx\", source->path);\n     --\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1);\n     -+\thold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1,\n     -+\t\t\t\t\t  LOCKFILE_PID_OTHER);\n     - \n     - \tfd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666);\n     - \tif (fd < 0)\n     -\n     - ## midx-write.c ##\n     -@@ midx-write.c: static int write_midx_internal(struct odb_source *source,\n     - \t\tstruct strbuf lock_name = STRBUF_INIT;\n     - \n     - \t\tget_midx_chain_filename(source, &lock_name);\n     --\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);\n     -+\t\thold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n     - \t\tstrbuf_release(&lock_name);\n     - \n     - \t\tincr = mks_tempfile_m(midx_name.buf, 0444);\n     -@@ midx-write.c: static int write_midx_internal(struct odb_source *source,\n     - \t\tf = hashfd(r->hash_algo, get_tempfile_fd(incr),\n     - \t\t\t   get_tempfile_path(incr));\n     - \t} else {\n     --\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR);\n     -+\t\thold_lock_file_for_update(&lk, midx_name.buf, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t  LOCKFILE_PID_MIDX);\n     - \t\tf = hashfd(r->hash_algo, get_lock_file_fd(&lk),\n     - \t\t\t   get_lock_file_path(&lk));\n     - \t}\n     -\n     - ## odb.c ##\n     -@@ odb.c: static int odb_source_write_alternate(struct odb_source *source,\n     - \tint found = 0;\n     - \tint ret;\n     - \n     --\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&lock, path, LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t  LOCKFILE_PID_OTHER);\n     - \tout = fdopen_lock_file(&lock, \"w\");\n     - \tif (!out) {\n     - \t\tret = error_errno(_(\"unable to fdopen alternates lockfile\"));\n     -\n     - ## refs/files-backend.c ##\n     -@@ refs/files-backend.c: retry:\n     - \n     - \tif (hold_lock_file_for_update_timeout(\n     - \t\t\t    &lock->lk, ref_file.buf, LOCK_NO_DEREF,\n     --\t\t\t    get_files_ref_lock_timeout_ms()) < 0) {\n     -+\t\t\t    get_files_ref_lock_timeout_ms(),\n     -+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n     - \t\tint myerr = errno;\n     - \t\terrno = 0;\n     - \t\tif (myerr == ENOENT && --attempts_remaining > 0) {\n     -@@ refs/files-backend.c: static int create_reflock(const char *path, void *cb)\n     - \n     - \treturn hold_lock_file_for_update_timeout(\n     - \t\t\tlk, path, LOCK_NO_DEREF,\n     --\t\t\tget_files_ref_lock_timeout_ms()) < 0 ? -1 : 0;\n     -+\t\t\tget_files_ref_lock_timeout_ms(),\n     -+\t\t\tLOCKFILE_PID_REFS) < 0 ? -1 : 0;\n     - }\n     - \n     - /*\n     -@@ refs/files-backend.c: static int files_reflog_expire(struct ref_store *ref_store,\n     - \t\t * work we need, including cleaning up if the program\n     - \t\t * exits unexpectedly.\n     - \t\t */\n     --\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {\n     -+\t\tif (hold_lock_file_for_update(&reflog_lock, log_file, 0,\n     -+\t\t\t\t\t      LOCKFILE_PID_REFS) < 0) {\n     - \t\t\tstruct strbuf err = STRBUF_INIT;\n     - \t\t\tunable_to_lock_message(log_file, errno, &err);\n     - \t\t\terror(\"%s\", err.buf);\n     -\n     - ## refs/packed-backend.c ##\n     -@@ refs/packed-backend.c: int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)\n     - \tif (hold_lock_file_for_update_timeout(\n     - \t\t\t    &refs->lock,\n     - \t\t\t    refs->path,\n     --\t\t\t    flags, timeout_value) < 0) {\n     -+\t\t\t    flags, timeout_value,\n     -+\t\t\t    LOCKFILE_PID_REFS) < 0) {\n     - \t\tunable_to_lock_message(refs->path, errno, err);\n     - \t\treturn -1;\n     - \t}\n     -\n     - ## reftable/system.c ##\n     -@@ reftable/system.c: int flock_acquire(struct reftable_flock *l, const char *target_path,\n     - \t\treturn REFTABLE_OUT_OF_MEMORY_ERROR;\n     - \n     - \terr = hold_lock_file_for_update_timeout(lockfile, target_path, LOCK_NO_DEREF,\n     --\t\t\t\t\t\ttimeout_ms);\n     -+\t\t\t\t\t\ttimeout_ms, LOCKFILE_PID_REFS);\n     - \tif (err < 0) {\n     - \t\treftable_free(lockfile);\n     - \t\tif (errno == EEXIST)\n     -\n     - ## repository.c ##\n     -@@ repository.c: int repo_hold_locked_index(struct repository *repo,\n     - {\n     - \tif (!repo->index_file)\n     - \t\tBUG(\"the repo hasn't been setup\");\n     --\treturn hold_lock_file_for_update(lf, repo->index_file, flags);\n     -+\treturn hold_lock_file_for_update(lf, repo->index_file, flags,\n     -+\t\t\t\t\t LOCKFILE_PID_INDEX);\n     - }\n     -\n     - ## rerere.c ##\n     -@@ rerere.c: int setup_rerere(struct repository *r, struct string_list *merge_rr, int flags)\n     - \telse\n     - \t\tfd = hold_lock_file_for_update(&write_lock,\n     - \t\t\t\t\t       git_path_merge_rr(r),\n     --\t\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t\t       LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     - \tread_rr(r, merge_rr);\n     - \treturn fd;\n     - }\n     -\n     - ## sequencer.c ##\n     -@@ sequencer.c: static int write_message(const void *buf, size_t len, const char *filename,\n     - {\n     - \tstruct lock_file msg_file = LOCK_INIT;\n     - \n     --\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0);\n     -+\tint msg_fd = hold_lock_file_for_update(&msg_file, filename, 0,\n     -+\t\t\t\t\t       LOCKFILE_PID_OTHER);\n     - \tif (msg_fd < 0)\n     - \t\treturn error_errno(_(\"could not lock '%s'\"), filename);\n     - \tif (write_in_full(msg_fd, buf, len) < 0) {\n     -@@ sequencer.c: static int save_todo(struct todo_list *todo_list, struct replay_opts *opts,\n     - \tif (is_rebase_i(opts) && !reschedule)\n     - \t\tnext++;\n     - \n     --\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0);\n     -+\tfd = hold_lock_file_for_update(&todo_lock, todo_path, 0,\n     -+\t\t\t\t       LOCKFILE_PID_OTHER);\n     - \tif (fd < 0)\n     - \t\treturn error_errno(_(\"could not lock '%s'\"), todo_path);\n     - \toffset = get_item_line_offset(todo_list, next);\n     -@@ sequencer.c: static int safe_append(const char *filename, const char *fmt, ...)\n     - \tva_list ap;\n     - \tstruct lock_file lock = LOCK_INIT;\n     - \tint fd = hold_lock_file_for_update(&lock, filename,\n     --\t\t\t\t\t   LOCK_REPORT_ON_ERROR);\n     -+\t\t\t\t\t   LOCK_REPORT_ON_ERROR,\n     -+\t\t\t\t\t   LOCKFILE_PID_OTHER);\n     - \tstruct strbuf buf = STRBUF_INIT;\n     - \n     - \tif (fd < 0)\n     -@@ sequencer.c: static int write_update_refs_state(struct string_list *refs_to_oids)\n     - \t\tgoto cleanup;\n     - \t}\n     - \n     --\tif (hold_lock_file_for_update(&lock, path, 0) < 0) {\n     -+\tif (hold_lock_file_for_update(&lock, path, 0, LOCKFILE_PID_OTHER) < 0) {\n     - \t\tresult = error(_(\"another 'rebase' process appears to be running; \"\n     - \t\t\t\t \"'%s.lock' already exists\"),\n     - \t\t\t       path);\n     -\n     - ## shallow.c ##\n     -@@ shallow.c: void setup_alternate_shallow(struct shallow_lock *shallow_lock,\n     - \n     - \tfd = hold_lock_file_for_update(&shallow_lock->lock,\n     - \t\t\t\t       git_path_shallow(the_repository),\n     --\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t       LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n     - \tcheck_shallow_file_for_update(the_repository);\n     - \tif (write_shallow_commits(&sb, 0, extra)) {\n     - \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n     -@@ shallow.c: void prune_shallow(unsigned options)\n     - \t}\n     - \tfd = hold_lock_file_for_update(&shallow_lock.lock,\n     - \t\t\t\t       git_path_shallow(the_repository),\n     --\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\t\t\t\t       LOCK_DIE_ON_ERROR,\n     -+\t\t\t\t       LOCKFILE_PID_SHALLOW);\n     - \tcheck_shallow_file_for_update(the_repository);\n     - \tif (write_shallow_commits_1(&sb, 0, NULL, flags)) {\n     - \t\tif (write_in_full(fd, sb.buf, sb.len) < 0)\n     -\n       ## t/meson.build ##\n      @@ t/meson.build: integration_tests = [\n         't0028-working-tree-encoding.sh',\n     @@ t/t0031-lockfile-pid.sh (new)\n      +test_description='lock file PID info tests\n      +\n      +Tests for PID info file alongside lock files.\n     -+The feature is opt-in via core.lockfilePid config setting.\n     ++The feature is opt-in via core.lockfilePid config setting (boolean).\n      +'\n      +\n      +. ./test-lib.sh\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\tcd repo &&\n      +\t\ttouch .git/index.lock &&\n      +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n     -+\t\ttest_must_fail git -c core.lockfilePid=index add . 2>err &&\n     ++\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n      +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n      +\t\ttest_grep \"Remove the stale lock file\" err\n      +\t)\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\ttouch .git/index.lock &&\n      +\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n      +\t\t# Verify our PID is shown in the error message\n     -+\t\ttest_must_fail git -c core.lockfilePid=index add file 2>err &&\n     ++\t\ttest_must_fail git -c core.lockfilePid=true add file 2>err &&\n      +\t\ttest_grep \"held by process $shell_pid\" err\n      +\t)\n      +'\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t(\n      +\t\tcd repo4 &&\n      +\t\techo content >file &&\n     -+\t\tgit -c core.lockfilePid=index add file &&\n     ++\t\tgit -c core.lockfilePid=true add file &&\n      +\t\t# After successful add, no lock or PID files should exist\n      +\t\ttest_path_is_missing .git/index.lock &&\n      +\t\ttest_path_is_missing .git/index~pid.lock\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t)\n      +'\n      +\n     -+test_expect_success 'core.lockfilePid=all enables for all components' '\n     ++test_expect_success 'core.lockfilePid=false does not create PID file' '\n      +\tgit init repo6 &&\n      +\t(\n      +\t\tcd repo6 &&\n     -+\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n     -+\t\ttest_must_fail git -c core.lockfilePid=all add . 2>err &&\n     -+\t\ttest_grep \"process 99999\" err\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'multiple components can be specified' '\n     -+\tgit init repo8 &&\n     -+\t(\n     -+\t\tcd repo8 &&\n     -+\t\ttouch .git/index.lock &&\n     -+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n     -+\t\ttest_must_fail git -c core.lockfilePid=index,config add . 2>err &&\n     -+\t\ttest_grep \"process 99999\" err\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'core.lockfilePid=none does not create PID file' '\n     -+\tgit init repo9 &&\n     -+\t(\n     -+\t\tcd repo9 &&\n      +\t\techo content >file &&\n     -+\t\tgit -c core.lockfilePid=none add file &&\n     ++\t\tgit -c core.lockfilePid=false add file &&\n      +\t\t# PID file should not be created when feature is disabled\n      +\t\ttest_path_is_missing .git/index~pid.lock\n      +\t)\n      +'\n      +\n      +test_expect_success 'existing PID files are read even when feature disabled' '\n     -+\tgit init repo10 &&\n     ++\tgit init repo7 &&\n      +\t(\n     -+\t\tcd repo10 &&\n     ++\t\tcd repo7 &&\n      +\t\ttouch .git/index.lock &&\n      +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\t# Even with lockfilePid disabled, existing PID files are read\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t)\n      +'\n      +\n     -+test_expect_success 'negative component syntax excludes specific components' '\n     -+\tgit init repo11 &&\n     -+\t(\n     -+\t\tcd repo11 &&\n     -+\t\techo content >file &&\n     -+\t\t# Enable all components except index\n     -+\t\tgit -c core.lockfilePid=all,-index add file &&\n     -+\t\t# PID file should not be created for index when excluded\n     -+\t\ttest_path_is_missing .git/index~pid.lock\n     -+\t)\n     -+'\n     -+\n      +test_done\n     -\n     - ## unix-stream-server.c ##\n     -@@ unix-stream-server.c: int unix_ss_create(const char *path,\n     - \t/*\n     - \t * Create a lock at \"<path>.lock\" if we can.\n     - \t */\n     --\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms) < 0)\n     -+\tif (hold_lock_file_for_update_timeout(&lock, path, 0, timeout_ms,\n     -+\t\t\t\t\t      LOCKFILE_PID_OTHER) < 0)\n     - \t\treturn -1;\n     - \n     - \t/*\n\n\n Documentation/config/core.adoc |  11 +++\n builtin/commit.c               |   3 +-\n builtin/gc.c                   |   6 +-\n compat/mingw.c                 |  10 ++\n environment.c                  |   6 ++\n lockfile.c                     | 170 ++++++++++++++++++++++++++++++---\n lockfile.h                     |  43 ++++++---\n t/meson.build                  |   1 +\n t/t0031-lockfile-pid.sh        | 105 ++++++++++++++++++++\n 9 files changed, 320 insertions(+), 35 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex 01202da7cd..5c4bc9206f 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a\n read-only snapshot of the same index to a location different from the\n repository's usual working tree).\n \n+core.lockfilePid::\n+\tIf true, Git will create a PID file alongside lock files. When a\n+\tlock acquisition fails and a PID file exists, Git can provide\n+\tadditional diagnostic information about the process holding the\n+\tlock, including whether it is still running. Defaults to `false`.\n++\n+The PID file is named by inserting `~pid` before the `.lock` suffix.\n+For example, if the lock file is `index.lock`, the PID file will be\n+`index~pid.lock`. The file contains a single line in the format\n+`pid <value>` followed by a newline.\n+\n core.logAllRefUpdates::\n \tEnable the reflog. Updates to a ref <ref> is logged to the file\n \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 0243f17d53..4378256fa5 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -539,8 +539,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \n \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n \t\t\t     (uintmax_t) getpid());\n-\thold_lock_file_for_update(&false_lock, path,\n-\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n \n \tcreate_base_index(current_head);\n \tadd_remove_files(&partial);\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 92c6e7b954..1dcc8dd550 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -748,8 +748,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n \n \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n-\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n \tif (!force) {\n \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n \t\tstatic char *scan_fmt;\n@@ -1016,8 +1015,7 @@ int cmd_gc(int argc,\n \n \tif (daemonized) {\n \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n-\t\thold_lock_file_for_update(&log_lock, path,\n-\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n \t\tdup2(get_lock_file_fd(&log_lock), 2);\n \t\tatexit(process_log_file_at_exit);\n \t\tfree(path);\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 939f938fe2..146b2585ce 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)\n \t\t\tCloseHandle(h);\n \t\t\treturn 0;\n \t\t}\n+\t\t/*\n+\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n+\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n+\t\t * compatibility with kill(pid, 0).\n+\t\t */\n+\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n+\t\t\terrno = ESRCH;\n+\t\telse\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n \t}\n \n \terrno = EINVAL;\ndiff --git a/environment.c b/environment.c\nindex a770b5921d..4adcce8606 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -21,6 +21,7 @@\n #include \"gettext.h\"\n #include \"git-zlib.h\"\n #include \"ident.h\"\n+#include \"lockfile.h\"\n #include \"mailmap.h\"\n #include \"object-name.h\"\n #include \"repository.h\"\n@@ -532,6 +533,11 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"core.lockfilepid\")) {\n+\t\tlockfile_pid_enabled = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"core.createobject\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..ac7eb86541 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,19 +74,119 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a foo~pid.lock file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ *\n+ * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n+ * forbidden in refnames and allowed in Windows filenames, guaranteeing\n+ * no collision with the refs namespace.\n+ */\n+\n+/* Global config variable, initialized from core.lockfilePid */\n+int lockfile_pid_enabled;\n+\n+/*\n+ * Path generation helpers.\n+ * Given base path \"foo\", generate:\n+ *   - lock path: \"foo.lock\"\n+ *   - pid path:  \"foo-pid.lock\"\n+ */\n+static void get_lock_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static void get_pid_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_PID_INFIX);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd = -1;\n+\n+\tif (!lockfile_pid_enabled)\n+\t\tgoto out;\n+\n+\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n+\tif (fd < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n+\tif (write_in_full(fd, content.buf, content.len) < 0) {\n+\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n+\t\tclose(fd);\n+\t\tfd = -1;\n+\t\tunlink(pid_path);\n+\t\tgoto out;\n+\t}\n+\n+\tclose(fd);\n+\tfd = -1;\n+\tpid_tempfile = register_tempfile(pid_path);\n+\n+out:\n+\tif (fd >= 0)\n+\t\tclose(fd);\n+\tstrbuf_release(&content);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tconst char *val;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n+\t\tgoto out;\n+\n+\tstrbuf_rtrim(&content);\n+\n+\tif (skip_prefix(content.buf, \"pid \", &val)) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(val, &endptr, 10);\n+\t\tif (*pid_out > 0 && !*endptr)\n+\t\t\tret = 0;\n+\t}\n+\n+\tif (ret)\n+\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n \t\t     int mode)\n {\n-\tstruct strbuf filename = STRBUF_INIT;\n+\tstruct strbuf base_path = STRBUF_INIT;\n+\tstruct strbuf lock_path = STRBUF_INIT;\n+\tstruct strbuf pid_path = STRBUF_INIT;\n \n-\tstrbuf_addstr(&filename, path);\n+\tstrbuf_addstr(&base_path, path);\n \tif (!(flags & LOCK_NO_DEREF))\n-\t\tresolve_symlink(&filename);\n+\t\tresolve_symlink(&base_path);\n+\n+\tget_lock_path(&lock_path, base_path.buf);\n+\tget_pid_path(&pid_path, base_path.buf);\n+\n+\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);\n \n-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n-\tstrbuf_release(&filename);\n+\tstrbuf_release(&base_path);\n+\tstrbuf_release(&lock_path);\n+\tstrbuf_release(&pid_path);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n \n@@ -151,16 +254,47 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n-\t} else\n+\t\tconst char *abs_path = absolute_path(path);\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tstruct strbuf pid_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tget_lock_path(&lock_path, abs_path);\n+\t\tget_pid_path(&pid_path, abs_path);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\t/*\n+\t\t * Try to read PID file unconditionally - it may exist if\n+\t\t * core.lockfilePid was enabled.\n+\t\t */\n+\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n+\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n+\t\t\telse if (errno == ESRCH)\n+\t\t\t\tpid_status = -1; /* no such process - stale lock */\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n+\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n+\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n+\t\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n+\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n+\n+\t\tstrbuf_release(&lock_path);\n+\t\tstrbuf_release(&pid_path);\n+\t} else {\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n+\t}\n }\n \n NORETURN void unable_to_lock_die(const char *path, int err)\n@@ -207,6 +341,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +352,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..e7233f28de 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,22 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/*\n+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n+ * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n+ * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n+ */\n+#define LOCK_PID_INFIX \"~pid\"\n+#define LOCK_PID_INFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/*\n+ * Whether to create PID files alongside lock files.\n+ * Configured via core.lockfilePid (boolean).\n+ */\n+extern int lockfile_pid_enabled;\n \n /*\n  * Flags\n@@ -169,12 +186,12 @@ struct lock_file {\n  * handling, and mode are described above.\n  */\n int hold_lock_file_for_update_timeout_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms, int mode);\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms, int mode);\n \n static inline int hold_lock_file_for_update_timeout(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n \t\t\t\t\t\t      timeout_ms, 0666);\n@@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(\n  * argument and error handling are described above.\n  */\n static inline int hold_lock_file_for_update(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags)\n+\tstruct lock_file *lk, const char *path, int flags)\n {\n \treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n }\n \n static inline int hold_lock_file_for_update_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, int mode)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, int mode)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n }\n@@ -319,13 +335,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..2aec2c011e 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..44f3ba1f25\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,105 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via core.lockfilePid config setting (boolean).\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"Remove the stale lock file\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Get the correct PID for this platform\n+\t\tshell_pid=$$ &&\n+\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n+\t\tthen\n+\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n+\t\t\t# uses Windows PIDs. Use the Windows PID.\n+\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n+\t\tfi &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail git -c core.lockfilePid=true add file 2>err &&\n+\t\ttest_grep \"held by process $shell_pid\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=true add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\ttest_path_is_missing .git/index.lock &&\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=false does not create PID file' '\n+\tgit init repo6 &&\n+\t(\n+\t\tcd repo6 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=false add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'existing PID files are read even when feature disabled' '\n+\tgit init repo7 &&\n+\t(\n+\t\tcd repo7 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\t# Even with lockfilePid disabled, existing PID files are read\n+\t\t# to help diagnose stale locks\n+\t\ttest_must_fail git add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_done\n\nbase-commit: 66ce5f8e8872f0183bb137911c52b07f1f242d13\n-- \ngitgitgadget\n"},{"id":"533260","messageId":"xmqqbjj4hnkr.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v4.git.1767804355831.gitgitgadget@gmail.com","subject":"Re: [PATCH v4] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-08T01:59:00Z","receivedAt":"2026-01-08T01:59:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> For a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\n> tilde character is forbidden in refnames and allowed in Windows\n> filenames, which guarantees no collision with the refs namespace\n> (e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file contains\n> a single line in the format \"pid <value>\" followed by a newline.\n\nFor a lockfile used for manipulating refs, that would be good, but\nit would be more assuring to enumerate what other things do we use\nlockfiles for, especially things whose filenames are under end-user\ncontrol (e.g., $GIT_DIR/index using index.lock and index~pid.lock\nwould probably be OK as the \"index\" part is unlikely to be renamed.\nIs the ref-files the only thing that needs to address names\ncontrolled by the end-user?).\n\n> The PID file is created when a lock is acquired (if enabled), and\n> automatically cleaned up when the lock is released (via commit or\n> rollback). The file is registered as a tempfile so it gets cleaned up\n> by signal and atexit handlers if the process terminates abnormally.\n>\n> When a lock conflict occurs, the code checks for an existing PID file\n> and, if found, uses kill(pid, 0) to determine if the process is still\n> running. This allows providing context-aware error messages:\n>\n>   Lock is held by process 12345. Wait for it to finish, or remove\n>   the lock file to continue.\n>\n> Or for a stale lock:\n>\n>   Lock was held by process 12345, which is no longer running.\n>   Remove the stale lock file to continue.\n\nTwo additional thoughts.\n\n * 12345 may be running as your kill(12345,0) did not fail, but that\n   may be a process different from the one that held the lock and\n   then died without cleaning up.\n\n * 12345 may be running and may have the lock.  You may decide to\n   break the lock and take the risk of breaking your repository, or\n   you may wait until that process finishes what it is doing.  If\n   you go the former route, however, would it make more sense to\n   kill the process so that it does not any harm interfering with\n   your new attempt?  \"remove the lock file to continue\" sounds like\n   a fairly irresponsible suggestion.  Not that \"kill the process,\n   remove the lock file and then do whatever you wanted to do\" is\n   any better.\n\n> The feature is controlled via core.lockfilePid configuration (boolean).\n> Defaults to false. When enabled, PID files are created for all lock\n> operations.\n>\n> Existing PID files are always read when displaying lock errors,\n> regardless of the core.lockfilePid setting. This ensures helpful\n> diagnostics even when the feature was previously enabled and later\n> disabled.\n\nThis makes it even more easily triggerable to notice a running\nprocess that happens to be reusing a process ID an old process used\nto have (i.e. the first \"thought\" above---the phrasing of the\nmessage needs to be carefully thought out).\n\n>     Changes in v4:\n>     \n>      * Simplified core.lockfilePid from per-component configuration to a\n>        simple boolean (true/false), as suggested by Jeff and Patrick. The\n>        per-subsystem granularity added complexity without a clear use case -\n>        if someone wants PID files for debugging, they likely want them\n>        everywhere.\n\nVery nice.\n\n>      * Removed the enum lockfile_pid_component and all LOCKFILE_PID_*\n>        constants\n>      * Removed the component parameter from hold_lock_file_for_update*()\n>        functions, restoring their original signatures\n>      * Simplified config parsing to use git_config_bool() instead of the\n>        fsync-style component parser\n>      * Fixed error handling in create_lock_pid_file() to unlink the file on\n>        write failure, as noted by Junio\n>      * Clarified documentation to describe the file format directly (\"pid \"\n>        followed by a newline) rather than referencing \"Git object headers\"\n>      * Updated tests and documentation to reflect the boolean config\n\nNicely enumerated changes.\n\n>  Documentation/config/core.adoc |  11 +++\n>  builtin/commit.c               |   3 +-\n>  builtin/gc.c                   |   6 +-\n>  compat/mingw.c                 |  10 ++\n>  environment.c                  |   6 ++\n>  lockfile.c                     | 170 ++++++++++++++++++++++++++++++---\n>  lockfile.h                     |  43 ++++++---\n>  t/meson.build                  |   1 +\n>  t/t0031-lockfile-pid.sh        | 105 ++++++++++++++++++++\n>  9 files changed, 320 insertions(+), 35 deletions(-)\n>  create mode 100755 t/t0031-lockfile-pid.sh\n>\n> diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\n> index 01202da7cd..5c4bc9206f 100644\n> --- a/Documentation/config/core.adoc\n> +++ b/Documentation/config/core.adoc\n> @@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a\n>  read-only snapshot of the same index to a location different from the\n>  repository's usual working tree).\n>  \n> +core.lockfilePid::\n> +\tIf true, Git will create a PID file alongside lock files. When a\n> +\tlock acquisition fails and a PID file exists, Git can provide\n\nIs it \"Git can provide\", or \"Git provides\"?  I thought that I read\nin the proposed log message that this configuration variable does\nnot control what happens when we see the pid file?\n\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 0243f17d53..4378256fa5 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -539,8 +539,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>  \n>  \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n>  \t\t\t     (uintmax_t) getpid());\n> -\thold_lock_file_for_update(&false_lock, path,\n> -\t\t\t\t  LOCK_DIE_ON_ERROR);\n> +\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n\nIs this change necessary to achieve the goal of your topic?\n\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 92c6e7b954..1dcc8dd550 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -748,8 +748,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n>  \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n>  \n>  \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n> -\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n> -\t\t\t\t       LOCK_DIE_ON_ERROR);\n> +\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n\nDitto.\n\n> @@ -1016,8 +1015,7 @@ int cmd_gc(int argc,\n>  \n>  \tif (daemonized) {\n>  \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n> -\t\thold_lock_file_for_update(&log_lock, path,\n> -\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n> +\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n\nDitto.\n\n> diff --git a/compat/mingw.c b/compat/mingw.c\n> index 939f938fe2..146b2585ce 100644\n> --- a/compat/mingw.c\n> +++ b/compat/mingw.c\n> @@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)\n>  \t\t\tCloseHandle(h);\n>  \t\t\treturn 0;\n>  \t\t}\n> +\t\t/*\n> +\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n> +\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n> +\t\t * compatibility with kill(pid, 0).\n> +\t\t */\n> +\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n> +\t\t\terrno = ESRCH;\n> +\t\telse\n> +\t\t\terrno = err_win_to_posix(GetLastError());\n> +\t\treturn -1;\n>  \t}\n>  \n>  \terrno = EINVAL;\n> diff --git a/environment.c b/environment.c\n> index a770b5921d..4adcce8606 100644\n> --- a/environment.c\n> +++ b/environment.c\n> @@ -21,6 +21,7 @@\n>  #include \"gettext.h\"\n>  #include \"git-zlib.h\"\n>  #include \"ident.h\"\n> +#include \"lockfile.h\"\n>  #include \"mailmap.h\"\n>  #include \"object-name.h\"\n>  #include \"repository.h\"\n> @@ -532,6 +533,11 @@ static int git_default_core_config(const char *var, const char *value,\n>  \t\treturn 0;\n>  \t}\n>  \n> +\tif (!strcmp(var, \"core.lockfilepid\")) {\n> +\t\tlockfile_pid_enabled = git_config_bool(var, value);\n> +\t\treturn 0;\n> +\t}\n> +\n>  \tif (!strcmp(var, \"core.createobject\")) {\n>  \t\tif (!value)\n>  \t\t\treturn config_error_nonbool(var);\n\nOK.  Presumably lockfile.h declares the variable.\n\n> diff --git a/lockfile.c b/lockfile.c\n> index 1d5ed01682..ac7eb86541 100644\n> --- a/lockfile.c\n> +++ b/lockfile.c\n> @@ -71,19 +74,119 @@ static void resolve_symlink(struct strbuf *path)\n> ...\n> + * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n> + * forbidden in refnames and allowed in Windows filenames, guaranteeing\n> + * no collision with the refs namespace.\n\nA comment on other uses of lockfiles is needed here.\n\n> +/*\n> + * Path generation helpers.\n> + * Given base path \"foo\", generate:\n> + *   - lock path: \"foo.lock\"\n> + *   - pid path:  \"foo-pid.lock\"\n> + */\n\nIs the caller responsible for supplying an empty \"out\" buf?  I am\nwondering if it would make it easier to use if these functions\ncleared the supplied strbuf as the first thing they do.\n\nYou could imagine a calling sequence\n\n\tstrbuf_reset(&outbuf);\n\tstrbuf_add(&outbuf, \"seed\");\n\tget_lock_path(&outbuf, \"index\");\n\t... use outbuf.buf ...\n\n\tstrbuf_reset(&outbuf);\n\tstrbuf_add(&outbuf, \"seed\");\n\tget_pid_path(&outbuf, \"index\");\n\t... use outbuf.buf ...\n\n\nto give some prefix to the filenames used for locks, and making\nthese functions clear the strbuf upfront would not allow such a\ncalling sequence, but such a caller can just feed \"seedindex\" to the\nAPI, so it does not feel like a huge loss.\n\nOf course, we could go to the other extreme, and make it the\nresponsibility of the caller to stuff \"path\" to the strbuf before\ncalling these functions, i.e.,\n\n\tstrbuf_reset(&outbuf);\n\tstrbuf_add(&outbuf, \"index\");\n\tget_lock_path(&outbuf);\n\nI am not sure if it is a good direction to go, though.  Certainly\nthe functions need to be renamed to \"add_lock_suffix()\" and\n\"add_pid_suffix\" respectively, if we go that route.\n\n> +static void get_lock_path(struct strbuf *out, const char *path)\n> +{\n> +\tstrbuf_addstr(out, path);\n> +\tstrbuf_addstr(out, LOCK_SUFFIX);\n> +}\n> +\n> +static void get_pid_path(struct strbuf *out, const char *path)\n> +{\n> +\tstrbuf_addstr(out, path);\n> +\tstrbuf_addstr(out, LOCK_PID_INFIX);\n> +\tstrbuf_addstr(out, LOCK_SUFFIX);\n> +}\n\n> +static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n> +{\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tstruct tempfile *pid_tempfile = NULL;\n> +\tint fd = -1;\n> +\n> +\tif (!lockfile_pid_enabled)\n> +\t\tgoto out;\n> +\n> +\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n> +\tif (fd < 0)\n> +\t\tgoto out;\n> +\n> +\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n> +\tif (write_in_full(fd, content.buf, content.len) < 0) {\n> +\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> +\t\tclose(fd);\n> +\t\tfd = -1;\n\nYou can leave the above two lines out, as jumping to \"out\" would\ntake care of closing the file descriptor.\n\n> +\t\tunlink(pid_path);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tclose(fd);\n> +\tfd = -1;\n> +\tpid_tempfile = register_tempfile(pid_path);\n> +\n> +out:\n> +\tif (fd >= 0)\n> +\t\tclose(fd);\n> +\tstrbuf_release(&content);\n> +\treturn pid_tempfile;\n> +}\n\n> +static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n> +{\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tconst char *val;\n> +\tint ret = -1;\n> +\n> +\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n> +\t\tgoto out;\n> +\n> +\tstrbuf_rtrim(&content);\n> +\n> +\tif (skip_prefix(content.buf, \"pid \", &val)) {\n> +\t\tchar *endptr;\n> +\t\t*pid_out = strtoumax(val, &endptr, 10);\n> +\t\tif (*pid_out > 0 && !*endptr)\n> +\t\t\tret = 0;\n\nZero is a valid process ID; comparing endptr with the val to ensure\nthat we did read _some_ digits would be a more robust test.\n\nIf you drop strbuf_rtrim() above, then you can even check if endptr\npoints at a '\\n' byte to validate the whole line while ensuring that\nyou read the decimal number correctly.\n\n>  static int lock_file(struct lock_file *lk, const char *path, int flags,\n>  \t\t     int mode)\n>  {\n> -\tstruct strbuf filename = STRBUF_INIT;\n> +\tstruct strbuf base_path = STRBUF_INIT;\n> +\tstruct strbuf lock_path = STRBUF_INIT;\n> +\tstruct strbuf pid_path = STRBUF_INIT;\n>  \n> -\tstrbuf_addstr(&filename, path);\n> +\tstrbuf_addstr(&base_path, path);\n>  \tif (!(flags & LOCK_NO_DEREF))\n> -\t\tresolve_symlink(&filename);\n> +\t\tresolve_symlink(&base_path);\n> +\n> +\tget_lock_path(&lock_path, base_path.buf);\n> +\tget_pid_path(&pid_path, base_path.buf);\n\nLooking at this place alone, add_lock/pid_suffix() I alluded to\nearlier may not be _too_ bad.  There may be other callers that makes\nthe \"caller prepares the filename in the strbuf and calls add suffix\nhelpers\" pattern unwieldy.\n\n> @@ -151,16 +254,47 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n>  void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n>  {\n>  \tif (err == EEXIST) {\n> -\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n> -\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n> -\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n> -\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n> -\t\t    \"may have crashed in this repository earlier:\\n\"\n> -\t\t    \"remove the file manually to continue.\"),\n> -\t\t\t    absolute_path(path), strerror(err));\n> -\t} else\n> +\t\tconst char *abs_path = absolute_path(path);\n> +\t\tstruct strbuf lock_path = STRBUF_INIT;\n> +\t\tstruct strbuf pid_path = STRBUF_INIT;\n> +\t\tuintmax_t pid;\n> +\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n> +\n> +\t\tget_lock_path(&lock_path, abs_path);\n> +\t\tget_pid_path(&pid_path, abs_path);\n> +\n> +\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n> +\t\t\t    lock_path.buf, strerror(err));\n\nI wonder if code snippet from here ....\n\n> +\t\t/*\n> +\t\t * Try to read PID file unconditionally - it may exist if\n> +\t\t * core.lockfilePid was enabled.\n> +\t\t */\n> +\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n> +\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n> +\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n> +\t\t\telse if (errno == ESRCH)\n> +\t\t\t\tpid_status = -1; /* no such process - stale lock */\n> +\t\t}\n> +\n> +\t\tif (pid_status == 1)\n> +\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n> +\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n> +\t\t\t\t    pid);\n> +\t\telse if (pid_status == -1)\n> +\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n> +\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n> +\t\t\t\t    pid);\n> +\t\telse\n> +\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n> +\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n\n... to here should be in a separate helper function.  It would make\nthe control flow easier to follow.\n\n> +\t\tstrbuf_release(&lock_path);\n> +\t\tstrbuf_release(&pid_path);\n> +\t} else {\n>  \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n>  \t\t\t    absolute_path(path), strerror(err));\n> +\t}\n>  }\n>  \n>  NORETURN void unable_to_lock_die(const char *path, int err)\n> @@ -207,6 +341,8 @@ int commit_lock_file(struct lock_file *lk)\n>  {\n>  \tchar *result_path = get_locked_file_path(lk);\n>  \n> +\tdelete_tempfile(&lk->pid_tempfile);\n> +\n>  \tif (commit_lock_file_to(lk, result_path)) {\n>  \t\tint save_errno = errno;\n>  \t\tfree(result_path);\n> @@ -216,3 +352,9 @@ int commit_lock_file(struct lock_file *lk)\n>  \tfree(result_path);\n>  \treturn 0;\n>  }\n> +\n> +int rollback_lock_file(struct lock_file *lk)\n> +{\n> +\tdelete_tempfile(&lk->pid_tempfile);\n> +\treturn delete_tempfile(&lk->tempfile);\n> +}\n> diff --git a/lockfile.h b/lockfile.h\n> index 1bb9926497..e7233f28de 100644\n> --- a/lockfile.h\n> +++ b/lockfile.h\n> @@ -119,6 +119,7 @@\n>  \n>  struct lock_file {\n>  \tstruct tempfile *tempfile;\n> +\tstruct tempfile *pid_tempfile;\n>  };\n>  \n>  #define LOCK_INIT { 0 }\n> @@ -127,6 +128,22 @@ struct lock_file {\n>  #define LOCK_SUFFIX \".lock\"\n>  #define LOCK_SUFFIX_LEN 5\n>  \n> +/*\n> + * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n> + * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n> + * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n> + */\n> +#define LOCK_PID_INFIX \"~pid\"\n> +#define LOCK_PID_INFIX_LEN 4\n> +\n> +/* Maximum length for PID file content */\n> +#define LOCK_PID_MAXLEN 32\n> +\n> +/*\n> + * Whether to create PID files alongside lock files.\n> + * Configured via core.lockfilePid (boolean).\n> + */\n> +extern int lockfile_pid_enabled;\n>  \n>  /*\n>   * Flags\n> @@ -169,12 +186,12 @@ struct lock_file {\n>   * handling, and mode are described above.\n>   */\n>  int hold_lock_file_for_update_timeout_mode(\n> -\t\tstruct lock_file *lk, const char *path,\n> -\t\tint flags, long timeout_ms, int mode);\n> +\tstruct lock_file *lk, const char *path,\n> +\tint flags, long timeout_ms, int mode);\n>  \n>  static inline int hold_lock_file_for_update_timeout(\n> -\t\tstruct lock_file *lk, const char *path,\n> -\t\tint flags, long timeout_ms)\n> +\tstruct lock_file *lk, const char *path,\n> +\tint flags, long timeout_ms)\n>  {\n>  \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n>  \t\t\t\t\t\t      timeout_ms, 0666);\n> @@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(\n>   * argument and error handling are described above.\n>   */\n>  static inline int hold_lock_file_for_update(\n> -\t\tstruct lock_file *lk, const char *path,\n> -\t\tint flags)\n> +\tstruct lock_file *lk, const char *path, int flags)\n>  {\n>  \treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n>  }\n>  \n>  static inline int hold_lock_file_for_update_mode(\n> -\t\tstruct lock_file *lk, const char *path,\n> -\t\tint flags, int mode)\n> +\tstruct lock_file *lk, const char *path,\n> +\tint flags, int mode)\n>  {\n>  \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n>  }\n> @@ -319,13 +335,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n>  \n>  /*\n>   * Roll back `lk`: close the file descriptor and/or file pointer and\n> - * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n> - * for a `lock_file` object that has already been committed or rolled\n> - * back. No error will be returned in this case.\n> + * remove the lockfile and any associated PID file. It is a NOOP to\n> + * call `rollback_lock_file()` for a `lock_file` object that has already\n> + * been committed or rolled back. No error will be returned in this case.\n>   */\n> -static inline int rollback_lock_file(struct lock_file *lk)\n> -{\n> -\treturn delete_tempfile(&lk->tempfile);\n> -}\n> +int rollback_lock_file(struct lock_file *lk);\n>  \n>  #endif /* LOCKFILE_H */\n> diff --git a/t/meson.build b/t/meson.build\n> index 459c52a489..2aec2c011e 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -98,6 +98,7 @@ integration_tests = [\n>    't0028-working-tree-encoding.sh',\n>    't0029-core-unsetenvvars.sh',\n>    't0030-stripspace.sh',\n> +  't0031-lockfile-pid.sh',\n>    't0033-safe-directory.sh',\n>    't0034-root-safe-directory.sh',\n>    't0035-safe-bare-repository.sh',\n> diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\n> new file mode 100755\n> index 0000000000..44f3ba1f25\n> --- /dev/null\n> +++ b/t/t0031-lockfile-pid.sh\n> @@ -0,0 +1,105 @@\n> +#!/bin/sh\n> +\n> +test_description='lock file PID info tests\n> +\n> +Tests for PID info file alongside lock files.\n> +The feature is opt-in via core.lockfilePid config setting (boolean).\n> +'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'stale lock detected when PID is not running' '\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttouch .git/index.lock &&\n> +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n> +\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n> +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n> +\t\ttest_grep \"Remove the stale lock file\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'PID info not shown by default' '\n> +\tgit init repo2 &&\n> +\t(\n> +\t\tcd repo2 &&\n> +\t\ttouch .git/index.lock &&\n> +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n> +\t\ttest_must_fail git add . 2>err &&\n> +\t\t# Should not crash, just show normal error without PID\n> +\t\ttest_grep \"Unable to create\" err &&\n> +\t\t! test_grep \"is held by process\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'running process detected when PID is alive' '\n> +\tgit init repo3 &&\n> +\t(\n> +\t\tcd repo3 &&\n> +\t\techo content >file &&\n> +\t\t# Get the correct PID for this platform\n> +\t\tshell_pid=$$ &&\n> +\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n> +\t\tthen\n> +\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n> +\t\t\t# uses Windows PIDs. Use the Windows PID.\n> +\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n> +\t\tfi &&\n> +\t\t# Create a lock and PID file with current shell PID (which is running)\n> +\t\ttouch .git/index.lock &&\n> +\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n> +\t\t# Verify our PID is shown in the error message\n> +\t\ttest_must_fail git -c core.lockfilePid=true add file 2>err &&\n> +\t\ttest_grep \"held by process $shell_pid\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n> +\tgit init repo4 &&\n> +\t(\n> +\t\tcd repo4 &&\n> +\t\techo content >file &&\n> +\t\tgit -c core.lockfilePid=true add file &&\n> +\t\t# After successful add, no lock or PID files should exist\n> +\t\ttest_path_is_missing .git/index.lock &&\n> +\t\ttest_path_is_missing .git/index~pid.lock\n> +\t)\n> +'\n> +\n> +test_expect_success 'no PID file created by default' '\n> +\tgit init repo5 &&\n> +\t(\n> +\t\tcd repo5 &&\n> +\t\techo content >file &&\n> +\t\tgit add file &&\n> +\t\t# PID file should not be created when feature is disabled\n> +\t\ttest_path_is_missing .git/index~pid.lock\n> +\t)\n> +'\n> +\n> +test_expect_success 'core.lockfilePid=false does not create PID file' '\n> +\tgit init repo6 &&\n> +\t(\n> +\t\tcd repo6 &&\n> +\t\techo content >file &&\n> +\t\tgit -c core.lockfilePid=false add file &&\n> +\t\t# PID file should not be created when feature is disabled\n> +\t\ttest_path_is_missing .git/index~pid.lock\n> +\t)\n> +'\n> +\n> +test_expect_success 'existing PID files are read even when feature disabled' '\n> +\tgit init repo7 &&\n> +\t(\n> +\t\tcd repo7 &&\n> +\t\ttouch .git/index.lock &&\n> +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n> +\t\t# Even with lockfilePid disabled, existing PID files are read\n> +\t\t# to help diagnose stale locks\n> +\t\ttest_must_fail git add . 2>err &&\n> +\t\ttest_grep \"process 99999\" err\n> +\t)\n> +'\n> +\n> +test_done\n>\n> base-commit: 66ce5f8e8872f0183bb137911c52b07f1f242d13\n"},{"id":"533283","messageId":"CALnO6CAx+7r9tj8irLPCxCVQXq0BJcjac9XCD1zv2MBKTm_NgQ@mail.gmail.com","threadId":"64568","inReplyTo":"xmqqbjj4hnkr.fsf@gitster.g","subject":"Re: [PATCH v4] lockfile: add PID file for debugging stale locks","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-08T14:19:36Z","receivedAt":"2026-01-08T14:19:49Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Jan 7, 2026 at 8:59 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> > The PID file is created when a lock is acquired (if enabled), and\n> > automatically cleaned up when the lock is released (via commit or\n> > rollback). The file is registered as a tempfile so it gets cleaned up\n> > by signal and atexit handlers if the process terminates abnormally.\n> >\n> > When a lock conflict occurs, the code checks for an existing PID file\n> > and, if found, uses kill(pid, 0) to determine if the process is still\n> > running. This allows providing context-aware error messages:\n> >\n> >   Lock is held by process 12345. Wait for it to finish, or remove\n> >   the lock file to continue.\n> >\n> > Or for a stale lock:\n> >\n> >   Lock was held by process 12345, which is no longer running.\n> >   Remove the stale lock file to continue.\n>\n> Two additional thoughts.\n>\n>  * 12345 may be running as your kill(12345,0) did not fail, but that\n>    may be a process different from the one that held the lock and\n>    then died without cleaning up.\n\n[snip]\n\n> > The feature is controlled via core.lockfilePid configuration (boolean).\n> > Defaults to false. When enabled, PID files are created for all lock\n> > operations.\n> >\n> > Existing PID files are always read when displaying lock errors,\n> > regardless of the core.lockfilePid setting. This ensures helpful\n> > diagnostics even when the feature was previously enabled and later\n> > disabled.\n>\n> This makes it even more easily triggerable to notice a running\n> process that happens to be reusing a process ID an old process used\n> to have (i.e. the first \"thought\" above---the phrasing of the\n> message needs to be carefully thought out).\n\nYep. I mentioned this back in\n<CALnO6CB1igUL7nv6ByUmwMRc9tqEvs=18wD81GNpaA=FLpL2vw@mail.gmail.com>:\n\n> just because a process X is running doesn't mean\n> it was the one holding the lock, right? Since PIDs can be reused.\n\nwhich seems to have been ignored, but I'm glad to know I didn't make\nthis concern up out of thin air :)\n\nBest,\n-- \nD. Ben Knoble\n"},{"id":"534277","messageId":"pull.2011.v5.git.1768933954845.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":"pull.2011.v4.git.1767804355831.gitgitgadget@gmail.com","subject":"[PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-20T18:32:34Z","receivedAt":"2026-01-20T18:32:37Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion PID file\nalongside each lock file, containing the PID of the lock holder.\n\nFor a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\ntilde character is forbidden in refnames and allowed in Windows\nfilenames, which guarantees no collision with the refs namespace\n(e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file contains\na single line in the format \"pid <value>\" followed by a newline.\n\nThe PID file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks for an existing PID file\nand, if found, uses kill(pid, 0) to determine if the process is still\nrunning. This allows providing context-aware error messages:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nThe feature is controlled via core.lockfilePid configuration (boolean).\nDefaults to false. When enabled, PID files are created for all lock\noperations.\n\nExisting PID files are always read when displaying lock errors,\nregardless of the core.lockfilePid setting. This ensures helpful\ndiagnostics even when the feature was previously enabled and later\ndisabled.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n    \n    Changes in v5:\n    \n     * Addressed PID reuse concern raised by Junio and Ben: the error\n       messages now acknowledge that a running process with the recorded PID\n       may not be the original lock holder. Messages changed from definitive\n       (\"Lock is held by process X\") to cautious (\"Lock may be held by\n       process X; if no git process is running, the lock file may be stale\n       (PIDs can be reused)\").\n     * Removed the \"remove the lock file to continue\" advice from all\n       messages, as this is too prescriptive about what action to take.\n     * Updated tests to match the new message wording.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v5\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nRange-diff vs v4:\n\n 1:  c26bc77e30 ! 1:  5260bf2d40 lockfile: add PID file for debugging stale locks\n     @@ lockfile.c: static int lock_file_timeout(struct lock_file *lk, const char *path,\n      +\t\t}\n      +\n      +\t\tif (pid_status == 1)\n     -+\t\t\tstrbuf_addf(buf, _(\"Lock is held by process %\" PRIuMAX \". \"\n     -+\t\t\t\t\t   \"Wait for it to finish, or remove the lock file to continue\"),\n     ++\t\t\tstrbuf_addf(buf, _(\"Lock may be held by process %\" PRIuMAX \"; \"\n     ++\t\t\t\t\t   \"if no git process is running, the lock file \"\n     ++\t\t\t\t\t   \"may be stale (PIDs can be reused)\"),\n      +\t\t\t\t    pid);\n      +\t\telse if (pid_status == -1)\n      +\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n     -+\t\t\t\t\t   \"which is no longer running. Remove the stale lock file to continue\"),\n     ++\t\t\t\t\t   \"which is no longer running; the lock file \"\n     ++\t\t\t\t\t   \"appears to be stale\"),\n      +\t\t\t\t    pid);\n      +\t\telse\n     -+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository. \"\n     -+\t\t\t\t\t     \"Wait for it to finish, or remove the lock file to continue\"));\n     ++\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository, \"\n     ++\t\t\t\t\t     \"or the lock file may be stale\"));\n      +\n      +\t\tstrbuf_release(&lock_path);\n      +\t\tstrbuf_release(&pid_path);\n     @@ t/t0031-lockfile-pid.sh (new)\n      +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n      +\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n      +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n     -+\t\ttest_grep \"Remove the stale lock file\" err\n     ++\t\ttest_grep \"appears to be stale\" err\n      +\t)\n      +'\n      +\n\n\n Documentation/config/core.adoc |  11 +++\n builtin/commit.c               |   3 +-\n builtin/gc.c                   |   6 +-\n compat/mingw.c                 |  10 ++\n environment.c                  |   6 ++\n lockfile.c                     | 172 ++++++++++++++++++++++++++++++---\n lockfile.h                     |  43 ++++++---\n t/meson.build                  |   1 +\n t/t0031-lockfile-pid.sh        | 105 ++++++++++++++++++++\n 9 files changed, 322 insertions(+), 35 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex 01202da7cd..5c4bc9206f 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a\n read-only snapshot of the same index to a location different from the\n repository's usual working tree).\n \n+core.lockfilePid::\n+\tIf true, Git will create a PID file alongside lock files. When a\n+\tlock acquisition fails and a PID file exists, Git can provide\n+\tadditional diagnostic information about the process holding the\n+\tlock, including whether it is still running. Defaults to `false`.\n++\n+The PID file is named by inserting `~pid` before the `.lock` suffix.\n+For example, if the lock file is `index.lock`, the PID file will be\n+`index~pid.lock`. The file contains a single line in the format\n+`pid <value>` followed by a newline.\n+\n core.logAllRefUpdates::\n \tEnable the reflog. Updates to a ref <ref> is logged to the file\n \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 0243f17d53..4378256fa5 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -539,8 +539,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \n \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n \t\t\t     (uintmax_t) getpid());\n-\thold_lock_file_for_update(&false_lock, path,\n-\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n \n \tcreate_base_index(current_head);\n \tadd_remove_files(&partial);\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 92c6e7b954..1dcc8dd550 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -748,8 +748,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n \n \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n-\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n-\t\t\t\t       LOCK_DIE_ON_ERROR);\n+\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n \tif (!force) {\n \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n \t\tstatic char *scan_fmt;\n@@ -1016,8 +1015,7 @@ int cmd_gc(int argc,\n \n \tif (daemonized) {\n \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n-\t\thold_lock_file_for_update(&log_lock, path,\n-\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n \t\tdup2(get_lock_file_fd(&log_lock), 2);\n \t\tatexit(process_log_file_at_exit);\n \t\tfree(path);\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 939f938fe2..146b2585ce 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)\n \t\t\tCloseHandle(h);\n \t\t\treturn 0;\n \t\t}\n+\t\t/*\n+\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n+\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n+\t\t * compatibility with kill(pid, 0).\n+\t\t */\n+\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n+\t\t\terrno = ESRCH;\n+\t\telse\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n \t}\n \n \terrno = EINVAL;\ndiff --git a/environment.c b/environment.c\nindex a770b5921d..4adcce8606 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -21,6 +21,7 @@\n #include \"gettext.h\"\n #include \"git-zlib.h\"\n #include \"ident.h\"\n+#include \"lockfile.h\"\n #include \"mailmap.h\"\n #include \"object-name.h\"\n #include \"repository.h\"\n@@ -532,6 +533,11 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"core.lockfilepid\")) {\n+\t\tlockfile_pid_enabled = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"core.createobject\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..6d03c60d50 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,19 +74,119 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a foo~pid.lock file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ *\n+ * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n+ * forbidden in refnames and allowed in Windows filenames, guaranteeing\n+ * no collision with the refs namespace.\n+ */\n+\n+/* Global config variable, initialized from core.lockfilePid */\n+int lockfile_pid_enabled;\n+\n+/*\n+ * Path generation helpers.\n+ * Given base path \"foo\", generate:\n+ *   - lock path: \"foo.lock\"\n+ *   - pid path:  \"foo-pid.lock\"\n+ */\n+static void get_lock_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static void get_pid_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_PID_INFIX);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd = -1;\n+\n+\tif (!lockfile_pid_enabled)\n+\t\tgoto out;\n+\n+\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n+\tif (fd < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n+\tif (write_in_full(fd, content.buf, content.len) < 0) {\n+\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n+\t\tclose(fd);\n+\t\tfd = -1;\n+\t\tunlink(pid_path);\n+\t\tgoto out;\n+\t}\n+\n+\tclose(fd);\n+\tfd = -1;\n+\tpid_tempfile = register_tempfile(pid_path);\n+\n+out:\n+\tif (fd >= 0)\n+\t\tclose(fd);\n+\tstrbuf_release(&content);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tconst char *val;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n+\t\tgoto out;\n+\n+\tstrbuf_rtrim(&content);\n+\n+\tif (skip_prefix(content.buf, \"pid \", &val)) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(val, &endptr, 10);\n+\t\tif (*pid_out > 0 && !*endptr)\n+\t\t\tret = 0;\n+\t}\n+\n+\tif (ret)\n+\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n \t\t     int mode)\n {\n-\tstruct strbuf filename = STRBUF_INIT;\n+\tstruct strbuf base_path = STRBUF_INIT;\n+\tstruct strbuf lock_path = STRBUF_INIT;\n+\tstruct strbuf pid_path = STRBUF_INIT;\n \n-\tstrbuf_addstr(&filename, path);\n+\tstrbuf_addstr(&base_path, path);\n \tif (!(flags & LOCK_NO_DEREF))\n-\t\tresolve_symlink(&filename);\n+\t\tresolve_symlink(&base_path);\n+\n+\tget_lock_path(&lock_path, base_path.buf);\n+\tget_pid_path(&pid_path, base_path.buf);\n+\n+\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);\n \n-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n-\tstrbuf_release(&filename);\n+\tstrbuf_release(&base_path);\n+\tstrbuf_release(&lock_path);\n+\tstrbuf_release(&pid_path);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n \n@@ -151,16 +254,49 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n-\t} else\n+\t\tconst char *abs_path = absolute_path(path);\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tstruct strbuf pid_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tget_lock_path(&lock_path, abs_path);\n+\t\tget_pid_path(&pid_path, abs_path);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\t/*\n+\t\t * Try to read PID file unconditionally - it may exist if\n+\t\t * core.lockfilePid was enabled.\n+\t\t */\n+\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n+\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n+\t\t\telse if (errno == ESRCH)\n+\t\t\t\tpid_status = -1; /* no such process - stale lock */\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock may be held by process %\" PRIuMAX \"; \"\n+\t\t\t\t\t   \"if no git process is running, the lock file \"\n+\t\t\t\t\t   \"may be stale (PIDs can be reused)\"),\n+\t\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n+\t\t\t\t\t   \"which is no longer running; the lock file \"\n+\t\t\t\t\t   \"appears to be stale\"),\n+\t\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository, \"\n+\t\t\t\t\t     \"or the lock file may be stale\"));\n+\n+\t\tstrbuf_release(&lock_path);\n+\t\tstrbuf_release(&pid_path);\n+\t} else {\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n+\t}\n }\n \n NORETURN void unable_to_lock_die(const char *path, int err)\n@@ -207,6 +343,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +354,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..e7233f28de 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,22 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/*\n+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n+ * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n+ * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n+ */\n+#define LOCK_PID_INFIX \"~pid\"\n+#define LOCK_PID_INFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/*\n+ * Whether to create PID files alongside lock files.\n+ * Configured via core.lockfilePid (boolean).\n+ */\n+extern int lockfile_pid_enabled;\n \n /*\n  * Flags\n@@ -169,12 +186,12 @@ struct lock_file {\n  * handling, and mode are described above.\n  */\n int hold_lock_file_for_update_timeout_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms, int mode);\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms, int mode);\n \n static inline int hold_lock_file_for_update_timeout(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n \t\t\t\t\t\t      timeout_ms, 0666);\n@@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(\n  * argument and error handling are described above.\n  */\n static inline int hold_lock_file_for_update(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags)\n+\tstruct lock_file *lk, const char *path, int flags)\n {\n \treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n }\n \n static inline int hold_lock_file_for_update_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, int mode)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, int mode)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n }\n@@ -319,13 +335,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..2aec2c011e 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..8ef87addf5\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,105 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via core.lockfilePid config setting (boolean).\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"appears to be stale\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Get the correct PID for this platform\n+\t\tshell_pid=$$ &&\n+\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n+\t\tthen\n+\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n+\t\t\t# uses Windows PIDs. Use the Windows PID.\n+\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n+\t\tfi &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail git -c core.lockfilePid=true add file 2>err &&\n+\t\ttest_grep \"held by process $shell_pid\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=true add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\ttest_path_is_missing .git/index.lock &&\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=false does not create PID file' '\n+\tgit init repo6 &&\n+\t(\n+\t\tcd repo6 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=false add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'existing PID files are read even when feature disabled' '\n+\tgit init repo7 &&\n+\t(\n+\t\tcd repo7 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\t# Even with lockfilePid disabled, existing PID files are read\n+\t\t# to help diagnose stale locks\n+\t\ttest_must_fail git add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_done\n\nbase-commit: 66ce5f8e8872f0183bb137911c52b07f1f242d13\n-- \ngitgitgadget\n"},{"id":"534283","messageId":"xmqqjyxcyrvm.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v5.git.1768933954845.gitgitgadget@gmail.com","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-20T20:02:37Z","receivedAt":"2026-01-20T20:02:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Existing PID files are always read when displaying lock errors,\n> regardless of the core.lockfilePid setting. This ensures helpful\n> diagnostics even when the feature was previously enabled and later\n> disabled.\n\n> +core.lockfilePid::\n> +\tIf true, Git will create a PID file alongside lock files. When a\n\nHmph, are there cases where a single process grab two more more lock\nfiles and a single PID file helps identify who holds these multiple\nlock files?  I somehow had an impression that with the configuration\non, we create an extra PID file for each lock file we create.\n\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 0243f17d53..4378256fa5 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -539,8 +539,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n>  \n>  \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n>  \t\t\t     (uintmax_t) getpid());\n> -\thold_lock_file_for_update(&false_lock, path,\n> -\t\t\t\t  LOCK_DIE_ON_ERROR);\n> +\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n\nA change that is not necessary/essential for the purpose of the\ntopic?\n\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 92c6e7b954..1dcc8dd550 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -748,8 +748,7 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n>  \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n>  \n>  \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n> -\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n> -\t\t\t\t       LOCK_DIE_ON_ERROR);\n> +\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n\nLikewise?\n\n> @@ -1016,8 +1015,7 @@ int cmd_gc(int argc,\n>  \n>  \tif (daemonized) {\n>  \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n> -\t\thold_lock_file_for_update(&log_lock, path,\n> -\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n> +\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n\nLikewise?\n\n> +static void get_pid_path(struct strbuf *out, const char *path)\n> +{\n> +\tstrbuf_addstr(out, path);\n> +\tstrbuf_addstr(out, LOCK_PID_INFIX);\n> +\tstrbuf_addstr(out, LOCK_SUFFIX);\n> +}\n\nIf we get rid of LOCK_PID_INFIX, and instead did\n\n    #define PID_LOCK_SUFFIX \"~pid.lock\"\n\nwouldn't it make the overall patch simpler?  I do not see any code\nthat uses LOCK_PID_INFIX independently (other than the above code,\nobviously).  Then get_pid_path() would become\n\n        static void get_pid_path(struct strbuf *out, const char *path)\n        {\n                strbuf_addstr(out, path);\n                strbuf_addstr(out, PID_LOCK_SUFFIX);\n        }\n\nWhile at it, we can also lose LOCK_PID_INFIX_LEN that nobody uses ...\n\n> @@ -127,6 +128,22 @@ struct lock_file {\n>  #define LOCK_SUFFIX \".lock\"\n>  #define LOCK_SUFFIX_LEN 5\n>  \n> +/*\n> + * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n> + * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n> + * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n> + */\n> +#define LOCK_PID_INFIX \"~pid\"\n> +#define LOCK_PID_INFIX_LEN 4\n\n... from here.\n\n> +\n> +/* Maximum length for PID file content */\n> +#define LOCK_PID_MAXLEN 32\n\nThis is used as the last parameter to strbuf_read_file(), which does\nnot mean we stop reading after reaching this length at all, so the\nname of this constant is very misleading.  We only read the file in\nthe error code path so there isn't much point in trying to optimize\nreading from these files too heavily.  I'd suggest getting rid of\nthis constant, and passing 0 to the function instead to signal \"we\nare willing to take the default given by the strbuf subsystem\".\n\n"},{"id":"534313","messageId":"20260121071344.GA570838@coredump.intra.peff.net","threadId":"64568","inReplyTo":"pull.2011.v5.git.1768933954845.gitgitgadget@gmail.com","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-21T07:13:44Z","receivedAt":"2026-01-21T07:13:50Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Tue, Jan 20, 2026 at 06:32:34PM +0000, Paulo Casaretto via GitGitGadget wrote:\n\n> +static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n> +{\n> +\tstruct strbuf content = STRBUF_INIT;\n> +\tstruct tempfile *pid_tempfile = NULL;\n> +\tint fd = -1;\n> +\n> +\tif (!lockfile_pid_enabled)\n> +\t\tgoto out;\n> +\n> +\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n> +\tif (fd < 0)\n> +\t\tgoto out;\n> +\n> +\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n> +\tif (write_in_full(fd, content.buf, content.len) < 0) {\n> +\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> +\t\tclose(fd);\n> +\t\tfd = -1;\n> +\t\tunlink(pid_path);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tclose(fd);\n> +\tfd = -1;\n> +\tpid_tempfile = register_tempfile(pid_path);\n> +\n> +out:\n> +\tif (fd >= 0)\n> +\t\tclose(fd);\n> +\tstrbuf_release(&content);\n> +\treturn pid_tempfile;\n> +}\n\nCoverity complains that the close(fd) call in the \"out\" label is\nunreachable, and I think it is right. When we jump from before the\nopen(), or if the open failed, then fd is negative (and thus no close).\nIf we get there when write_in_full() fails, then we close ourselves in\nthe conditional. And if we succeed, then we close the descriptor before\nregistering the tempfile.\n\nI don't think it's wrong, but the cleanup is redundant between the \"out\"\npath and the others.\n\nDid you mean this:\n\ndiff --git a/lockfile.c b/lockfile.c\nindex 731cdd4944..e5d6ae0df6 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -122,14 +122,10 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n \tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n \tif (write_in_full(fd, content.buf, content.len) < 0) {\n \t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n-\t\tclose(fd);\n-\t\tfd = -1;\n \t\tunlink(pid_path);\n \t\tgoto out;\n \t}\n \n-\tclose(fd);\n-\tfd = -1;\n \tpid_tempfile = register_tempfile(pid_path);\n \n out:\n\nwhich would just let the close after the out label handle all cases?\n\n-Peff\n"},{"id":"534319","messageId":"CAPig+cSE7Y-MLu1PTdo2kUq_MztMQgm0eYby03cX2K5YAJLwsg@mail.gmail.com","threadId":"64568","inReplyTo":"20260121071344.GA570838@coredump.intra.peff.net","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-01-21T08:13:41Z","receivedAt":"2026-01-21T08:13:54Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Wed, Jan 21, 2026 at 2:15 AM Jeff King <peff@peff.net> wrote:\n> I don't think it's wrong, but the cleanup is redundant between the \"out\"\n> path and the others.\n>\n> Did you mean this:\n>\n> diff --git a/lockfile.c b/lockfile.c\n> @@ -122,14 +122,10 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n>         strbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n>         if (write_in_full(fd, content.buf, content.len) < 0) {\n>                 warning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> -               close(fd);\n> -               fd = -1;\n>                 unlink(pid_path);\n>                 goto out;\n>         }\n>\n> -       close(fd);\n> -       fd = -1;\n>         pid_tempfile = register_tempfile(pid_path);\n>\n>  out:\n>\n> which would just let the close after the out label handle all cases?\n\nCorrect me if I'm wrong, but wouldn't this suggested change be\nproblematic on Microsoft Windows? Specifically, if I recall correctly,\nWindows won't allow a file to be deleted if any processes still have\nit open, and this change eliminates the call to close() preceding the\ncall to unlink(), so the file would still be held open when the\nattempt is made to remove it.\n\nIf so, then probably better would be to drop the unreachable `if (fd\n>= 0) close(fd)` after the `out` label.\n"},{"id":"534324","messageId":"bd725e17-b9bf-49d2-8d59-a479e30ceb76@kdbg.org","threadId":"64568","inReplyTo":"CAPig+cSE7Y-MLu1PTdo2kUq_MztMQgm0eYby03cX2K5YAJLwsg@mail.gmail.com","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-01-21T10:14:38Z","receivedAt":"2026-01-21T10:14:49Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 21.01.26 um 09:13 schrieb Eric Sunshine:\n> On Wed, Jan 21, 2026 at 2:15 AM Jeff King <peff@peff.net> wrote:\n>> I don't think it's wrong, but the cleanup is redundant between the \"out\"\n>> path and the others.\n>>\n>> Did you mean this:\n>>\n>> diff --git a/lockfile.c b/lockfile.c\n>> @@ -122,14 +122,10 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n>>         strbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n>>         if (write_in_full(fd, content.buf, content.len) < 0) {\n>>                 warning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n>> -               close(fd);\n>> -               fd = -1;\n>>                 unlink(pid_path);\n>>                 goto out;\n>>         }\n>>\n>> -       close(fd);\n>> -       fd = -1;\n>>         pid_tempfile = register_tempfile(pid_path);\n>>\n>>  out:\n>>\n>> which would just let the close after the out label handle all cases?\n> \n> Correct me if I'm wrong, but wouldn't this suggested change be\n> problematic on Microsoft Windows? Specifically, if I recall correctly,\n> Windows won't allow a file to be deleted if any processes still have\n> it open, and this change eliminates the call to close() preceding the\n> call to unlink(), so the file would still be held open when the\n> attempt is made to remove it.\nYou analysis is correct. I was just about to point this out, too.\n\n-- Hannes\n\n"},{"id":"534360","messageId":"xmqqcy33vsrt.fsf@gitster.g","threadId":"64568","inReplyTo":"20260121071344.GA570838@coredump.intra.peff.net","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-21T16:23:50Z","receivedAt":"2026-01-21T16:23:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> I don't think it's wrong, but the cleanup is redundant between the \"out\"\n> path and the others.\n>\n> Did you mean this:\n>\n> diff --git a/lockfile.c b/lockfile.c\n> index 731cdd4944..e5d6ae0df6 100644\n> --- a/lockfile.c\n> +++ b/lockfile.c\n> @@ -122,14 +122,10 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n>  \tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n>  \tif (write_in_full(fd, content.buf, content.len) < 0) {\n>  \t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> -\t\tclose(fd);\n> -\t\tfd = -1;\n>  \t\tunlink(pid_path);\n>  \t\tgoto out;\n>  \t}\n>  \n> -\tclose(fd);\n> -\tfd = -1;\n>  \tpid_tempfile = register_tempfile(pid_path);\n>  \n>  out:\n>\n> which would just let the close after the out label handle all cases?\n\nI recall suggesting this myself in\n\nhttps://lore.kernel.org/git/xmqqbjj4hnkr.fsf@gitster.g\n\nwithout realizing that this would probably not work on Windows where\nunlink() cannot work correctly until the file descriptor is closed.\n"},{"id":"534364","messageId":"20260121163924.GA576236@coredump.intra.peff.net","threadId":"64568","inReplyTo":"CAPig+cSE7Y-MLu1PTdo2kUq_MztMQgm0eYby03cX2K5YAJLwsg@mail.gmail.com","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-21T16:39:24Z","receivedAt":"2026-01-21T16:39:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 21, 2026 at 03:13:41AM -0500, Eric Sunshine wrote:\n\n> > diff --git a/lockfile.c b/lockfile.c\n> > @@ -122,14 +122,10 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n> >         strbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n> >         if (write_in_full(fd, content.buf, content.len) < 0) {\n> >                 warning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n> > -               close(fd);\n> > -               fd = -1;\n> >                 unlink(pid_path);\n> >                 goto out;\n> >         }\n> >\n> > -       close(fd);\n> > -       fd = -1;\n> >         pid_tempfile = register_tempfile(pid_path);\n> >\n> >  out:\n> >\n> > which would just let the close after the out label handle all cases?\n> \n> Correct me if I'm wrong, but wouldn't this suggested change be\n> problematic on Microsoft Windows? Specifically, if I recall correctly,\n> Windows won't allow a file to be deleted if any processes still have\n> it open, and this change eliminates the call to close() preceding the\n> call to unlink(), so the file would still be held open when the\n> attempt is made to remove it.\n> \n> If so, then probably better would be to drop the unreachable `if (fd\n> >= 0) close(fd)` after the `out` label.\n\nAh, yeah, you're right. Ironically I spent quite a while thinking on the\nimplications of calling close() before register_tempfile() and decided\nit didn't matter, but totally ignored the first half of the hunk. ;)\n\nThe second half is still valid, I think, but at that point it is the\nonly path that uses the close() in the out-path, so we might as well\ndrop the out-path one.\n\n-Peff\n"},{"id":"534375","messageId":"xmqqcy32u769.fsf@gitster.g","threadId":"64568","inReplyTo":"20260121163924.GA576236@coredump.intra.peff.net","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-21T18:55:42Z","receivedAt":"2026-01-21T18:55:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> The second half is still valid, I think, but at that point it is the\n> only path that uses the close() in the out-path, so we might as well\n> drop the out-path one.\n\nTrue.  A fix-up may look like this.  I've got rid of the assignments\nto \"fd\" that are not used.\n\nThe changes to first two files simply revert unnecessary changes.\n\n builtin/commit.c | 3 ++-\n builtin/gc.c     | 6 ++++--\n lockfile.c       | 6 +-----\n 3 files changed, 7 insertions(+), 8 deletions(-)\n\ndiff --git c/builtin/commit.c w/builtin/commit.c\nindex 4378256fa5..0243f17d53 100644\n--- c/builtin/commit.c\n+++ w/builtin/commit.c\n@@ -539,7 +539,8 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \n \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n \t\t\t     (uintmax_t) getpid());\n-\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n+\thold_lock_file_for_update(&false_lock, path,\n+\t\t\t\t  LOCK_DIE_ON_ERROR);\n \n \tcreate_base_index(current_head);\n \tadd_remove_files(&partial);\ndiff --git c/builtin/gc.c w/builtin/gc.c\nindex 1dcc8dd550..92c6e7b954 100644\n--- c/builtin/gc.c\n+++ w/builtin/gc.c\n@@ -748,7 +748,8 @@ static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n \n \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n-\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n+\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n+\t\t\t\t       LOCK_DIE_ON_ERROR);\n \tif (!force) {\n \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n \t\tstatic char *scan_fmt;\n@@ -1015,7 +1016,8 @@ int cmd_gc(int argc,\n \n \tif (daemonized) {\n \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n-\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n+\t\thold_lock_file_for_update(&log_lock, path,\n+\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n \t\tdup2(get_lock_file_fd(&log_lock), 2);\n \t\tatexit(process_log_file_at_exit);\n \t\tfree(path);\ndiff --git c/lockfile.c w/lockfile.c\nindex 6d03c60d50..13e2ad1307 100644\n--- c/lockfile.c\n+++ w/lockfile.c\n@@ -110,7 +110,7 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n {\n \tstruct strbuf content = STRBUF_INIT;\n \tstruct tempfile *pid_tempfile = NULL;\n-\tint fd = -1;\n+\tint fd;\n \n \tif (!lockfile_pid_enabled)\n \t\tgoto out;\n@@ -123,18 +123,14 @@ static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n \tif (write_in_full(fd, content.buf, content.len) < 0) {\n \t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n \t\tclose(fd);\n-\t\tfd = -1;\n \t\tunlink(pid_path);\n \t\tgoto out;\n \t}\n \n \tclose(fd);\n-\tfd = -1;\n \tpid_tempfile = register_tempfile(pid_path);\n \n out:\n-\tif (fd >= 0)\n-\t\tclose(fd);\n \tstrbuf_release(&content);\n \treturn pid_tempfile;\n }\n"},{"id":"534378","messageId":"20260121195329.GA584009@coredump.intra.peff.net","threadId":"64568","inReplyTo":"xmqqcy32u769.fsf@gitster.g","subject":"Re: [PATCH v5] lockfile: add PID file for debugging stale locks","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-01-21T19:53:29Z","receivedAt":"2026-01-21T19:53:36Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jan 21, 2026 at 10:55:42AM -0800, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > The second half is still valid, I think, but at that point it is the\n> > only path that uses the close() in the out-path, so we might as well\n> > drop the out-path one.\n> \n> True.  A fix-up may look like this.  I've got rid of the assignments\n> to \"fd\" that are not used.\n\nYup, that all looks correct to me.\n\n-Peff\n"},{"id":"534489","messageId":"pull.2011.v6.git.1769109815197.gitgitgadget@gmail.com","threadId":"64568","inReplyTo":"pull.2011.v5.git.1768933954845.gitgitgadget@gmail.com","subject":"[PATCH v6] lockfile: add PID file for debugging stale locks","fromName":"Paulo Casaretto via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-01-22T19:23:35Z","receivedAt":"2026-01-22T19:23:42Z","isPatch":true,"sender":{"key":"name:Paulo Casaretto","avatar":null},"body":"From: Paulo Casaretto <pcasaretto@gmail.com>\n\nWhen a lock file is held, it can be helpful to know which process owns\nit, especially when debugging stale locks left behind by crashed\nprocesses. Add an optional feature that creates a companion PID file\nalongside each lock file, containing the PID of the lock holder.\n\nFor a lock file \"foo.lock\", the PID file is named \"foo~pid.lock\". The\ntilde character is forbidden in refnames and allowed in Windows\nfilenames, which guarantees no collision with the refs namespace\n(e.g., refs \"foo\" and \"foo~pid\" cannot both exist). The file contains\na single line in the format \"pid <value>\" followed by a newline.\n\nThe PID file is created when a lock is acquired (if enabled), and\nautomatically cleaned up when the lock is released (via commit or\nrollback). The file is registered as a tempfile so it gets cleaned up\nby signal and atexit handlers if the process terminates abnormally.\n\nWhen a lock conflict occurs, the code checks for an existing PID file\nand, if found, uses kill(pid, 0) to determine if the process is still\nrunning. This allows providing context-aware error messages:\n\n  Lock is held by process 12345. Wait for it to finish, or remove\n  the lock file to continue.\n\nOr for a stale lock:\n\n  Lock was held by process 12345, which is no longer running.\n  Remove the stale lock file to continue.\n\nThe feature is controlled via core.lockfilePid configuration (boolean).\nDefaults to false. When enabled, PID files are created for all lock\noperations.\n\nExisting PID files are always read when displaying lock errors,\nregardless of the core.lockfilePid setting. This ensures helpful\ndiagnostics even when the feature was previously enabled and later\ndisabled.\n\nSigned-off-by: Paulo Casaretto <pcasaretto@gmail.com>\n---\n    lockfile: add holder info file for debugging stale locks\n    \n    Changes in v6:\n    \n     * Applied Junio's fix-up: removed unused fd = -1 assignments and the\n       unreachable if (fd >= 0) close(fd) cleanup at the out label in\n       create_lock_pid_file().\n     * Kept close(fd) before unlink() in the error path for Windows\n       compatibility, as noted by Eric Sunshine and Johannes Sixt.\n     * Reverted the unnecessary whitespace-only changes to builtin/commit.c\n       and builtin/gc.c - these files are no longer touched by this patch.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v6\nPull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nRange-diff vs v5:\n\n 1:  5260bf2d40 ! 1:  f9ba2c20ec lockfile: add PID file for debugging stale locks\n     @@ Documentation/config/core.adoc: confusion unless you know what you are doing (e.\n       \tEnable the reflog. Updates to a ref <ref> is logged to the file\n       \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\n      \n     - ## builtin/commit.c ##\n     -@@ builtin/commit.c: static const char *prepare_index(const char **argv, const char *prefix,\n     - \n     - \tpath = repo_git_path(the_repository, \"next-index-%\"PRIuMAX,\n     - \t\t\t     (uintmax_t) getpid());\n     --\thold_lock_file_for_update(&false_lock, path,\n     --\t\t\t\t  LOCK_DIE_ON_ERROR);\n     -+\thold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);\n     - \n     - \tcreate_base_index(current_head);\n     - \tadd_remove_files(&partial);\n     -\n     - ## builtin/gc.c ##\n     -@@ builtin/gc.c: static const char *lock_repo_for_gc(int force, pid_t* ret_pid)\n     - \t\txsnprintf(my_host, sizeof(my_host), \"unknown\");\n     - \n     - \tpidfile_path = repo_git_path(the_repository, \"gc.pid\");\n     --\tfd = hold_lock_file_for_update(&lock, pidfile_path,\n     --\t\t\t\t       LOCK_DIE_ON_ERROR);\n     -+\tfd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);\n     - \tif (!force) {\n     - \t\tstatic char locking_host[HOST_NAME_MAX + 1];\n     - \t\tstatic char *scan_fmt;\n     -@@ builtin/gc.c: int cmd_gc(int argc,\n     - \n     - \tif (daemonized) {\n     - \t\tchar *path = repo_git_path(the_repository, \"gc.log\");\n     --\t\thold_lock_file_for_update(&log_lock, path,\n     --\t\t\t\t\t  LOCK_DIE_ON_ERROR);\n     -+\t\thold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);\n     - \t\tdup2(get_lock_file_fd(&log_lock), 2);\n     - \t\tatexit(process_log_file_at_exit);\n     - \t\tfree(path);\n     -\n       ## compat/mingw.c ##\n      @@ compat/mingw.c: int mingw_kill(pid_t pid, int sig)\n       \t\t\tCloseHandle(h);\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +{\n      +\tstruct strbuf content = STRBUF_INIT;\n      +\tstruct tempfile *pid_tempfile = NULL;\n     -+\tint fd = -1;\n     ++\tint fd;\n      +\n      +\tif (!lockfile_pid_enabled)\n      +\t\tgoto out;\n     @@ lockfile.c: static void resolve_symlink(struct strbuf *path)\n      +\tif (write_in_full(fd, content.buf, content.len) < 0) {\n      +\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n      +\t\tclose(fd);\n     -+\t\tfd = -1;\n      +\t\tunlink(pid_path);\n      +\t\tgoto out;\n      +\t}\n      +\n      +\tclose(fd);\n     -+\tfd = -1;\n      +\tpid_tempfile = register_tempfile(pid_path);\n      +\n      +out:\n     -+\tif (fd >= 0)\n     -+\t\tclose(fd);\n      +\tstrbuf_release(&content);\n      +\treturn pid_tempfile;\n      +}\n\n\n Documentation/config/core.adoc |  11 +++\n compat/mingw.c                 |  10 ++\n environment.c                  |   6 ++\n lockfile.c                     | 168 ++++++++++++++++++++++++++++++---\n lockfile.h                     |  43 ++++++---\n t/meson.build                  |   1 +\n t/t0031-lockfile-pid.sh        | 105 +++++++++++++++++++++\n 7 files changed, 315 insertions(+), 29 deletions(-)\n create mode 100755 t/t0031-lockfile-pid.sh\n\ndiff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc\nindex 01202da7cd..5c4bc9206f 100644\n--- a/Documentation/config/core.adoc\n+++ b/Documentation/config/core.adoc\n@@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a\n read-only snapshot of the same index to a location different from the\n repository's usual working tree).\n \n+core.lockfilePid::\n+\tIf true, Git will create a PID file alongside lock files. When a\n+\tlock acquisition fails and a PID file exists, Git can provide\n+\tadditional diagnostic information about the process holding the\n+\tlock, including whether it is still running. Defaults to `false`.\n++\n+The PID file is named by inserting `~pid` before the `.lock` suffix.\n+For example, if the lock file is `index.lock`, the PID file will be\n+`index~pid.lock`. The file contains a single line in the format\n+`pid <value>` followed by a newline.\n+\n core.logAllRefUpdates::\n \tEnable the reflog. Updates to a ref <ref> is logged to the file\n \t\"`$GIT_DIR/logs/<ref>`\", by appending the new and old\ndiff --git a/compat/mingw.c b/compat/mingw.c\nindex 939f938fe2..146b2585ce 100644\n--- a/compat/mingw.c\n+++ b/compat/mingw.c\n@@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)\n \t\t\tCloseHandle(h);\n \t\t\treturn 0;\n \t\t}\n+\t\t/*\n+\t\t * OpenProcess returns ERROR_INVALID_PARAMETER for\n+\t\t * non-existent PIDs. Map this to ESRCH for POSIX\n+\t\t * compatibility with kill(pid, 0).\n+\t\t */\n+\t\tif (GetLastError() == ERROR_INVALID_PARAMETER)\n+\t\t\terrno = ESRCH;\n+\t\telse\n+\t\t\terrno = err_win_to_posix(GetLastError());\n+\t\treturn -1;\n \t}\n \n \terrno = EINVAL;\ndiff --git a/environment.c b/environment.c\nindex a770b5921d..4adcce8606 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -21,6 +21,7 @@\n #include \"gettext.h\"\n #include \"git-zlib.h\"\n #include \"ident.h\"\n+#include \"lockfile.h\"\n #include \"mailmap.h\"\n #include \"object-name.h\"\n #include \"repository.h\"\n@@ -532,6 +533,11 @@ static int git_default_core_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"core.lockfilepid\")) {\n+\t\tlockfile_pid_enabled = git_config_bool(var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"core.createobject\")) {\n \t\tif (!value)\n \t\t\treturn config_error_nonbool(var);\ndiff --git a/lockfile.c b/lockfile.c\nindex 1d5ed01682..13e2ad1307 100644\n--- a/lockfile.c\n+++ b/lockfile.c\n@@ -6,6 +6,9 @@\n #include \"abspath.h\"\n #include \"gettext.h\"\n #include \"lockfile.h\"\n+#include \"parse.h\"\n+#include \"strbuf.h\"\n+#include \"wrapper.h\"\n \n /*\n  * path = absolute or relative path name\n@@ -71,19 +74,115 @@ static void resolve_symlink(struct strbuf *path)\n \tstrbuf_reset(&link);\n }\n \n+/*\n+ * Lock PID file functions - write PID to a foo~pid.lock file alongside\n+ * the lock file for debugging stale locks. The PID file is registered\n+ * as a tempfile so it gets cleaned up by signal/atexit handlers.\n+ *\n+ * Naming: For \"foo.lock\", the PID file is \"foo~pid.lock\". The tilde is\n+ * forbidden in refnames and allowed in Windows filenames, guaranteeing\n+ * no collision with the refs namespace.\n+ */\n+\n+/* Global config variable, initialized from core.lockfilePid */\n+int lockfile_pid_enabled;\n+\n+/*\n+ * Path generation helpers.\n+ * Given base path \"foo\", generate:\n+ *   - lock path: \"foo.lock\"\n+ *   - pid path:  \"foo-pid.lock\"\n+ */\n+static void get_lock_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static void get_pid_path(struct strbuf *out, const char *path)\n+{\n+\tstrbuf_addstr(out, path);\n+\tstrbuf_addstr(out, LOCK_PID_INFIX);\n+\tstrbuf_addstr(out, LOCK_SUFFIX);\n+}\n+\n+static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tstruct tempfile *pid_tempfile = NULL;\n+\tint fd;\n+\n+\tif (!lockfile_pid_enabled)\n+\t\tgoto out;\n+\n+\tfd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);\n+\tif (fd < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&content, \"pid %\" PRIuMAX \"\\n\", (uintmax_t)getpid());\n+\tif (write_in_full(fd, content.buf, content.len) < 0) {\n+\t\twarning_errno(_(\"could not write lock pid file '%s'\"), pid_path);\n+\t\tclose(fd);\n+\t\tunlink(pid_path);\n+\t\tgoto out;\n+\t}\n+\n+\tclose(fd);\n+\tpid_tempfile = register_tempfile(pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn pid_tempfile;\n+}\n+\n+static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)\n+{\n+\tstruct strbuf content = STRBUF_INIT;\n+\tconst char *val;\n+\tint ret = -1;\n+\n+\tif (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)\n+\t\tgoto out;\n+\n+\tstrbuf_rtrim(&content);\n+\n+\tif (skip_prefix(content.buf, \"pid \", &val)) {\n+\t\tchar *endptr;\n+\t\t*pid_out = strtoumax(val, &endptr, 10);\n+\t\tif (*pid_out > 0 && !*endptr)\n+\t\t\tret = 0;\n+\t}\n+\n+\tif (ret)\n+\t\twarning(_(\"malformed lock pid file '%s'\"), pid_path);\n+\n+out:\n+\tstrbuf_release(&content);\n+\treturn ret;\n+}\n+\n /* Make sure errno contains a meaningful value on error */\n static int lock_file(struct lock_file *lk, const char *path, int flags,\n \t\t     int mode)\n {\n-\tstruct strbuf filename = STRBUF_INIT;\n+\tstruct strbuf base_path = STRBUF_INIT;\n+\tstruct strbuf lock_path = STRBUF_INIT;\n+\tstruct strbuf pid_path = STRBUF_INIT;\n \n-\tstrbuf_addstr(&filename, path);\n+\tstrbuf_addstr(&base_path, path);\n \tif (!(flags & LOCK_NO_DEREF))\n-\t\tresolve_symlink(&filename);\n+\t\tresolve_symlink(&base_path);\n+\n+\tget_lock_path(&lock_path, base_path.buf);\n+\tget_pid_path(&pid_path, base_path.buf);\n+\n+\tlk->tempfile = create_tempfile_mode(lock_path.buf, mode);\n+\tif (lk->tempfile)\n+\t\tlk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);\n \n-\tstrbuf_addstr(&filename, LOCK_SUFFIX);\n-\tlk->tempfile = create_tempfile_mode(filename.buf, mode);\n-\tstrbuf_release(&filename);\n+\tstrbuf_release(&base_path);\n+\tstrbuf_release(&lock_path);\n+\tstrbuf_release(&pid_path);\n \treturn lk->tempfile ? lk->tempfile->fd : -1;\n }\n \n@@ -151,16 +250,49 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,\n void unable_to_lock_message(const char *path, int err, struct strbuf *buf)\n {\n \tif (err == EEXIST) {\n-\t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s.\\n\\n\"\n-\t\t    \"Another git process seems to be running in this repository, e.g.\\n\"\n-\t\t    \"an editor opened by 'git commit'. Please make sure all processes\\n\"\n-\t\t    \"are terminated then try again. If it still fails, a git process\\n\"\n-\t\t    \"may have crashed in this repository earlier:\\n\"\n-\t\t    \"remove the file manually to continue.\"),\n-\t\t\t    absolute_path(path), strerror(err));\n-\t} else\n+\t\tconst char *abs_path = absolute_path(path);\n+\t\tstruct strbuf lock_path = STRBUF_INIT;\n+\t\tstruct strbuf pid_path = STRBUF_INIT;\n+\t\tuintmax_t pid;\n+\t\tint pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */\n+\n+\t\tget_lock_path(&lock_path, abs_path);\n+\t\tget_pid_path(&pid_path, abs_path);\n+\n+\t\tstrbuf_addf(buf, _(\"Unable to create '%s': %s.\\n\\n\"),\n+\t\t\t    lock_path.buf, strerror(err));\n+\n+\t\t/*\n+\t\t * Try to read PID file unconditionally - it may exist if\n+\t\t * core.lockfilePid was enabled.\n+\t\t */\n+\t\tif (!read_lock_pid(pid_path.buf, &pid)) {\n+\t\t\tif (kill((pid_t)pid, 0) == 0 || errno == EPERM)\n+\t\t\t\tpid_status = 1;  /* running (or no permission to signal) */\n+\t\t\telse if (errno == ESRCH)\n+\t\t\t\tpid_status = -1; /* no such process - stale lock */\n+\t\t}\n+\n+\t\tif (pid_status == 1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock may be held by process %\" PRIuMAX \"; \"\n+\t\t\t\t\t   \"if no git process is running, the lock file \"\n+\t\t\t\t\t   \"may be stale (PIDs can be reused)\"),\n+\t\t\t\t    pid);\n+\t\telse if (pid_status == -1)\n+\t\t\tstrbuf_addf(buf, _(\"Lock was held by process %\" PRIuMAX \", \"\n+\t\t\t\t\t   \"which is no longer running; the lock file \"\n+\t\t\t\t\t   \"appears to be stale\"),\n+\t\t\t\t    pid);\n+\t\telse\n+\t\t\tstrbuf_addstr(buf, _(\"Another git process seems to be running in this repository, \"\n+\t\t\t\t\t     \"or the lock file may be stale\"));\n+\n+\t\tstrbuf_release(&lock_path);\n+\t\tstrbuf_release(&pid_path);\n+\t} else {\n \t\tstrbuf_addf(buf, _(\"Unable to create '%s.lock': %s\"),\n \t\t\t    absolute_path(path), strerror(err));\n+\t}\n }\n \n NORETURN void unable_to_lock_die(const char *path, int err)\n@@ -207,6 +339,8 @@ int commit_lock_file(struct lock_file *lk)\n {\n \tchar *result_path = get_locked_file_path(lk);\n \n+\tdelete_tempfile(&lk->pid_tempfile);\n+\n \tif (commit_lock_file_to(lk, result_path)) {\n \t\tint save_errno = errno;\n \t\tfree(result_path);\n@@ -216,3 +350,9 @@ int commit_lock_file(struct lock_file *lk)\n \tfree(result_path);\n \treturn 0;\n }\n+\n+int rollback_lock_file(struct lock_file *lk)\n+{\n+\tdelete_tempfile(&lk->pid_tempfile);\n+\treturn delete_tempfile(&lk->tempfile);\n+}\ndiff --git a/lockfile.h b/lockfile.h\nindex 1bb9926497..e7233f28de 100644\n--- a/lockfile.h\n+++ b/lockfile.h\n@@ -119,6 +119,7 @@\n \n struct lock_file {\n \tstruct tempfile *tempfile;\n+\tstruct tempfile *pid_tempfile;\n };\n \n #define LOCK_INIT { 0 }\n@@ -127,6 +128,22 @@ struct lock_file {\n #define LOCK_SUFFIX \".lock\"\n #define LOCK_SUFFIX_LEN 5\n \n+/*\n+ * PID file naming: for a lock file \"foo.lock\", the PID file is \"foo~pid.lock\".\n+ * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding\n+ * namespace collisions (e.g., refs \"foo\" and \"foo~pid\" cannot both exist).\n+ */\n+#define LOCK_PID_INFIX \"~pid\"\n+#define LOCK_PID_INFIX_LEN 4\n+\n+/* Maximum length for PID file content */\n+#define LOCK_PID_MAXLEN 32\n+\n+/*\n+ * Whether to create PID files alongside lock files.\n+ * Configured via core.lockfilePid (boolean).\n+ */\n+extern int lockfile_pid_enabled;\n \n /*\n  * Flags\n@@ -169,12 +186,12 @@ struct lock_file {\n  * handling, and mode are described above.\n  */\n int hold_lock_file_for_update_timeout_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms, int mode);\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms, int mode);\n \n static inline int hold_lock_file_for_update_timeout(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, long timeout_ms)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, long timeout_ms)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags,\n \t\t\t\t\t\t      timeout_ms, 0666);\n@@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(\n  * argument and error handling are described above.\n  */\n static inline int hold_lock_file_for_update(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags)\n+\tstruct lock_file *lk, const char *path, int flags)\n {\n \treturn hold_lock_file_for_update_timeout(lk, path, flags, 0);\n }\n \n static inline int hold_lock_file_for_update_mode(\n-\t\tstruct lock_file *lk, const char *path,\n-\t\tint flags, int mode)\n+\tstruct lock_file *lk, const char *path,\n+\tint flags, int mode)\n {\n \treturn hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);\n }\n@@ -319,13 +335,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)\n \n /*\n  * Roll back `lk`: close the file descriptor and/or file pointer and\n- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`\n- * for a `lock_file` object that has already been committed or rolled\n- * back. No error will be returned in this case.\n+ * remove the lockfile and any associated PID file. It is a NOOP to\n+ * call `rollback_lock_file()` for a `lock_file` object that has already\n+ * been committed or rolled back. No error will be returned in this case.\n  */\n-static inline int rollback_lock_file(struct lock_file *lk)\n-{\n-\treturn delete_tempfile(&lk->tempfile);\n-}\n+int rollback_lock_file(struct lock_file *lk);\n \n #endif /* LOCKFILE_H */\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..2aec2c011e 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -98,6 +98,7 @@ integration_tests = [\n   't0028-working-tree-encoding.sh',\n   't0029-core-unsetenvvars.sh',\n   't0030-stripspace.sh',\n+  't0031-lockfile-pid.sh',\n   't0033-safe-directory.sh',\n   't0034-root-safe-directory.sh',\n   't0035-safe-bare-repository.sh',\ndiff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\nnew file mode 100755\nindex 0000000000..8ef87addf5\n--- /dev/null\n+++ b/t/t0031-lockfile-pid.sh\n@@ -0,0 +1,105 @@\n+#!/bin/sh\n+\n+test_description='lock file PID info tests\n+\n+Tests for PID info file alongside lock files.\n+The feature is opt-in via core.lockfilePid config setting (boolean).\n+'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'stale lock detected when PID is not running' '\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n+\t\ttest_grep \"process 99999, which is no longer running\" err &&\n+\t\ttest_grep \"appears to be stale\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info not shown by default' '\n+\tgit init repo2 &&\n+\t(\n+\t\tcd repo2 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\ttest_must_fail git add . 2>err &&\n+\t\t# Should not crash, just show normal error without PID\n+\t\ttest_grep \"Unable to create\" err &&\n+\t\t! test_grep \"is held by process\" err\n+\t)\n+'\n+\n+test_expect_success 'running process detected when PID is alive' '\n+\tgit init repo3 &&\n+\t(\n+\t\tcd repo3 &&\n+\t\techo content >file &&\n+\t\t# Get the correct PID for this platform\n+\t\tshell_pid=$$ &&\n+\t\tif test_have_prereq MINGW && test -f /proc/$shell_pid/winpid\n+\t\tthen\n+\t\t\t# In Git for Windows, Bash uses MSYS2 PIDs but git.exe\n+\t\t\t# uses Windows PIDs. Use the Windows PID.\n+\t\t\tshell_pid=$(cat /proc/$shell_pid/winpid)\n+\t\tfi &&\n+\t\t# Create a lock and PID file with current shell PID (which is running)\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid %d\" \"$shell_pid\" >.git/index~pid.lock &&\n+\t\t# Verify our PID is shown in the error message\n+\t\ttest_must_fail git -c core.lockfilePid=true add file 2>err &&\n+\t\ttest_grep \"held by process $shell_pid\" err\n+\t)\n+'\n+\n+test_expect_success 'PID info file cleaned up on successful operation when enabled' '\n+\tgit init repo4 &&\n+\t(\n+\t\tcd repo4 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=true add file &&\n+\t\t# After successful add, no lock or PID files should exist\n+\t\ttest_path_is_missing .git/index.lock &&\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'no PID file created by default' '\n+\tgit init repo5 &&\n+\t(\n+\t\tcd repo5 &&\n+\t\techo content >file &&\n+\t\tgit add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'core.lockfilePid=false does not create PID file' '\n+\tgit init repo6 &&\n+\t(\n+\t\tcd repo6 &&\n+\t\techo content >file &&\n+\t\tgit -c core.lockfilePid=false add file &&\n+\t\t# PID file should not be created when feature is disabled\n+\t\ttest_path_is_missing .git/index~pid.lock\n+\t)\n+'\n+\n+test_expect_success 'existing PID files are read even when feature disabled' '\n+\tgit init repo7 &&\n+\t(\n+\t\tcd repo7 &&\n+\t\ttouch .git/index.lock &&\n+\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n+\t\t# Even with lockfilePid disabled, existing PID files are read\n+\t\t# to help diagnose stale locks\n+\t\ttest_must_fail git add . 2>err &&\n+\t\ttest_grep \"process 99999\" err\n+\t)\n+'\n+\n+test_done\n\nbase-commit: 66ce5f8e8872f0183bb137911c52b07f1f242d13\n-- \ngitgitgadget\n"},{"id":"534496","messageId":"xmqqh5sdmmga.fsf@gitster.g","threadId":"64568","inReplyTo":"pull.2011.v6.git.1769109815197.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-22T20:17:25Z","receivedAt":"2026-01-22T20:17:28Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Paulo Casaretto via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>     lockfile: add holder info file for debugging stale locks\n>     \n>     Changes in v6:\n>     \n>      * Applied Junio's fix-up: removed unused fd = -1 assignments and the\n>        unreachable if (fd >= 0) close(fd) cleanup at the out label in\n>        create_lock_pid_file().\n>      * Kept close(fd) before unlink() in the error path for Windows\n>        compatibility, as noted by Eric Sunshine and Johannes Sixt.\n>      * Reverted the unnecessary whitespace-only changes to builtin/commit.c\n>        and builtin/gc.c - these files are no longer touched by this patch.\n>\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v6\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v6\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2011\n\nLooking good.  Shall we declare victory and mark it for 'next' now?\n\nThanks.\n"},{"id":"535364","messageId":"aYYWfb1E6EHxEyQQ@pks.im","threadId":"64568","inReplyTo":"pull.2011.v6.git.1769109815197.gitgitgadget@gmail.com","subject":"Re: [PATCH v6] lockfile: add PID file for debugging stale locks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-06T16:27:41Z","receivedAt":"2026-02-06T16:27:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jan 22, 2026 at 07:23:35PM +0000, Paulo Casaretto via GitGitGadget wrote:\n> diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\n> new file mode 100755\n> index 0000000000..8ef87addf5\n> --- /dev/null\n> +++ b/t/t0031-lockfile-pid.sh\n> @@ -0,0 +1,105 @@\n> +#!/bin/sh\n> +\n> +test_description='lock file PID info tests\n> +\n> +Tests for PID info file alongside lock files.\n> +The feature is opt-in via core.lockfilePid config setting (boolean).\n> +'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'stale lock detected when PID is not running' '\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttouch .git/index.lock &&\n> +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n> +\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n> +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n> +\t\ttest_grep \"appears to be stale\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'PID info not shown by default' '\n> +\tgit init repo2 &&\n\nNit, not really worth addressing on its own: you could avoid the\nnumbering if you used `test_when_finished rm -rf repo`.\n\nOther than that I don't have any complaints from the ref side anymore,\nand the code seems sensible to me. Thanks!\n\nPatrick\n"},{"id":"535386","messageId":"xmqqecmxekjd.fsf@gitster.g","threadId":"64568","inReplyTo":"aYYWfb1E6EHxEyQQ@pks.im","subject":"Re: [PATCH v6] lockfile: add PID file for debugging stale locks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-06T19:31:18Z","receivedAt":"2026-02-06T19:31:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Thu, Jan 22, 2026 at 07:23:35PM +0000, Paulo Casaretto via GitGitGadget wrote:\n>> diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh\n>> new file mode 100755\n>> index 0000000000..8ef87addf5\n>> --- /dev/null\n>> +++ b/t/t0031-lockfile-pid.sh\n>> @@ -0,0 +1,105 @@\n>> +#!/bin/sh\n>> +\n>> +test_description='lock file PID info tests\n>> +\n>> +Tests for PID info file alongside lock files.\n>> +The feature is opt-in via core.lockfilePid config setting (boolean).\n>> +'\n>> +\n>> +. ./test-lib.sh\n>> +\n>> +test_expect_success 'stale lock detected when PID is not running' '\n>> +\tgit init repo &&\n>> +\t(\n>> +\t\tcd repo &&\n>> +\t\ttouch .git/index.lock &&\n>> +\t\tprintf \"pid 99999\" >.git/index~pid.lock &&\n>> +\t\ttest_must_fail git -c core.lockfilePid=true add . 2>err &&\n>> +\t\ttest_grep \"process 99999, which is no longer running\" err &&\n>> +\t\ttest_grep \"appears to be stale\" err\n>> +\t)\n>> +'\n>> +\n>> +test_expect_success 'PID info not shown by default' '\n>> +\tgit init repo2 &&\n>\n> Nit, not really worth addressing on its own: you could avoid the\n> numbering if you used `test_when_finished rm -rf repo`.\n>\n> Other than that I don't have any complaints from the ref side anymore,\n> and the code seems sensible to me. Thanks!\n\nThanks, both.\n"}]}