read-only snapshot of the same index to a location different from the
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::
Enable the reflog. Updates to a ref <ref> is logged to the file
"`$GIT_DIR/logs/<ref>`", by appending the new and old
path = repo_git_path(the_repository, "next-index-%"PRIuMAX,
(uintmax_t) getpid());
- hold_lock_file_for_update(&false_lock, path,
- LOCK_DIE_ON_ERROR);
+ hold_lock_file_for_update(&false_lock, path, LOCK_DIE_ON_ERROR);
create_base_index(current_head);
add_remove_files(&partial);
xsnprintf(my_host, sizeof(my_host), "unknown");
pidfile_path = repo_git_path(the_repository, "gc.pid");
- fd = hold_lock_file_for_update(&lock, pidfile_path,
- LOCK_DIE_ON_ERROR);
+ fd = hold_lock_file_for_update(&lock, pidfile_path, LOCK_DIE_ON_ERROR);
if (!force) {
static char locking_host[HOST_NAME_MAX + 1];
static char *scan_fmt;
if (daemonized) {
char *path = repo_git_path(the_repository, "gc.log");
- hold_lock_file_for_update(&log_lock, path,
- LOCK_DIE_ON_ERROR);
+ hold_lock_file_for_update(&log_lock, path, LOCK_DIE_ON_ERROR);
dup2(get_lock_file_fd(&log_lock), 2);
atexit(process_log_file_at_exit);
free(path);
CloseHandle(h);
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;
#include "gettext.h"
#include "git-zlib.h"
#include "ident.h"
+#include "lockfile.h"
#include "mailmap.h"
#include "object-name.h"
#include "repository.h"
return 0;
}
+ if (!strcmp(var, "core.lockfilepid")) {
+ lockfile_pid_enabled = git_config_bool(var, value);
+ return 0;
+ }
+
if (!strcmp(var, "core.createobject")) {
if (!value)
return config_error_nonbool(var);
#include "abspath.h"
#include "gettext.h"
#include "lockfile.h"
+#include "parse.h"
+#include "strbuf.h"
+#include "wrapper.h"
/*
* path = absolute or relative path name
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 = -1;
+
+ 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);
+ fd = -1;
+ unlink(pid_path);
+ goto out;
+ }
+
+ close(fd);
+ fd = -1;
+ pid_tempfile = register_tempfile(pid_path);
+
+out:
+ if (fd >= 0)
+ close(fd);
+ 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 */
static int lock_file(struct lock_file *lk, const char *path, int flags,
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))
- resolve_symlink(&filename);
+ resolve_symlink(&base_path);
+
+ get_lock_path(&lock_path, base_path.buf);
+ get_pid_path(&pid_path, base_path.buf);
+
+ lk->tempfile = create_tempfile_mode(lock_path.buf, mode);
+ if (lk->tempfile)
+ lk->pid_tempfile = create_lock_pid_file(pid_path.buf, mode);
- strbuf_addstr(&filename, LOCK_SUFFIX);
- lk->tempfile = create_tempfile_mode(filename.buf, mode);
- strbuf_release(&filename);
+ strbuf_release(&base_path);
+ strbuf_release(&lock_path);
+ strbuf_release(&pid_path);
return lk->tempfile ? lk->tempfile->fd : -1;
}
void unable_to_lock_message(const char *path, int err, struct strbuf *buf)
{
if (err == EEXIST) {
- strbuf_addf(buf, _("Unable to create '%s.lock': %s.\n\n"
- "Another git process seems to be running in this repository, e.g.\n"
- "an editor opened by 'git commit'. Please make sure all processes\n"
- "are terminated then try again. If it still fails, a git process\n"
- "may have crashed in this repository earlier:\n"
- "remove the file manually to continue."),
- absolute_path(path), strerror(err));
- } else
+ const char *abs_path = absolute_path(path);
+ struct strbuf lock_path = STRBUF_INIT;
+ struct strbuf pid_path = STRBUF_INIT;
+ uintmax_t pid;
+ int pid_status = 0; /* 0 = unknown, 1 = running, -1 = stale */
+
+ get_lock_path(&lock_path, abs_path);
+ 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 is held by process %" PRIuMAX ". "
+ "Wait for it to finish, or remove the lock file to continue"),
+ pid);
+ else if (pid_status == -1)
+ strbuf_addf(buf, _("Lock was held by process %" PRIuMAX ", "
+ "which is no longer running. Remove the stale lock file to continue"),
+ pid);
+ else
+ strbuf_addstr(buf, _("Another git process seems to be running in this repository. "
+ "Wait for it to finish, or remove the lock file to continue"));
+
+ strbuf_release(&lock_path);
+ strbuf_release(&pid_path);
+ } else {
strbuf_addf(buf, _("Unable to create '%s.lock': %s"),
absolute_path(path), strerror(err));
+ }
}
NORETURN void unable_to_lock_die(const char *path, int err)
{
char *result_path = get_locked_file_path(lk);
+ delete_tempfile(&lk->pid_tempfile);
+
if (commit_lock_file_to(lk, result_path)) {
int save_errno = errno;
free(result_path);
free(result_path);
return 0;
}
+
+int rollback_lock_file(struct lock_file *lk)
+{
+ delete_tempfile(&lk->pid_tempfile);
+ return delete_tempfile(&lk->tempfile);
+}
struct lock_file {
struct tempfile *tempfile;
+ struct tempfile *pid_tempfile;
};
#define LOCK_INIT { 0 }
#define LOCK_SUFFIX ".lock"
#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
* handling, and mode are described above.
*/
int hold_lock_file_for_update_timeout_mode(
- struct lock_file *lk, const char *path,
- int flags, long timeout_ms, int mode);
+ struct lock_file *lk, const char *path,
+ int flags, long timeout_ms, int mode);
static inline int hold_lock_file_for_update_timeout(
- struct lock_file *lk, const char *path,
- int flags, long timeout_ms)
+ struct lock_file *lk, const char *path,
+ int flags, long timeout_ms)
{
return hold_lock_file_for_update_timeout_mode(lk, path, flags,
timeout_ms, 0666);
* argument and error handling are described above.
*/
static inline int hold_lock_file_for_update(
- struct lock_file *lk, const char *path,
- int flags)
+ struct lock_file *lk, const char *path, int flags)
{
return hold_lock_file_for_update_timeout(lk, path, flags, 0);
}
static inline int hold_lock_file_for_update_mode(
- struct lock_file *lk, const char *path,
- int flags, int mode)
+ struct lock_file *lk, const char *path,
+ int flags, int mode)
{
return hold_lock_file_for_update_timeout_mode(lk, path, flags, 0, mode);
}
/*
* Roll back `lk`: close the file descriptor and/or file pointer and
- * remove the lockfile. It is a NOOP to call `rollback_lock_file()`
- * for a `lock_file` object that has already been committed or rolled
- * back. No error will be returned in this case.
+ * remove the lockfile and any associated PID file. It is a NOOP to
+ * call `rollback_lock_file()` for a `lock_file` object that has already
+ * been committed or rolled back. No error will be returned in this case.
*/
-static inline int rollback_lock_file(struct lock_file *lk)
-{
- return delete_tempfile(&lk->tempfile);
-}
+int rollback_lock_file(struct lock_file *lk);
#endif /* LOCKFILE_H */
't0028-working-tree-encoding.sh',
't0029-core-unsetenvvars.sh',
't0030-stripspace.sh',
+ 't0031-lockfile-pid.sh',
't0033-safe-directory.sh',
't0034-root-safe-directory.sh',
't0035-safe-bare-repository.sh',
--- /dev/null
+#!/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 "Remove the stale lock file" 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