Skip to content

Commit 7068ec0

Browse files
committed
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 .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 <[email protected]>
1 parent 6ab38b7 commit 7068ec0

File tree

5 files changed

+199
-15
lines changed

5 files changed

+199
-15
lines changed

‎Documentation/git.adoc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,16 @@ be in the future).
10101010
the background which do not want to cause lock contention with
10111011
other operations on the repository. Defaults to `1`.
10121012

1013+
`GIT_LOCK_PID_INFO`::
1014+
If this Boolean environment variable is set to `1`, Git will create
1015+
a `.lock.pid` file alongside each lock file containing the PID of the
1016+
process that created the lock. This information is displayed in error
1017+
messages when a lock conflict occurs, making it easier to identify
1018+
stale locks or debug locking issues. The PID files are automatically
1019+
cleaned up via signal and atexit handlers; however, if a process is
1020+
terminated abnormally (e.g., SIGKILL), the file may remain as a stale
1021+
indicator. Disabled by default.
1022+
10131023
`GIT_REDIRECT_STDIN`::
10141024
`GIT_REDIRECT_STDOUT`::
10151025
`GIT_REDIRECT_STDERR`::

‎lockfile.c‎

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
#include"abspath.h"
77
#include"gettext.h"
88
#include"lockfile.h"
9+
#include"parse.h"
10+
#include"strbuf.h"
11+
#include"wrapper.h"
912

