Merge branch 'pc/lockfile-pid'

Allow recording process ID of the process that holds the lock next
to a lockfile for diagnosis.

* pc/lockfile-pid:
  lockfile: add PID file for debugging stale locks
main
Junio C Hamano 2026-02-17 13:30:41 -08:00
commit 5779c47fa0
7 changed files with 315 additions and 29 deletions

View File

@ -348,6 +348,17 @@ confusion unless you know what you are doing (e.g. you are creating a
read-only snapshot of the same index to a location different from the read-only snapshot of the same index to a location different from the
repository's usual working tree). repository's usual working tree).


core.lockfilePid::
If true, Git will create a PID file alongside lock files. When a
lock acquisition fails and a PID file exists, Git can provide
additional diagnostic information about the process holding the
lock, including whether it is still running. Defaults to `false`.
+
The PID file is named by inserting `~pid` before the `.lock` suffix.
For example, if the lock file is `index.lock`, the PID file will be
`index~pid.lock`. The file contains a single line in the format
`pid <value>` followed by a newline.

core.logAllRefUpdates:: core.logAllRefUpdates::
Enable the reflog. Updates to a ref <ref> is logged to the file Enable the reflog. Updates to a ref <ref> is logged to the file
"`$GIT_DIR/logs/<ref>`", by appending the new and old "`$GIT_DIR/logs/<ref>`", by appending the new and old

View File

@ -2220,6 +2220,16 @@ int mingw_kill(pid_t pid, int sig)
CloseHandle(h); CloseHandle(h);
return 0; return 0;
} }
/*
* OpenProcess returns ERROR_INVALID_PARAMETER for
* non-existent PIDs. Map this to ESRCH for POSIX
* compatibility with kill(pid, 0).
*/
if (GetLastError() == ERROR_INVALID_PARAMETER)
errno = ESRCH;
else
errno = err_win_to_posix(GetLastError());
return -1;
} }


errno = EINVAL; errno = EINVAL;

View File

@ -21,6 +21,7 @@
#include "gettext.h" #include "gettext.h"
#include "git-zlib.h" #include "git-zlib.h"
#include "ident.h" #include "ident.h"
#include "lockfile.h"
#include "mailmap.h" #include "mailmap.h"
#include "object-name.h" #include "object-name.h"
#include "repository.h" #include "repository.h"
@ -508,6 +509,11 @@ int git_default_core_config(const char *var, const char *value,
return 0; return 0;
} }


if (!strcmp(var, "core.lockfilepid")) {
lockfile_pid_enabled = git_config_bool(var, value);
return 0;
}

