git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH] lockfile: add PID file for debugging stale locks

From
PGPaulo Casaretto via GitGitGadget <gitgitgadget@gmail.com>
Date
Dec 2, 2025, 15:07 UTC
Message-ID
<pull.2011.git.1764688047077.gitgitgadget@gmail.com>
From: Paulo Casaretto <pcasaretto@gmail.com>

When a lock file is held, it can be helpful to know which process owns it, especially when debugging stale locks left behind by crashed processes. Add an optional feature that creates a companion .lock.pid file alongside each lock file, containing the PID of the lock holder.

The .lock.pid file is created when a lock is acquired (if enabled), and automatically cleaned up when the lock is released (via commit or rollback). The file is registered as a tempfile so it gets cleaned up by signal and atexit handlers if the process terminates abnormally.

When a lock conflict occurs, the code checks if the PID from the .pid file is still running using kill(pid, 0). This allows providing context-aware error messages. With PID info enabled:

  Lock is held by process 12345. Wait for it to finish, or remove
  the lock file to continue.
Or for a stale lock:
  Lock was held by process 12345, which is no longer running.
  Remove the stale lock file to continue.
Without PID info (default):
  Another git process seems to be running in this repository.
  Wait for it to finish, or remove the lock file to continue.
The feature is opt-in via GIT_LOCK_PID_INFO=1 environment variable.
Signed-off-by: Paulo Casaretto <pcasaretto@gmail.com>
---
    lockfile: add holder info file for debugging stale locks
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2011%2Fpcasaretto%2Fpid-holder-file-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2011/pcasaretto/pid-holder-file-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2011
 Documentation/git.adoc  |  10 ++++
 lockfile.c              | 109 +++++++++++++++++++++++++++++++++++++---
 lockfile.h              |  21 +++++---
 t/meson.build           |   1 +
 t/t0031-lockfile-pid.sh |  73 +++++++++++++++++++++++++++
 5 files changed, 199 insertions(+), 15 deletions(-)
 create mode 100755 t/t0031-lockfile-pid.sh
diff --git a/Documentation/git.adoc b/Documentation/git.adoc
index ce099e78b8..6fdd509d34 100644
--- a/Documentation/git.adoc
+++ b/Documentation/git.adoc
@@ -1010,6 +1010,16 @@ be in the future).
 	the background which do not want to cause lock contention with
 	other operations on the repository.  Defaults to `1`.
 
+`GIT_LOCK_PID_INFO`::
+	If this Boolean environment variable is set to `1`, Git will create
+	a `.lock.pid` file alongside each lock file containing the PID of the
+	process that created the lock. This information is displayed in error
+	messages when a lock conflict occurs, making it easier to identify
+	stale locks or debug locking issues. The PID files are automatically
+	cleaned up via signal and atexit handlers; however, if a process is
+	terminated abnormally (e.g., SIGKILL), the file may remain as a stale
+	indicator. Disabled by default.
+
 `GIT_REDIRECT_STDIN`::
 `GIT_REDIRECT_STDOUT`::
 `GIT_REDIRECT_STDERR`::
