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+
static int lock_pid_info_enabled(void)
84+
{
85+
return git_env_bool(GIT_LOCK_PID_INFO_ENVIRONMENT, 0);
86+
}
87+
88+
static struct tempfile *create_lock_pid_file(const char *lock_path, int mode)
89+
{
90+
struct strbuf pid_path = STRBUF_INIT;
91+
struct strbuf content = STRBUF_INIT;
92+
struct tempfile *pid_tempfile = NULL;
93+
int fd;
94+
95+
if (!lock_pid_info_enabled())
96+
return NULL;
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+
return pid_tempfile;
111+
}
112+
113+
static int read_lock_pid(const char *lock_path, uintmax_t *pid_out)
114+
{
115+
struct strbuf pid_path = STRBUF_INIT;
116+
struct strbuf content = STRBUF_INIT;
117+
int ret = -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+
return ret;
131+
}
132+
74133
/* Make sure errno contains a meaningful value on error */
75134
static int lock_file(struct lock_file *lk, const char *path, int flags,
76135
int mode)
@@ -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
return lk->tempfile ? lk->tempfile->fd : -1;
88150
}
@@ -151,13 +213,36 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
151213
void unable_to_lock_message(const char *path, int err, struct strbuf *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+
struct strbuf lock_path = STRBUF_INIT;
217+
uintmax_t pid;
218+
int pid_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+
else if (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
int save_errno = errno;
212299
free(result_path);
@@ -216,3 +303,9 @@ int commit_lock_file(struct lock_file *lk)
216303
free(result_path);
217304
return 0;
218305
}
306+
307+
int rollback_lock_file(struct lock_file *lk)
308+
{
309+
delete_tempfile(&lk->pid_tempfile);
310+
return delete_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
struct lock_file {
121121
struct tempfile *tempfile;
122+
struct tempfile *pid_tempfile;
122123
};
123124

124125
#define LOCK_INIT { 0 }
@@ -127,6 +128,15 @@ struct lock_file {
127128
#define LOCK_SUFFIX ".lock"
128129
#define LOCK_SUFFIX_LEN 5
129130

131+
/* Suffix for PID file that stores PID of lock holder: */
132+
#define LOCK_PID_SUFFIX ".pid"
133+
#define LOCK_PID_SUFFIX_LEN 4
134+
135+
/* Maximum length for PID file content */
136+
#define LOCK_PID_MAXLEN 32
137+
138+
/* Environment variable to enable lock PID info (default: disabled) */
139+
#define GIT_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-
static inline int rollback_lock_file(struct lock_file *lk)
327-
{
328-
return delete_tempfile(&lk->tempfile);
329-
}
336+
int rollback_lock_file(struct lock_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)