if (!strcmp(var, "core.createobject")) { if (!strcmp(var, "core.createobject")) {
if (!value) if (!value)
return config_error_nonbool(var); return config_error_nonbool(var);

View File

@ -6,6 +6,9 @@
#include "abspath.h" #include "abspath.h"
#include "gettext.h" #include "gettext.h"
#include "lockfile.h" #include "lockfile.h"
#include "parse.h"
#include "strbuf.h"
#include "wrapper.h"


/* /*
* path = absolute or relative path name * path = absolute or relative path name
@ -71,19 +74,115 @@ static void resolve_symlink(struct strbuf *path)
strbuf_reset(&link); strbuf_reset(&link);
} }


/*
* Lock PID file functions - write PID to a foo~pid.lock 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.
*
* Naming: For "foo.lock", the PID file is "foo~pid.lock". The tilde is
* forbidden in refnames and allowed in Windows filenames, guaranteeing
* no collision with the refs namespace.
*/

/* Global config variable, initialized from core.lockfilePid */
int lockfile_pid_enabled;

/*
* Path generation helpers.
* Given base path "foo", generate:
* - lock path: "foo.lock"
* - pid path: "foo-pid.lock"
*/
static void get_lock_path(struct strbuf *out, const char *path)
{
strbuf_addstr(out, path);
strbuf_addstr(out, LOCK_SUFFIX);
}

static void get_pid_path(struct strbuf *out, const char *path)
{
strbuf_addstr(out, path);
strbuf_addstr(out, LOCK_PID_INFIX);
strbuf_addstr(out, LOCK_SUFFIX);
}

static struct tempfile *create_lock_pid_file(const char *pid_path, int mode)
{
struct strbuf content = STRBUF_INIT;
struct tempfile *pid_tempfile = NULL;
int fd;

if (!lockfile_pid_enabled)
goto out;

fd = open(pid_path, O_WRONLY | O_CREAT | O_EXCL, mode);
if (fd < 0)
goto out;

strbuf_addf(&content, "pid %" 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);
close(fd);
unlink(pid_path);
goto out;
}

close(fd);
pid_tempfile = register_tempfile(pid_path);

out:
strbuf_release(&content);
return pid_tempfile;
}

static int read_lock_pid(const char *pid_path, uintmax_t *pid_out)
{
struct strbuf content = STRBUF_INIT;
const char *val;
int ret = -1;

if (strbuf_read_file(&content, pid_path, LOCK_PID_MAXLEN) <= 0)
goto out;

strbuf_rtrim(&content);

if (skip_prefix(content.buf, "pid ", &val)) {
char *endptr;
*pid_out = strtoumax(val, &endptr, 10);
if (*pid_out > 0 && !*endptr)
ret = 0;
}

if (ret)
warning(_("malformed lock pid file '%s'"), pid_path);

out:
strbuf_release(&content);
return ret;
}

/* Make sure errno contains a meaningful value on error */ /* Make sure errno contains a meaningful value on error */
static int lock_file(struct lock_file *lk, const char *path, int flags, static int lock_file(struct lock_file *lk, const char *path, int flags,
int mode) int mode)
{ {
struct strbuf filename = STRBUF_INIT; struct strbuf base_path = STRBUF_INIT;
struct strbuf lock_path = STRBUF_INIT;
struct strbuf pid_path = STRBUF_INIT;


strbuf_addstr(&filename, path); strbuf_addstr(&base_path, path);
if (!(flags & LOCK_NO_DEREF)) if (!(flags & LOCK_NO_DEREF))
resolve_symlink(&filename); resolve_symlink(&base_path);


strbuf_addstr(&filename, LOCK_SUFFIX); get_lock_path(&lock_path, base_path.buf);
lk->tempfile = create_tempfile_mode(filename.buf, mode); get_pid_path(&pid_path, base_path.buf);
strbuf_release(&filename);
lk->tempfile = create_tempfile_mode(lock_path.buf, mode);
if (lk->tempfile)
lk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);

strbuf_release(&base_path);
strbuf_release(&lock_path);
strbuf_release(&pid_path);
return lk->tempfile ? lk->tempfile->fd : -1; return lk->tempfile ? lk->tempfile->fd : -1;
} }


@ -151,16 +250,49 @@ 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) void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
{ {
if (err == EEXIST) { if (err == EEXIST) {
strbuf_addf(buf, _("Unable to create '%s.lock': %s.\n\n" const char *abs_path = absolute_path(path);
"Another git process seems to be running in this repository, e.g.\n" struct strbuf lock_path = STRBUF_INIT;
"an editor opened by 'git commit'. Please make sure all processes\n" struct strbuf pid_path = STRBUF_INIT;
"are terminated then try again. If it still fails, a git process\n" uintmax_t pid;
"may have crashed in this repository earlier:\n" int pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */
"remove the file manually to continue."),
absolute_path(path), strerror(err)); get_lock_path(&lock_path, abs_path);
} else get_pid_path(&pid_path, abs_path);

strbuf_addf(buf, _("Unable to create '%s': %s.\n\n"),
lock_path.buf, strerror(err));

/*
* Try to read PID file unconditionally - it may exist if
* core.lockfilePid was enabled.
*/
if (!read_lock_pid(pid_path.buf, &pid)) {
if (kill((pid_t)pid, 0) == 0 || errno == EPERM)
pid_status = 1; /* running (or no permission to signal) */
else if (errno == ESRCH)
pid_status = -1; /* no such process - stale lock */
}

if (pid_status == 1)
strbuf_addf(buf, _("Lock may be held by process %" PRIuMAX "; "
"if no git process is running, the lock file "
"may be stale (PIDs can be reused)"),
pid);
else if (pid_status == -1)
strbuf_addf(buf, _("Lock was held by process %" PRIuMAX ", "
"which is no longer running; the lock file "
"appears to be stale"),
pid);
else
strbuf_addstr(buf, _("Another git process seems to be running in this repository, "
"or the lock file may be stale"));

strbuf_release(&lock_path);
strbuf_release(&pid_path);
} else {
strbuf_addf(buf, _("Unable to create '%s.lock': %s"), strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
absolute_path(path), strerror(err)); absolute_path(path), strerror(err));
}
} }