diff --git a/lockfile.c b/lockfile.c
index 1d5ed01682..4a694b9c3d 100644
--- a/lockfile.c
+++ b/lockfile.c
@@ -6,6 +6,9 @@
 #include "abspath.h"
 #include "gettext.h"
 #include "lockfile.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "wrapper.h"
 
 /*
  * path = absolute or relative path name
@@ -71,6 +74,62 @@ static void resolve_symlink(struct strbuf *path)
 	strbuf_reset(&link);
 }
 
+/*
+ * Lock PID file functions - write PID to a .lock.pid file alongside
+ * the lock file for debugging stale locks. The PID file is registered
+ * as a tempfile so it gets cleaned up by signal/atexit handlers.
+ */
+
+static int lock_pid_info_enabled(void)
+{
+	return git_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);
+}
+
+static struct tempfile *create_lock_pid_file(const char *lock_path, int mode)
+{
+	struct strbuf pid_path = STRBUF_INIT;
+	struct strbuf content = STRBUF_INIT;
+	struct tempfile *pid_tempfile = NULL;
+	int fd;
+
+	if (!lock_pid_info_enabled())
+		return NULL;
+
+	strbuf_addf(&pid_path, "%s%s", lock_path, LOCK_PID_SUFFIX);
+	fd = open(pid_path.buf, O_WRONLY | O_CREAT | O_TRUNC, mode);
+	if (fd >= 0) {
+		strbuf_addf(&content, "%"PRIuMAX"\n", (uintmax_t)getpid());
+		if (write_in_full(fd, content.buf, content.len) < 0)
+			warning_errno(_("could not write lock pid file '%s'"),
+				      pid_path.buf);
+		close(fd);
+		pid_tempfile = register_tempfile(pid_path.buf);
+	}
+	strbuf_release(&content);
+	strbuf_release(&pid_path);
+	return pid_tempfile;
+}
+
+static int read_lock_pid(const char *lock_path, uintmax_t *pid_out)
+{
+	struct strbuf pid_path = STRBUF_INIT;
+	struct strbuf content = STRBUF_INIT;
+	int ret = -1;
+
+	strbuf_addf(&pid_path, "%s%s", lock_path, LOCK_PID_SUFFIX);
+	if (strbuf_read_file(&content, pid_path.buf, LOCK_PID_MAXLEN) > 0) {
+		char *endptr;
+		*pid_out = strtoumax(content.buf, &endptr, 10);
+		if (*pid_out > 0 && (*endptr == '\n' || *endptr == '\0'))
+			ret = 0;
+		else
+			warning(_("malformed lock pid file '%s'"), pid_path.buf);
+	}
+	strbuf_release(&pid_path);
+	strbuf_release(&content);
+	return ret;
+}
+
 /* Make sure errno contains a meaningful value on error */
 static int lock_file(struct lock_file *lk, const char *path, int flags,
 		     int mode)
@@ -80,9 +139,12 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,
 	strbuf_addstr(&filename, path);
 	if (!(flags & LOCK_NO_DEREF))
 		resolve_symlink(&filename);
-
 	strbuf_addstr(&filename, LOCK_SUFFIX);
+
 	lk->tempfile = create_tempfile_mode(filename.buf, mode);
+	if (lk->tempfile)
+		lk->pid_tempfile = create_lock_pid_file(filename.buf, mode);
+
 	strbuf_release(&filename);
 	return lk->tempfile ? lk->tempfile->fd : -1;
 }
@@ -151,13 +213,36 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
 void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
 {
 	if (err == EEXIST) {
-		strbuf_addf(buf, _("Unable to create '%s.lock': %s.\n\n"
-		    "Another git process seems to be running in this repository, e.g.\n"
-		    "an editor opened by 'git commit'. Please make sure all processes\n"
-		    "are terminated then try again. If it still fails, a git process\n"
-		    "may have crashed in this repository earlier:\n"
-		    "remove the file manually to continue."),
-			    absolute_path(path), strerror(err));
+		struct strbuf lock_path = STRBUF_INIT;
+		uintmax_t pid;
+		int pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */
+
+		strbuf_addf(&lock_path, "%s%s", absolute_path(path), LOCK_SUFFIX);
+
+		strbuf_addf(buf, _("Unable to create '%s': %s.\n\n"),
+			    lock_path.buf, strerror(err));
+
+		if (lock_pid_info_enabled() &&
+		    !read_lock_pid(lock_path.buf, &pid)) {
+			if (kill((pid_t)pid, 0) == 0)
+				pid_status = 1;
+			else
+				pid_status = -1;
+		}
+
+		if (pid_status == 1)
+			strbuf_addf(buf, _("Lock is held by process %"PRIuMAX". "
+			    "Wait for it to finish, or remove the lock file to continue"),
+			    pid);
+		else if (pid_status == -1)
+			strbuf_addf(buf, _("Lock was held by process %"PRIuMAX", "
+			    "which is no longer running. Remove the stale lock file to continue"),
+			    pid);
+		else
+			strbuf_addstr(buf, _("Another git process seems to be running in this repository. "
+			    "Wait for it to finish, or remove the lock file to continue"));
+
+		strbuf_release(&lock_path);
 	} else
 		strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
 			    absolute_path(path), strerror(err));
