lockfile: add PID file for debugging stale locks

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 PID file alongside each lock file, containing the PID of the lock holder. For a lock file "foo.lock", the PID file is named "foo~pid.lock". The tilde character is forbidden in refnames and allowed in Windows filenames, which guarantees no collision with the refs namespace (e.g., refs "foo" and "foo~pid" cannot both exist). The file contains a single line in the format "pid <value>" followed by a newline. The 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 for an existing PID file and, if found, uses kill(pid, 0) to determine if the process is still running. This allows providing context-aware error messages: 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. The feature is controlled via core.lockfilePid configuration (boolean). Defaults to false. When enabled, PID files are created for all lock operations. Existing PID files are always read when displaying lock errors, regardless of the core.lockfilePid setting. This ensures helpful diagnostics even when the feature was previously enabled and later disabled. Signed-off-by: Paulo Casaretto <pcasaretto@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Paulo Casaretto committed Jan 22, 2026 at 19:23 UTC dbdcab6b89ea86fe58ece01bbb7be297ff23b2c4
7 files changed +315 -29
Documentation/config/core.adoc
+11
@@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a
348 read-only snapshot of the same index to a location different from the
349 repository's usual working tree).
350
351 +core.lockfilePid::
352 + If true, Git will create a PID file alongside lock files. When a
353 + lock acquisition fails and a PID file exists, Git can provide
354 + additional diagnostic information about the process holding the
355 + lock, including whether it is still running. Defaults to `false`.
356 ++
357 +The PID file is named by inserting `~pid` before the `.lock` suffix.
358 +For example, if the lock file is `index.lock`, the PID file will be
359 +`index~pid.lock`. The file contains a single line in the format
360 +`pid <value>` followed by a newline.
361 +
362 core.logAllRefUpdates::
363 Enable the reflog. Updates to a ref <ref> is logged to the file
364 "`$GIT_DIR/logs/<ref>`", by appending the new and old
compat/mingw.c
+10
@@ -1972,6 +1972,16 @@ int mingw_kill(pid_t pid, int sig)
1972 CloseHandle(h);
1973 return 0;
1974 }
1975 + /*
1976 + * OpenProcess returns ERROR_INVALID_PARAMETER for
1977 + * non-existent PIDs. Map this to ESRCH for POSIX
1978 + * compatibility with kill(pid, 0).
1979 + */
1980 + if (GetLastError() == ERROR_INVALID_PARAMETER)
1981 + errno = ESRCH;
1982 + else
1983 + errno = err_win_to_posix(GetLastError());
1984 + return -1;
1985 }
1986
1987 errno = EINVAL;
environment.c
+6
@@ -21,6 +21,7 @@
21 #include "gettext.h"
22 #include "git-zlib.h"
23 #include "ident.h"
24 +#include "lockfile.h"
25 #include "mailmap.h"
26 #include "object-name.h"
27 #include "repository.h"
@@ -532,6 +533,11 @@ static int git_default_core_config(const char *var, const char *value,
533 return 0;
534 }
535
536 + if (!strcmp(var, "core.lockfilepid")) {
537 + lockfile_pid_enabled = git_config_bool(var, value);
538 + return 0;
539 + }
540 +
541 if (!strcmp(var, "core.createobject")) {
542 if (!value)
543 return config_error_nonbool(var);
lockfile.c
+154 -14
@@ -6,6 +6,9 @@
6 #include "abspath.h"
7 #include "gettext.h"
8 #include "lockfile.h"
9 +#include "parse.h"
10 +#include "strbuf.h"
11 +#include "wrapper.h"
12
13 /*
14 * path = absolute or relative path name
@@ -71,19 +74,115 @@ static void resolve_symlink(struct strbuf *path)
74 strbuf_reset(&link);
75 }
76
77 +/*
78 + * Lock PID file functions - write PID to a foo~pid.lock file alongside
79 + * the lock file for debugging stale locks. The PID file is registered
80 + * as a tempfile so it gets cleaned up by signal/atexit handlers.
81 + *
82 + * Naming: For "foo.lock", the PID file is "foo~pid.lock". The tilde is
83 + * forbidden in refnames and allowed in Windows filenames, guaranteeing
84 + * no collision with the refs namespace.
85 + */
86 +
87 +/* Global config variable, initialized from core.lockfilePid */
88 +int lockfile_pid_enabled;
89 +
90 +/*
91 + * Path generation helpers.
92 + * Given base path "foo", generate:
93 + * - lock path: "foo.lock"
94 + * - pid path: "foo-pid.lock"
95 + */
96 +static void get_lock_path(struct strbuf *out, const char *path)
97 +{
98 + strbuf_addstr(out, path);
99 + strbuf_addstr(out, LOCK_SUFFIX);
100 +}
101 +
102 +static void get_pid_path(struct strbuf *out, const char *path)
103 +{
104 + strbuf_addstr(out, path);
105 + strbuf_addstr(out, LOCK_PID_INFIX);
106 + strbuf_addstr(out, LOCK_SUFFIX);
107 +}
108 +
109 +static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)
110 +{
111 + struct strbuf content = STRBUF_INIT;
112 + struct tempfile *pid_tempfile = NULL;
113 + int fd;
114 +
115 + if (!lockfile_pid_enabled)
116 + goto out;
117 +
118 + fd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);
119 + if (fd < 0)
120 + goto out;
121 +
122 + strbuf_addf(&content, "pid %" PRIuMAX "\n", (uintmax_t)getpid());
123 + if (write_in_full(fd, content.buf, content.len) < 0) {
124 + warning_errno(_("could not write lock pid file '%s'"), pid_path);
125 + close(fd);
126 + unlink(pid_path);
127 + goto out;
128 + }
129 +
130 + close(fd);
131 + pid_tempfile = register_tempfile(pid_path);
132 +
133 +out:
134 + strbuf_release(&content);
135 + return pid_tempfile;
136 +}
137 +
138 +static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)
139 +{
140 + struct strbuf content = STRBUF_INIT;
141 + const char *val;
142 + int ret = -1;
143 +
144 + if (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)
145 + goto out;
146 +
147 + strbuf_rtrim(&content);
148 +
149 + if (skip_prefix(content.buf, "pid ", &val)) {
150 + char *endptr;
151 + *pid_out = strtoumax(val, &endptr, 10);
152 + if (*pid_out > 0 && !*endptr)
153 + ret = 0;
154 + }
155 +
156 + if (ret)
157 + warning(_("malformed lock pid file '%s'"), pid_path);
158 +
159 +out:
160 + strbuf_release(&content);
161 + return ret;
162 +}
163 +
164 /* Make sure errno contains a meaningful value on error */
165 static int lock_file(struct lock_file *lk, const char *path, int flags,
166 int mode)
167 {
78 - struct strbuf filename = STRBUF_INIT;
168 + struct strbuf base_path = STRBUF_INIT;
169 + struct strbuf lock_path = STRBUF_INIT;
170 + struct strbuf pid_path = STRBUF_INIT;
171
80 - strbuf_addstr(&filename, path);
172 + strbuf_addstr(&base_path, path);
173 if (!(flags & LOCK_NO_DEREF))
82 - resolve_symlink(&filename);
174 + resolve_symlink(&base_path);
175 +
176 + get_lock_path(&lock_path, base_path.buf);
177 + get_pid_path(&pid_path, base_path.buf);
178 +
179 + lk->tempfile = create_tempfile_mode(lock_path.buf, mode);
180 + if (lk->tempfile)
181 + lk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);
182
84 - strbuf_addstr(&filename, LOCK_SUFFIX);
85 - lk->tempfile = create_tempfile_mode(filename.buf, mode);
86 - strbuf_release(&filename);
183 + strbuf_release(&base_path);
184 + strbuf_release(&lock_path);
185 + strbuf_release(&pid_path);
186 return lk->tempfile ? lk->tempfile->fd : -1;
187 }
188
@@ -151,16 +250,49 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
250 void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
251 {
252 if (err == EEXIST) {
154 - strbuf_addf(buf, _("Unable to create '%s.lock': %s.\n\n"
155 - "Another git process seems to be running in this repository, e.g.\n"
156 - "an editor opened by 'git commit'. Please make sure all processes\n"
157 - "are terminated then try again. If it still fails, a git process\n"
158 - "may have crashed in this repository earlier:\n"
159 - "remove the file manually to continue."),
160 - absolute_path(path), strerror(err));
161 - } else
253 + const char *abs_path = absolute_path(path);
254 + struct strbuf lock_path = STRBUF_INIT;
255 + struct strbuf pid_path = STRBUF_INIT;
256 + uintmax_t pid;
257 + int pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */
258 +
259 + get_lock_path(&lock_path, abs_path);
260 + get_pid_path(&pid_path, abs_path);
261 +
262 + strbuf_addf(buf, _("Unable to create '%s': %s.\n\n"),
263 + lock_path.buf, strerror(err));
264 +
265 + /*
266 + * Try to read PID file unconditionally - it may exist if
267 + * core.lockfilePid was enabled.
268 + */
269 + if (!read_lock_pid(pid_path.buf, &pid)) {
270 + if (kill((pid_t)pid, 0) == 0 || errno == EPERM)
271 + pid_status = 1; /* running (or no permission to signal) */
272 + else if (errno == ESRCH)
273 + pid_status = -1; /* no such process - stale lock */
274 + }
275 +
276 + if (pid_status == 1)
277 + strbuf_addf(buf, _("Lock may be held by process %" PRIuMAX "; "
278 + "if no git process is running, the lock file "
279 + "may be stale (PIDs can be reused)"),
280 + pid);
281 + else if (pid_status == -1)
282 + strbuf_addf(buf, _("Lock was held by process %" PRIuMAX ", "
283 + "which is no longer running; the lock file "
284 + "appears to be stale"),
285 + pid);
286 + else
287 + strbuf_addstr(buf, _("Another git process seems to be running in this repository, "
288 + "or the lock file may be stale"));
289 +
290 + strbuf_release(&lock_path);
291 + strbuf_release(&pid_path);
292 + } else {
293 strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
294 absolute_path(path), strerror(err));
295 + }
296 }
297
298 NORETURN void unable_to_lock_die(const char *path, int err)
@@ -207,6 +339,8 @@ int commit_lock_file(struct lock_file *lk)
339 {
340 char *result_path = get_locked_file_path(lk);
341
342 + delete_tempfile(&lk->pid_tempfile);
343 +
344 if (commit_lock_file_to(lk, result_path)) {
345 int save_errno = errno;
346 free(result_path);
@@ -216,3 +350,9 @@ int commit_lock_file(struct lock_file *lk)
350 free(result_path);
351 return 0;
352 }
353 +
354 +int rollback_lock_file(struct lock_file *lk)
355 +{
356 + delete_tempfile(&lk->pid_tempfile);
357 + return delete_tempfile(&lk->tempfile);
358 +}
lockfile.h
+28 -15
@@ -119,6 +119,7 @@
119
120 struct lock_file {
121 struct tempfile *tempfile;
122 + struct tempfile *pid_tempfile;
123 };
124
125 #define LOCK_INIT { 0 }
@@ -127,6 +128,22 @@ struct lock_file {
128 #define LOCK_SUFFIX ".lock"
129 #define LOCK_SUFFIX_LEN 5
130
131 +/*
132 + * PID file naming: for a lock file "foo.lock", the PID file is "foo~pid.lock".
133 + * The tilde is forbidden in refnames and allowed in Windows filenames, avoiding
134 + * namespace collisions (e.g., refs "foo" and "foo~pid" cannot both exist).
135 + */
136 +#define LOCK_PID_INFIX "~pid"
137 +#define LOCK_PID_INFIX_LEN 4
138 +
139 +/* Maximum length for PID file content */
140 +#define LOCK_PID_MAXLEN 32
141 +
142 +/*
143 + * Whether to create PID files alongside lock files.
144 + * Configured via core.lockfilePid (boolean).
145 + */
146 +extern int lockfile_pid_enabled;
147
148 /*
149 * Flags
@@ -169,12 +186,12 @@ struct lock_file {
186 * handling, and mode are described above.
187 */
188 int hold_lock_file_for_update_timeout_mode(
172 - struct lock_file *lk, const char *path,
173 - int flags, long timeout_ms, int mode);
189 + struct lock_file *lk, const char *path,
190 + int flags, long timeout_ms, int mode);
191
192 static inline int hold_lock_file_for_update_timeout(
176 - struct lock_file *lk, const char *path,
177 - int flags, long timeout_ms)
193 + struct lock_file *lk, const char *path,
194 + int flags, long timeout_ms)
195 {
196 return hold_lock_file_for_update_timeout_mode(lk, path, flags,
197 timeout_ms, 0666);
@@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(
203 * argument and error handling are described above.
204 */
205 static inline int hold_lock_file_for_update(
189 - struct lock_file *lk, const char *path,
190 - int flags)
206 + struct lock_file *lk, const char *path, int flags)
207 {
208 return hold_lock_file_for_update_timeout(lk, path, flags, 0);
209 }
210
211 static inline int hold_lock_file_for_update_mode(
196 - struct lock_file *lk, const char *path,
197 - int flags, int mode)
212 + struct lock_file *lk, const char *path,
213 + int flags, int mode)
214 {
215 return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);
216 }
@@ -319,13 +335,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)
335
336 /*
337 * Roll back `lk`: close the file descriptor and/or file pointer and
322 - * remove the lockfile. It is a NOOP to call `rollback_lock_file()`
323 - * for a `lock_file` object that has already been committed or rolled
324 - * back. No error will be returned in this case.
338 + * remove the lockfile and any associated PID file. It is a NOOP to
339 + * call `rollback_lock_file()` for a `lock_file` object that has already
340 + * been committed or rolled back. No error will be returned in this case.
341 */
326 -static inline int rollback_lock_file(struct lock_file *lk)
327 -{
328 - return delete_tempfile(&lk->tempfile);
329 -}
342 +int rollback_lock_file(struct lock_file *lk);
343
344 #endif /* LOCKFILE_H */
t/meson.build
+1
@@ -98,6 +98,7 @@ integration_tests = [
98 't0028-working-tree-encoding.sh',
99 't0029-core-unsetenvvars.sh',
100 't0030-stripspace.sh',
101 + 't0031-lockfile-pid.sh',
102 't0033-safe-directory.sh',
103 't0034-root-safe-directory.sh',
104 't0035-safe-bare-repository.sh',
t/t0031-lockfile-pid.sh new
+105
@@ -0,0 +1,105 @@
1 +#!/bin/sh
2 +
3 +test_description='lock file PID info tests
4 +
5 +Tests for PID info file alongside lock files.
6 +The feature is opt-in via core.lockfilePid config setting (boolean).
7 +'
8 +
9 +. ./test-lib.sh
10 +
11 +test_expect_success 'stale lock detected when PID is not running' '
12 + git init repo &&
13 + (
14 + cd repo &&
15 + touch .git/index.lock &&
16 + printf "pid 99999" >.git/index~pid.lock &&
17 + test_must_fail git -c core.lockfilePid=true add . 2>err &&
18 + test_grep "process 99999, which is no longer running" err &&
19 + test_grep "appears to be stale" err
20 + )
21 +'
22 +
23 +test_expect_success 'PID info not shown by default' '
24 + git init repo2 &&
25 + (
26 + cd repo2 &&
27 + touch .git/index.lock &&
28 + printf "pid 99999" >.git/index~pid.lock &&
29 + test_must_fail git add . 2>err &&
30 + # Should not crash, just show normal error without PID
31 + test_grep "Unable to create" err &&
32 + ! test_grep "is held by process" err
33 + )
34 +'
35 +
36 +test_expect_success 'running process detected when PID is alive' '
37 + git init repo3 &&
38 + (
39 + cd repo3 &&
40 + echo content >file &&
41 + # Get the correct PID for this platform
42 + shell_pid=$$ &&
43 + if test_have_prereq MINGW && test -f /proc/$shell_pid/winpid
44 + then
45 + # In Git for Windows, Bash uses MSYS2 PIDs but git.exe
46 + # uses Windows PIDs. Use the Windows PID.
47 + shell_pid=$(cat /proc/$shell_pid/winpid)
48 + fi &&
49 + # Create a lock and PID file with current shell PID (which is running)
50 + touch .git/index.lock &&
51 + printf "pid %d" "$shell_pid" >.git/index~pid.lock &&
52 + # Verify our PID is shown in the error message
53 + test_must_fail git -c core.lockfilePid=true add file 2>err &&
54 + test_grep "held by process $shell_pid" err
55 + )
56 +'
57 +
58 +test_expect_success 'PID info file cleaned up on successful operation when enabled' '
59 + git init repo4 &&
60 + (
61 + cd repo4 &&
62 + echo content >file &&
63 + git -c core.lockfilePid=true add file &&
64 + # After successful add, no lock or PID files should exist
65 + test_path_is_missing .git/index.lock &&
66 + test_path_is_missing .git/index~pid.lock
67 + )
68 +'
69 +
70 +test_expect_success 'no PID file created by default' '
71 + git init repo5 &&
72 + (
73 + cd repo5 &&
74 + echo content >file &&
75 + git add file &&
76 + # PID file should not be created when feature is disabled
77 + test_path_is_missing .git/index~pid.lock
78 + )
79 +'
80 +
81 +test_expect_success 'core.lockfilePid=false does not create PID file' '
82 + git init repo6 &&
83 + (
84 + cd repo6 &&
85 + echo content >file &&
86 + git -c core.lockfilePid=false add file &&
87 + # PID file should not be created when feature is disabled
88 + test_path_is_missing .git/index~pid.lock
89 + )
90 +'
91 +
92 +test_expect_success 'existing PID files are read even when feature disabled' '
93 + git init repo7 &&
94 + (
95 + cd repo7 &&
96 + touch .git/index.lock &&
97 + printf "pid 99999" >.git/index~pid.lock &&
98 + # Even with lockfilePid disabled, existing PID files are read
99 + # to help diagnose stale locks
100 + test_must_fail git add . 2>err &&
101 + test_grep "process 99999" err
102 + )
103 +'
104 +
105 +test_done