NORETURN void unable_to_lock_die(const char *path, int err) NORETURN void unable_to_lock_die(const char *path, int err)
@ -207,6 +339,8 @@ int commit_lock_file(struct lock_file *lk)
{ {
char *result_path = get_locked_file_path(lk); char *result_path = get_locked_file_path(lk);


delete_tempfile(&lk->pid_tempfile);

if (commit_lock_file_to(lk, result_path)) { if (commit_lock_file_to(lk, result_path)) {
int save_errno = errno; int save_errno = errno;
free(result_path); free(result_path);
@ -216,3 +350,9 @@ int commit_lock_file(struct lock_file *lk)
free(result_path); free(result_path);
return 0; return 0;
} }

int rollback_lock_file(struct lock_file *lk)
{
delete_tempfile(&lk->pid_tempfile);
return delete_tempfile(&lk->tempfile);
}

View File

@ -119,6 +119,7 @@


struct lock_file { struct lock_file {
struct tempfile *tempfile; struct tempfile *tempfile;
struct tempfile *pid_tempfile;
}; };


#define LOCK_INIT { 0 } #define LOCK_INIT { 0 }
@ -127,6 +128,22 @@ struct lock_file {
#define LOCK_SUFFIX ".lock" #define LOCK_SUFFIX ".lock"
#define LOCK_SUFFIX_LEN 5 #define LOCK_SUFFIX_LEN 5


/*
* PID file naming: for a lock file "foo.lock", the PID file is "foo~pid.lock".
* The tilde is forbidden in refnames and allowed in Windows filenames, avoiding
* namespace collisions (e.g., refs "foo" and "foo~pid" cannot both exist).
*/
#define LOCK_PID_INFIX "~pid"
#define LOCK_PID_INFIX_LEN 4

/* Maximum length for PID file content */
#define LOCK_PID_MAXLEN 32

/*
* Whether to create PID files alongside lock files.
* Configured via core.lockfilePid (boolean).
*/
extern int lockfile_pid_enabled;


/* /*
* Flags * Flags
@ -169,12 +186,12 @@ struct lock_file {
* handling, and mode are described above. * handling, and mode are described above.
*/ */
int hold_lock_file_for_update_timeout_mode( int hold_lock_file_for_update_timeout_mode(
struct lock_file *lk, const char *path, struct lock_file *lk, const char *path,
int flags, long timeout_ms, int mode); int flags, long timeout_ms, int mode);


static inline int hold_lock_file_for_update_timeout( static inline int hold_lock_file_for_update_timeout(
struct lock_file *lk, const char *path, struct lock_file *lk, const char *path,
int flags, long timeout_ms) int flags, long timeout_ms)
{ {
return hold_lock_file_for_update_timeout_mode(lk, path, flags, return hold_lock_file_for_update_timeout_mode(lk, path, flags,
timeout_ms, 0666); timeout_ms, 0666);
@ -186,15 +203,14 @@ static inline int hold_lock_file_for_update_timeout(
* argument and error handling are described above. * argument and error handling are described above.
*/ */
static inline int hold_lock_file_for_update( static inline int hold_lock_file_for_update(
struct lock_file *lk, const char *path, struct lock_file *lk, const char *path, int flags)
int flags)
{ {
return hold_lock_file_for_update_timeout(lk, path, flags, 0); return hold_lock_file_for_update_timeout(lk, path, flags, 0);
} }


static inline int hold_lock_file_for_update_mode( static inline int hold_lock_file_for_update_mode(
struct lock_file *lk, const char *path, struct lock_file *lk, const char *path,
int flags, int mode) int flags, int mode)
{ {
return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode); return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);
} }
@ -319,13 +335,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 * Roll back `lk`: close the file descriptor and/or file pointer and
* remove the lockfile. It is a NOOP to call `rollback_lock_file()` * remove the lockfile and any associated PID file. It is a NOOP to
* for a `lock_file` object that has already been committed or rolled * call `rollback_lock_file()` for a `lock_file` object that has already
* back. No error will be returned in this case. * been committed or rolled back. No error will be returned in this case.
*/ */
static inline int rollback_lock_file(struct lock_file *lk) int rollback_lock_file(struct lock_file *lk);
{
return delete_tempfile(&lk->tempfile);
}


#endif /* LOCKFILE_H */ #endif /* LOCKFILE_H */

View File

@ -98,6 +98,7 @@ integration_tests = [
't0028-working-tree-encoding.sh', 't0028-working-tree-encoding.sh',
't0029-core-unsetenvvars.sh', 't0029-core-unsetenvvars.sh',
't0030-stripspace.sh', 't0030-stripspace.sh',
't0031-lockfile-pid.sh',
't0033-safe-directory.sh', 't0033-safe-directory.sh',
't0034-root-safe-directory.sh', 't0034-root-safe-directory.sh',
't0035-safe-bare-repository.sh', 't0035-safe-bare-repository.sh',

105
t/t0031-lockfile-pid.sh Executable file
View File

@ -0,0 +1,105 @@
#!/bin/sh

test_description='lock file PID info tests

Tests for PID info file alongside lock files.
The feature is opt-in via core.lockfilePid config setting (boolean).
'

. ./test-lib.sh

test_expect_success 'stale lock detected when PID is not running' '
git init repo &&
(
cd repo &&
touch .git/index.lock &&
printf "pid 99999" >.git/index~pid.lock &&
test_must_fail git -c core.lockfilePid=true add . 2>err &&
test_grep "process 99999, which is no longer running" err &&
test_grep "appears to be stale" err
)
'

test_expect_success 'PID info not shown by default' '
git init repo2 &&
(
cd repo2 &&
touch .git/index.lock &&
printf "pid 99999" >.git/index~pid.lock &&
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 &&
# Get the correct PID for this platform
shell_pid=$$ &&
if test_have_prereq MINGW && test -f /proc/$shell_pid/winpid
then
# In Git for Windows, Bash uses MSYS2 PIDs but git.exe
# uses Windows PIDs. Use the Windows PID.
shell_pid=$(cat /proc/$shell_pid/winpid)
fi &&
# Create a lock and PID file with current shell PID (which is running)
touch .git/index.lock &&
printf "pid %d" "$shell_pid" >.git/index~pid.lock &&
# Verify our PID is shown in the error message
test_must_fail git -c core.lockfilePid=true add file 2>err &&
test_grep "held by process $shell_pid" err
)
'

test_expect_success 'PID info file cleaned up on successful operation when enabled' '
git init repo4 &&
(
cd repo4 &&
echo content >file &&
git -c core.lockfilePid=true add file &&
# After successful add, no lock or PID files should exist
test_path_is_missing .git/index.lock &&
test_path_is_missing .git/index~pid.lock
)
'

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_path_is_missing .git/index~pid.lock
)
'

test_expect_success 'core.lockfilePid=false does not create PID file' '
git init repo6 &&
(
cd repo6 &&
echo content >file &&
git -c core.lockfilePid=false add file &&
# PID file should not be created when feature is disabled
test_path_is_missing .git/index~pid.lock
)
'

test_expect_success 'existing PID files are read even when feature disabled' '
git init repo7 &&
(
cd repo7 &&
touch .git/index.lock &&
printf "pid 99999" >.git/index~pid.lock &&
# Even with lockfilePid disabled, existing PID files are read
# to help diagnose stale locks
test_must_fail git add . 2>err &&
test_grep "process 99999" err
)
'

test_done