@@ -207,6 +292,8 @@ int commit_lock_file(struct lock_file *lk)
 {
 	char *result_path = get_locked_file_path(lk);
 
+	delete_tempfile(&lk->pid_tempfile);
+
 	if (commit_lock_file_to(lk, result_path)) {
 		int save_errno = errno;
 		free(result_path);
@@ -216,3 +303,9 @@ int commit_lock_file(struct lock_file *lk)
 	free(result_path);
 	return 0;
 }
+
+int rollback_lock_file(struct lock_file *lk)
+{
+	delete_tempfile(&lk->pid_tempfile);
+	return delete_tempfile(&lk->tempfile);
+}
diff --git a/lockfile.h b/lockfile.h
index 1bb9926497..b2d5a1bc6c 100644
--- a/lockfile.h
+++ b/lockfile.h
@@ -119,6 +119,7 @@
 
 struct lock_file {
 	struct tempfile *tempfile;
+	struct tempfile *pid_tempfile;
 };
 
 #define LOCK_INIT { 0 }
@@ -127,6 +128,15 @@ struct lock_file {
 #define LOCK_SUFFIX ".lock"
 #define LOCK_SUFFIX_LEN 5
 
+/* Suffix for PID file that stores PID of lock holder: */
+#define LOCK_PID_SUFFIX ".pid"
+#define LOCK_PID_SUFFIX_LEN 4
+
+/* Maximum length for PID file content */
+#define LOCK_PID_MAXLEN 32
+
+/* Environment variable to enable lock PID info (default: disabled) */
+#define GIT_LOCK_PID_INFO_ENVIRONMENT "GIT_LOCK_PID_INFO"
 
 /*
  * Flags
@@ -319,13 +329,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)
 
 /*
  * Roll back `lk`: close the file descriptor and/or file pointer and
- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`
- * for a `lock_file` object that has already been committed or rolled
- * back. No error will be returned in this case.
+ * remove the lockfile and any associated PID file. It is a NOOP to
+ * call `rollback_lock_file()` for a `lock_file` object that has already
+ * been committed or rolled back. No error will be returned in this case.
  */
-static inline int rollback_lock_file(struct lock_file *lk)
-{
-	return delete_tempfile(&lk->tempfile);
-}
+int rollback_lock_file(struct lock_file *lk);
 
 #endif /* LOCKFILE_H */
diff --git a/t/meson.build b/t/meson.build
index dc43d69636..9a880db57c 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -98,6 +98,7 @@ integration_tests = [
   't0028-working-tree-encoding.sh',
   't0029-core-unsetenvvars.sh',
   't0030-stripspace.sh',
+  't0031-lockfile-pid.sh',
   't0033-safe-directory.sh',
   't0034-root-safe-directory.sh',
   't0035-safe-bare-repository.sh',
diff --git a/t/t0031-lockfile-pid.sh b/t/t0031-lockfile-pid.sh
new file mode 100755
index 0000000000..3c1e9bf60e
--- /dev/null
+++ b/t/t0031-lockfile-pid.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+test_description='lock file PID info tests
+
+Tests for PID info file alongside lock files.
+The feature is opt-in via GIT_LOCK_PID_INFO=1.
+'
+
+. ./test-lib.sh
+
+test_expect_success 'stale lock detected when PID is not running' '
+	git init repo &&
+	(
+		cd repo &&
+		touch .git/index.lock &&
+		echo "99999" >.git/index.lock.pid &&
+		test_must_fail env GIT_LOCK_PID_INFO=1 git add . 2>err &&
+		test_grep "process 99999, which is no longer running" err &&
+		test_grep "Remove the stale lock file" err
+	)
+'
+
+test_expect_success 'PID info not shown by default' '
+	git init repo2 &&
+	(
+		cd repo2 &&
+		touch .git/index.lock &&
+		echo "99999" >.git/index.lock.pid &&
+		test_must_fail git add . 2>err &&
+		# Should not crash, just show normal error without PID
+		test_grep "Unable to create" err &&
+		! test_grep "is held by process" err
+	)
+'
+
+test_expect_success 'running process detected when PID is alive' '
+	git init repo3 &&
+	(
+		cd repo3 &&
+		echo content >file &&
+		# Create a lock and PID file with current shell PID (which is running)
+		touch .git/index.lock &&
+		echo $$ >.git/index.lock.pid &&
+		# Verify our PID is shown in the error message
+		test_must_fail env GIT_LOCK_PID_INFO=1 git add file 2>err &&
+		test_grep "held by process $$" err
+	)
+'
+
+test_expect_success 'PID info file cleaned up on successful operation when enabled' '
+	git init repo4 &&
+	(
+		cd repo4 &&
+		echo content >file &&
+		env GIT_LOCK_PID_INFO=1 git add file &&
+		# After successful add, no lock or PID files should exist
+		! test -f .git/index.lock &&
+		! test -f .git/index.lock.pid
+	)
+'
+
+test_expect_success 'no PID file created by default' '
+	git init repo5 &&
+	(
+		cd repo5 &&
+		echo content >file &&
+		git add file &&
+		# PID file should not be created when feature is disabled
+		! test -f .git/index.lock.pid
+	)
+'
+
+test_done

base-commit: 6ab38b7e9cc7adafc304f3204616a4debd49c6e9
-- 
gitgitgadget
Next: D. Ben Knoble
Message 1 of 36 in “lockfile: add PID file for debugging stale locks”
  1. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Dec 2, 2025
  2. D. Ben KnobleDec 2, 2025
  3. Torsten BögershausenDec 3, 2025
  4. Jeff KingDec 3, 2025
  5. Junio C HamanoDec 3, 2025
  6. Jeff KingDec 3, 2025
  7. Taylor BlauDec 3, 2025
  8. Patrick SteinhardtDec 5, 2025
  9. Jeff KingDec 5, 2025
  10. Taylor BlauDec 3, 2025
  11. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Dec 17, 2025
  12. Junio C HamanoDec 18, 2025
  13. Junio C HamanoDec 18, 2025
  14. Junio C HamanoDec 18, 2025
  15. Ben KnobleDec 18, 2025
  16. Patrick SteinhardtDec 18, 2025
  17. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Dec 24, 2025
  18. Junio C HamanoDec 25, 2025
  19. Jeff KingDec 27, 2025
  20. Patrick SteinhardtJan 5, 2026
  21. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Jan 7, 2026
  22. Junio C HamanoJan 8, 2026
  23. D. Ben KnobleJan 8, 2026
  24. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Jan 20, 2026
  25. Junio C HamanoJan 20, 2026
  26. Jeff KingJan 21, 2026
  27. Eric SunshineJan 21, 2026
  28. Johannes SixtJan 21, 2026
  29. Jeff KingJan 21, 2026
  30. Junio C HamanoJan 21, 2026
  31. Jeff KingJan 21, 2026
  32. Junio C HamanoJan 21, 2026
  33. lockfile: add PID file for debugging stale locksPaulo Casaretto via GitGitGadget, Jan 22, 2026
  34. Junio C HamanoJan 22, 2026
  35. Patrick SteinhardtFeb 6, 2026
  36. Junio C HamanoFeb 6, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.