Skip to content

Commit 5452ae9

Browse files
committed
lockfile: add holder info file for debugging stale locks
When a lock file is left behind (e.g., after a crash or SIGKILL), the error message tells users to remove it manually but provides no information about which process created it. This makes debugging difficult, especially when multiple Git processes may be running. Add an opt-in feature that creates a `.holder` file alongside each lock file containing the PID of the process that created the lock. When enabled via GIT_LOCK_HOLDER_INFO=1, this PID is displayed in lock conflict error messages, making it easier to identify stale locks or debug locking issues. The holder file is: - Created only when GIT_LOCK_HOLDER_INFO=1 is set (opt-in) - Registered as a tempfile for automatic cleanup by signal handlers - Stored separately from the lock file to maintain backward compatibility with existing tools that parse lock file contents Example error message with the feature enabled: Unable to create '/path/to/.git/index.lock': File exists. Lock is held by process 12345. Another git process seems to be running in this repository... Signed-off-by: Paulo Casaretto <[email protected]>
1 parent 6ab38b7 commit 5452ae9

File tree

5 files changed

+176
-12
lines changed

5 files changed

+176
-12
lines changed

Documentation/git.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,13 @@ 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_HOLDER_INFO`::
1014+
If this Boolean environment variable is set to `1`, Git will create
1015+
a `.holder` 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. Disabled by default.
1019+
10131020
`GIT_REDIRECT_STDIN`::
10141021
`GIT_REDIRECT_STDOUT`::
10151022
`GIT_REDIRECT_STDERR`::

lockfile.c

Lines changed: 85 additions & 5 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,59 @@ static void resolve_symlink(struct strbuf *path)
7174
strbuf_reset(&link);
7275
}
7376

77+
/*
78+
* Lock holder info functions - write PID to a .holder file alongside
79+
* the lock file for debugging stale locks. The holder file is registered
80+
* as a tempfile so it gets cleaned up by signal/atexit handlers.
81+
*/
82+
83+
static int lock_holder_info_enabled(void)
84+
{
85+
return git_env_bool(GIT_LOCK_HOLDER_INFO_ENVIRONMENT, 0);
86+
}
87+
88+
static struct tempfile *create_lock_holder_file(const char *lock_path, int mode)
89+
{
90+
struct strbuf holder_path = STRBUF_INIT;
91+
struct strbuf content = STRBUF_INIT;
92+
struct tempfile *holder_tempfile = NULL;
93+
int fd;
94+
95+
if (!lock_holder_info_enabled())
96+
return NULL;
97+
98+
strbuf_addf(&holder_path, "%s%s", lock_path, LOCK_HOLDER_SUFFIX);
99+
fd = open(holder_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 holder file '%s'"),
104+
holder_path.buf);
105+
close(fd);
106+
holder_tempfile = register_tempfile(holder_path.buf);
107+
}
108+
strbuf_release(&content);
109+
strbuf_release(&holder_path);
110+
return holder_tempfile;
111+
}
112+
113+
static int read_lock_holder_pid(const char *lock_path, uintmax_t *pid_out)
114+
{
115+
struct strbuf holder_path = STRBUF_INIT;
116+
struct strbuf content = STRBUF_INIT;
117+
int ret = -1;
118+
119+
strbuf_addf(&holder_path, "%s%s", lock_path, LOCK_HOLDER_SUFFIX);
120+
if (strbuf_read_file(&content, holder_path.buf, 32) > 0) {
121+
*pid_out = strtoumax(content.buf, NULL, 10);
122+
if (*pid_out > 0)
123+
ret = 0;
124+
}
125+
strbuf_release(&holder_path);
126+
strbuf_release(&content);
127+
return ret;
128+
}
129+
74130
/* Make sure errno contains a meaningful value on error */
75131
static int lock_file(struct lock_file *lk, const char *path, int flags,
76132
int mode)
@@ -80,9 +136,12 @@ static int lock_file(struct lock_file *lk, const char *path, int flags,
80136
strbuf_addstr(&filename, path);
81137
if (!(flags & LOCK_NO_DEREF))
82138
resolve_symlink(&filename);
83-
84139
strbuf_addstr(&filename, LOCK_SUFFIX);
140+
85141
lk->tempfile = create_tempfile_mode(filename.buf, mode);
142+
if (lk->tempfile)
143+
lk->holder_tempfile = create_lock_holder_file(filename.buf, mode);
144+
86145
strbuf_release(&filename);
87146
return lk->tempfile ? lk->tempfile->fd : -1;
88147
}
@@ -151,13 +210,26 @@ static int lock_file_timeout(struct lock_file *lk, const char *path,
151210
void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
152211
{
153212
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"
213+
struct strbuf lock_path = STRBUF_INIT;
214+
uintmax_t holder_pid;
215+
216+
strbuf_addf(&lock_path, "%s%s", absolute_path(path), LOCK_SUFFIX);
217+
218+
strbuf_addf(buf, _("Unable to create '%s': %s.\n\n"),
219+
lock_path.buf, strerror(err));
220+
221+
if (lock_holder_info_enabled() &&
222+
!read_lock_holder_pid(lock_path.buf, &holder_pid))
223+
strbuf_addf(buf, _("Lock is held by process %"PRIuMAX".\n\n"),
224+
holder_pid);
225+
226+
strbuf_addstr(buf, _("Another git process seems to be running in this repository, e.g.\n"
156227
"an editor opened by 'git commit'. Please make sure all processes\n"
157228
"are terminated then try again. If it still fails, a git process\n"
158229
"may have crashed in this repository earlier:\n"
159-
"remove the file manually to continue."),
160-
absolute_path(path), strerror(err));
230+
"remove the file manually to continue."));
231+
232+
strbuf_release(&lock_path);
161233
} else
162234
strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
163235
absolute_path(path), strerror(err));
@@ -207,6 +279,8 @@ int commit_lock_file(struct lock_file *lk)
207279
{
208280
char *result_path = get_locked_file_path(lk);
209281

282+
delete_tempfile(&lk->holder_tempfile);
283+
210284
if (commit_lock_file_to(lk, result_path)) {
211285
int save_errno = errno;
212286
free(result_path);
@@ -216,3 +290,9 @@ int commit_lock_file(struct lock_file *lk)
216290
free(result_path);
217291
return 0;
218292
}
293+
294+
int rollback_lock_file(struct lock_file *lk)
295+
{
296+
delete_tempfile(&lk->holder_tempfile);
297+
return delete_tempfile(&lk->tempfile);
298+
}

lockfile.h

Lines changed: 11 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 *holder_tempfile;
122123
};
123124

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

131+
/* Suffix for holder info file that stores PID of lock holder: */
132+
#define LOCK_HOLDER_SUFFIX ".holder"
133+
#define LOCK_HOLDER_SUFFIX_LEN 7
134+
135+
/* Environment variable to enable lock holder info (default: disabled) */
136+
#define GIT_LOCK_HOLDER_INFO_ENVIRONMENT "GIT_LOCK_HOLDER_INFO"
130137

131138
/*
132139
* Flags
@@ -319,13 +326,10 @@ static inline int commit_lock_file_to(struct lock_file *lk, const char *path)
319326

320327
/*
321328
* 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.
329+
* remove the lockfile and any associated holder file. It is a NOOP to
330+
* call `rollback_lock_file()` for a `lock_file` object that has already
331+
* been committed or rolled back. No error will be returned in this case.
325332
*/
326-
static inline int rollback_lock_file(struct lock_file *lk)
327-
{
328-
return delete_tempfile(&lk->tempfile);
329-
}
333+
int rollback_lock_file(struct lock_file *lk);
330334

331335
#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-holder.sh',
101102
't0033-safe-directory.sh',
102103
't0034-root-safe-directory.sh',
103104
't0035-safe-bare-repository.sh',

t/t0031-lockfile-holder.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/sh
2+
3+
test_description='lock file holder info tests
4+
5+
Tests for PID holder info file alongside lock files.
6+
The feature is opt-in via GIT_LOCK_HOLDER_INFO=1.
7+
'
8+
9+
. ./test-lib.sh
10+
11+
test_expect_success 'holder info shown in lock error message when enabled' '
12+
git init repo &&
13+
(
14+
cd repo &&
15+
touch .git/index.lock &&
16+
echo "99999" >.git/index.lock.holder &&
17+
test_must_fail env GIT_LOCK_HOLDER_INFO=1 git add . 2>err &&
18+
test_grep "Lock is held by process 99999" err
19+
)
20+
'
21+
22+
test_expect_success 'holder info not shown by default' '
23+
git init repo2 &&
24+
(
25+
cd repo2 &&
26+
touch .git/index.lock &&
27+
echo "99999" >.git/index.lock.holder &&
28+
test_must_fail git add . 2>err &&
29+
# Should not crash, just show normal error without PID
30+
test_grep "Unable to create" err &&
31+
! test_grep "Lock is held by process" err
32+
)
33+
'
34+
35+
test_expect_success 'holder file created with correct PID during lock' '
36+
git init repo3 &&
37+
(
38+
cd repo3 &&
39+
echo content >file &&
40+
# Create a lock and holder file manually to simulate a holding process
41+
touch .git/index.lock &&
42+
echo $$ >.git/index.lock.holder &&
43+
# Verify our PID is shown in the error message
44+
test_must_fail env GIT_LOCK_HOLDER_INFO=1 git add file 2>err &&
45+
test_grep "Lock is held by process $$" err
46+
)
47+
'
48+
49+
test_expect_success 'holder info file cleaned up on successful operation when enabled' '
50+
git init repo4 &&
51+
(
52+
cd repo4 &&
53+
echo content >file &&
54+
env GIT_LOCK_HOLDER_INFO=1 git add file &&
55+
# After successful add, no lock or holder files should exist
56+
! test -f .git/index.lock &&
57+
! test -f .git/index.lock.holder
58+
)
59+
'
60+
61+
test_expect_success 'no holder file created by default' '
62+
git init repo5 &&
63+
(
64+
cd repo5 &&
65+
echo content >file &&
66+
git add file &&
67+
# Holder file should not be created when feature is disabled
68+
! test -f .git/index.lock.holder
69+
)
70+
'
71+
72+
test_done

0 commit comments

Comments
 (0)