From: "Paulo Casaretto via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Taylor Blau <me@ttaylorr.com>,
Paulo Casaretto <pcasaretto@gmail.com>,
Paulo Casaretto <pcasaretto@gmail.com>
Subject: [PATCH] lockfile: add PID file for debugging stale locks
Date: Tue, 02 Dec 2025 15:07:27 +0000 [thread overview]
Message-ID: <pull.2011.git.1764688047077.gitgitgadget@gmail.com> (raw)
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 reply other threads:[~2025-12-02 15:07 UTC|newest]
Thread overview: 36+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-02 15:07 Paulo Casaretto via GitGitGadget [this message]
2025-12-02 22:29 ` [PATCH] lockfile: add PID file for debugging stale locks D. Ben Knoble
2025-12-03 19:48 ` Torsten Bögershausen
2025-12-03 21:16 ` Jeff King
2025-12-03 22:21 ` Junio C Hamano
2025-12-03 22:32 ` Jeff King
2025-12-03 23:19 ` Taylor Blau
2025-12-05 11:03 ` Patrick Steinhardt
2025-12-05 18:46 ` Jeff King
2025-12-03 23:39 ` Taylor Blau
2025-12-17 18:59 ` [PATCH v2] " Paulo Casaretto via GitGitGadget
2025-12-18 0:32 ` Junio C Hamano
2025-12-18 0:47 ` Junio C Hamano
2025-12-18 1:33 ` Junio C Hamano
2025-12-18 3:38 ` Ben Knoble
2025-12-18 8:07 ` Patrick Steinhardt
2025-12-24 12:24 ` [PATCH v3] " Paulo Casaretto via GitGitGadget
2025-12-25 0:01 ` Junio C Hamano
2025-12-27 7:50 ` Jeff King
2026-01-05 12:23 ` Patrick Steinhardt
2026-01-07 16:45 ` [PATCH v4] " Paulo Casaretto via GitGitGadget
2026-01-08 1:59 ` Junio C Hamano
2026-01-08 14:19 ` D. Ben Knoble
2026-01-20 18:32 ` [PATCH v5] " Paulo Casaretto via GitGitGadget
2026-01-20 20:02 ` Junio C Hamano
2026-01-21 7:13 ` Jeff King
2026-01-21 8:13 ` Eric Sunshine
2026-01-21 10:14 ` Johannes Sixt
2026-01-21 16:39 ` Jeff King
2026-01-21 18:55 ` Junio C Hamano
2026-01-21 19:53 ` Jeff King
2026-01-21 16:23 ` Junio C Hamano
2026-01-22 19:23 ` [PATCH v6] " Paulo Casaretto via GitGitGadget
2026-01-22 20:17 ` Junio C Hamano
2026-02-06 16:27 ` Patrick Steinhardt
2026-02-06 19:31 ` Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2011.git.1764688047077.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=me@ttaylorr.com \
--cc=pcasaretto@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.