1013
/*
1114
* path = absolute or relative path name
@@ -71,6 +74,62 @@ static void resolve_symlink(struct strbuf *path)
7174
strbuf_reset(&link);
7275
}
7376

77+
/*
78+
* Lock PID file functions - write PID to a .lock.pid 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+
83+
staticintlock_pid_info_enabled(void)
84+
{
85+
returngit_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);
86+
}
87+
88+
staticstructtempfile*create_lock_pid_file(constchar*lock_path, intmode)
89+
{
90+
structstrbufpid_path=STRBUF_INIT;
91+
structstrbufcontent=STRBUF_INIT;
92+
structtempfile*pid_tempfile=NULL;
93+
intfd;
94+
95+
if (!lock_pid_info_enabled())
96+
returnNULL;
97+
98+
strbuf_addf(&pid_path, "%s%s", lock_path, LOCK_PID_SUFFIX);
99+
fd=open(pid_path.buf, O_WRONLY | O_CREAT | O_TRUNC, mode);
100+
if (fd >= 0){
101+
strbuf_addf(&content, "%"PRIuMAX"\n", (uintmax_t)getpid());
102+
if (write_in_full(fd, content.buf, content.len) <0)
103+
warning_errno(_("could not write lock pid file '%s'"),
104+
pid_path.buf);
105+
close(fd);
106+
pid_tempfile=register_tempfile(pid_path.buf);
107+
}
108+
strbuf_release(&content);
109+
strbuf_release(&pid_path);
110+
returnpid_tempfile;
111+
}
112+
113+
staticintread_lock_pid(constchar*lock_path, uintmax_t*pid_out)
114+
{
115+
structstrbufpid_path=STRBUF_INIT;
116+
structstrbufcontent=STRBUF_INIT;
117+
intret=-1;
118+
119+
strbuf_addf(&pid_path, "%s%s", lock_path, LOCK_PID_SUFFIX);
120+
if (strbuf_read_file(&content, pid_path.buf, LOCK_PID_MAXLEN) >0){
121+
char*endptr;
122+
*pid_out=strtoumax(content.buf, &endptr, 10);
123+
if (*pid_out>0&& (*endptr=='\n'||*endptr=='\0'))
124+
ret=0;
125+
else
126+
warning(_("malformed lock pid file '%s'"), pid_path.buf);
127+
}
128+
strbuf_release(&pid_path);
129+
strbuf_release(&content);
130+
returnret;
131+
}
132+
74133
/* Make sure errno contains a meaningful value on error */
75134
staticintlock_file(structlock_file*lk, constchar*path, intflags,
76135
intmode)
@@ -80,9 +139,12 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,
80139
strbuf_addstr(&filename, path);
81140
if (!(flags&LOCK_NO_DEREF))
82141
resolve_symlink(&filename);
83-
84142
strbuf_addstr(&filename, LOCK_SUFFIX);
143+
85144
lk->tempfile=create_tempfile_mode(filename.buf, mode);
145+
if (lk->tempfile)
146+
lk->pid_tempfile=create_lock_pid_file(filename.buf, mode);
147+
86148
strbuf_release(&filename);
87149
returnlk->tempfile ? lk->tempfile->fd : -1;
88150
}
@@ -151,13 +213,36 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
151213
voidunable_to_lock_message(constchar*path, interr, structstrbuf*buf)
152214
{
153215
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));
216+
structstrbuflock_path=STRBUF_INIT;
217+
uintmax_tpid;
218+
intpid_status=0; /* 0 = unknown, 1 = running, -1 = stale */
219+
220+
strbuf_addf(&lock_path, "%s%s", absolute_path(path), LOCK_SUFFIX);
221+
222+
strbuf_addf(buf, _("Unable to create '%s': %s.\n\n"),
223+
lock_path.buf, strerror(err));
224+
225+
if (lock_pid_info_enabled() &&
226+
!read_lock_pid(lock_path.buf, &pid)){
227+
if (kill((pid_t)pid, 0) ==0)
228+
pid_status=1;
229+
else
230+
pid_status=-1;
231+
}
232+
233+
if (pid_status==1)
234+
strbuf_addf(buf, _("Lock is held by process %"PRIuMAX". "
235+
"Wait for it to finish, or remove the lock file to continue"),
236+
pid);
237+
elseif (pid_status==-1)
238+
strbuf_addf(buf, _("Lock was held by process %"PRIuMAX", "
239+
"which is no longer running. Remove the stale lock file to continue"),
240+
pid);
241+
else
242+
strbuf_addstr(buf, _("Another git process seems to be running in this repository. "
243+
"Wait for it to finish, or remove the lock file to continue"));
244+
245+
strbuf_release(&lock_path);
161246
} else
162247
strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
163248
absolute_path(path), strerror(err));
@@ -207,6 +292,8 @@ int commit_lock_file(struct lock_file *lk)
207292
{
208293
char*result_path=get_locked_file_path(lk);
209294

295+
delete_tempfile(&lk->pid_tempfile);
296+
210297
if (commit_lock_file_to(lk, result_path)){
211298
intsave_errno=errno;
212299
free(result_path);
@@ -216,3 +303,9 @@ int commit_lock_file(struct lock_file *lk)
216303
free(result_path);
217304
return0;
218305
}
306+
307+
introllback_lock_file(structlock_file*lk)
308+
{
309+
delete_tempfile(&lk->pid_tempfile);
310+
returndelete_tempfile(&lk->tempfile);
311+
}

‎lockfile.h‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119

120120
structlock_file{
121121
structtempfile*tempfile;
122+
structtempfile*pid_tempfile;
122123
};
123124

124125
#defineLOCK_INIT{0 }
@@ -127,6 +128,15 @@ struct lock_file{
127128
#defineLOCK_SUFFIX ".lock"
128129
#defineLOCK_SUFFIX_LEN 5
129130

131+
/* Suffix for PID file that stores PID of lock holder: */
132+
#defineLOCK_PID_SUFFIX ".pid"
133+
#defineLOCK_PID_SUFFIX_LEN 4
134+
135+
/* Maximum length for PID file content */
136+
#defineLOCK_PID_MAXLEN 32
137+
138+
/* Environment variable to enable lock PID info (default: disabled) */
139+
#defineGIT_LOCK_PID_INFO_ENVIRONMENT "GIT_LOCK_PID_INFO"
130140

131141
/*
132142
* Flags
@@ -319,13 +329,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)
319329

320330
/*
321331
* 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.
332+
* remove the lockfile and any associated PID file. It is a NOOP to
333+
* call `rollback_lock_file()` for a `lock_file` object that has already
334+
* been committed or rolled back. No error will be returned in this case.
325335
*/
326-
staticinlineintrollback_lock_file(structlock_file*lk)
327-
{
328-
returndelete_tempfile(&lk->tempfile);
329-
}
336+
introllback_lock_file(structlock_file*lk);
330337

331338
#endif/* LOCKFILE_H */

‎t/meson.build‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ integration_tests = [
9898
't0028-working-tree-encoding.sh',
9999
't0029-core-unsetenvvars.sh',
100100
't0030-stripspace.sh',
101+
't0031-lockfile-pid.sh',
101102
't0033-safe-directory.sh',
102103
't0034-root-safe-directory.sh',
103104
't0035-safe-bare-repository.sh',

‎t/t0031-lockfile-pid.sh‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 GIT_LOCK_PID_INFO=1.
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+
echo "99999" >.git/index.lock.pid &&
17+
test_must_fail env GIT_LOCK_PID_INFO=1 git add . 2>err &&
18+
test_grep "process 99999, which is no longer running" err &&
19+
test_grep "Remove the stale lock file" 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+
echo "99999" >.git/index.lock.pid &&
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+
# Create a lock and PID file with current shell PID (which is running)
42+
touch .git/index.lock &&
43+
echo $$ >.git/index.lock.pid &&
44+
# Verify our PID is shown in the error message
45+
test_must_fail env GIT_LOCK_PID_INFO=1 git add file 2>err &&
46+
test_grep "held by process $$" err
47+
)
48+
'
49+
50+
test_expect_success 'PID info file cleaned up on successful operation when enabled''
51+
git init repo4 &&
52+
(
53+
cd repo4 &&
54+
echo content >file &&
55+
env GIT_LOCK_PID_INFO=1 git add file &&
56+
# After successful add, no lock or PID files should exist
57+
! test -f .git/index.lock &&
58+
! test -f .git/index.lock.pid
59+
)
60+
'
61+
62+
test_expect_success 'no PID file created by default''
63+
git init repo5 &&
64+
(
65+
cd repo5 &&
66+
echo content >file &&
67+
git add file &&
68+
# PID file should not be created when feature is disabled
69+
! test -f .git/index.lock.pid
70+
)
71+
'
72+
73+
test_done

0 commit comments

Comments
(0)