Skip to main content

Module lock

Module lock 

Source
Expand description

Locks and their owners: the index lock, stale reference locks, and the claims that let one command work on a worktree. What: Locks and their owners: the index lock, stale reference locks, and the claims that let one command work on a worktree. Why: Every lock question has one answer in one place.

In TS you’d write (pseudocode):

// lock/index.ts: one `export * from './file'` per module below.

Modules§

forwarded_lock_notice
The notice a forwarded fetch or push is preceded by when reference locks of a killed Git are found. What: The clearing of stale reference locks for the person’s own git fetch and git push, and the notice that goes before Git’s own words. Why: A forwarded plain fetch or push replaces the wrapper’s process with Git, so there is no “after” to look at: the wrapper can only judge the locks before it hands over. Locks it removes are said, and locks it will not remove are described with the command that clears them, so the notice precedes Git’s refusal instead of leaving the person with “File exists” alone. The walk reads directory entries of refs and two files, and starts no Git.
index_holder_wait
The wait for any program’s hold on index.lock before a command changes the checkout. What: The wait of a command that is about to change the checkout for any program’s hold on Git’s index.lock: the commit service’s, a foreign git’s, or a lock nothing proves held. Event-driven, with a notice that names what is waited for and the holder, and over as soon as the lock is gone. A holder proven alive and an owner that cannot be proven alive are both waited for with no bound, because the person’s own git commit with its editor open holds the lock with no proof; the line for the second says what is known and how to remove the lock if no program uses the repository. A PID file naming a process that is gone ends the wait at once with a diagnostic. Why: Native Git never waits for index.lock: a merge that finds it held dies (Unable to create ... index.lock or, from the stash that every merge commit takes first, could not write index and stash failed). Other sessions commit into a shared checkout all the time, so a held lock is ordinary and passes by itself. This wait is the sibling of the wait for the commit service’s own hold (service_index_wait.rs), which returns at once for a foreign holder because a plain forwarded writer keeps the behavior of native Git; a merge is held to the owner’s rule that merges never fail (SPEC.md, “Merges and commits never fail”). The wait blocks on an inotify watch set before the first look, so a release between the look and the wait is not missed; where no watch can be set it looks again after a short pause. It never removes a lock.
index_lock_evidence
Evidence about a foreign index.lock and the verdict it supports. What: Evidence about a foreign index.lock and the verdict it supports: proven alive, dead, or evidence-free. Why: A live owner is proven by a process holding the lock open (device and inode), or by Git’s core.lockfilePid file naming a running process that started no later than the lock changed. A PID file naming a gone process proves a dead owner. Everything else is evidence-free; an absent open holder never proves abandonment, because native git commit keeps index.lock without a descriptor through its hooks and editor (src/index-lock/index-lock-evidence.ts).
index_lock_holder_parsers
Parsers of lsof -F pcDi records and Restart Manager lines, matched by device and inode. What: Parsers of the holder-scan output that is not /proc: lsof -F pcDi records and the Restart Manager script lines, both matched to a lock by device and inode. Why: The parsers are pure text functions, so their controls run on every host, whichever scan produced the text (src/index-lock/index-lock-holders-darwin.ts, -win32.ts).
index_lock_holders
Processes holding a lock file open, found by device and inode. What: Processes holding a lock file open, found by device and inode: /proc/<pid>/fd on Linux, lsof on macOS, the Restart Manager on Windows. Why: An open descriptor proves a live owner. Matching by device and inode, never by path text, means bind mounts and renamed paths cannot fake or hide a holder. Processes the scan cannot read are partial evidence (src/index-lock/index-lock-holders-linux.ts, -darwin.ts, -win32.ts).
index_lock_wait
Waiting for a foreign index.lock under the classification rules and Git’s backoff schedule. What: Git’s own lock-retry schedule: the backoff position, its next step, and the jittered wait it prescribes. Why: A caller that retries a lock the way Git retries its own waits on the same schedule, so it neither hammers the lock nor starves behind it. Waiting for a foreign index.lock is gone: a command that cannot take Git’s index.lock ends as native Git ends it, and the commit service’s landing retries it a bounded number of times itself (src/index-lock/index-lock-wait.ts is the incumbent of the schedule).
index_writer_commands
Index-writer classification and the commands that coordinate with a landing. What: Whether a resolved forwarded command writes the real index. Why: Index writers take the landing lock so they never interleave with a landing commit. The set follows SPEC.md “Index-writer coordination”: add, rm, mv, restore --staged, reset except --soft, stash, checkout, switch, merge, rebase, cherry-pick, revert, apply --cached and apply --index, update-index, read-tree, am, pull and sparse-checkout. Options count only before Git’s --; long options match in full or as the abbreviations Git accepts (src/index-lock/index-writer-commands.ts).
index_writer_forward
Running a coordinated forwarded index writer: the landing lock held while real Git runs as a child. What: Running a forwarded index writer as a child of the wrapper: wait for the commit service’s own hold on the index, complete an isolated merge when one applies, run real Git (again when the service took the index first), and report its status. Why: A writer competes for the index only through Git’s own index.lock, as it does without the wrapper; the one holder that is waited for is the commit service, whose hold ends by itself. The wrapper stays alive around Git so that a run the service beat to the lock can be started again and so that a merge the staged index would make Git refuse can be completed. A command that needs neither replaces the process with Git.
push_lock_clearing
The wrapper’s own fetch and push, started after the stale reference locks of their repository are cleared. What: The wrapper’s own fetch and push, run after the reference locks of a killed Git in their repository have been judged and the provably stale ones removed. Why: The commit service pushes unattended, and the reconcile fetch runs inside it, so a lock that a killed Git left blocks them with nobody there to clear it. Clearing before every start also covers every repetition of a push (each repetition starts through here), and nothing is waited for. A failed attempt carries the notice, so the person who reads the failure (or the refusal Git prints and still exits 0 on) finds what was removed, what was left, and the command that clears a lock the wrapper will not remove.
single_owner_claim
The numbered claim files that let exactly one command work on one worktree or checkout, without a lock. What: The right of one running command to work on one item (a worktree being converted, a checkout being migrated), held as a numbered claim file that exactly one command creates and that names its owner by process birth. Why: Two commands that both pass the preflight would both write the journal, share a token on a resume and exchange the directories back, so one command must own the item for as long as it works. The claim is lock-free: a claim file is made whole in a private temporary file and published with one exclusive link under the next generation number, so the kernel lets exactly one command create generation n + 1; no lock is held, no command ever waits, and a command that finds a live owner stops with the owner’s process id. The newest generation is the claim. A newer generation replaces an older one only when the older one records a released claim or an owner whose process no longer exists with that birth (the PID alone is not trusted, because it is reused), so a killed command leaves nothing to clean up by hand: the next command takes the next generation. The newest generation file is never removed, because removing it would let a command that read an older generation create the same number again; a released claim is rewritten in place by renaming a one-line record over it. Older generations are removed by the command that took the newer one. An owner in another PID namespace cannot be probed; it counts as alive until its claim file is older than FOREIGN_OWNER_PATIENCE, as every owner record of the wrapper does.
stale_ref_lock_found
The record of one judged reference lock and the outcome of a clearing pass. What: The record of one judged reference lock, and the outcome of one clearing pass. Why: The callers decide what to do from the outcome (run the command again only when a lock was cleared) and the notice is worded from the same record, so what was printed is what was decided.
stale_ref_lock_identity
A file’s identity at the moment it was judged, so the removal can tell it from a new file at the same path. What: A file’s identity at the moment it was judged: the removal checks that it is still that file. Why: A freed inode number may be handed to the next file created at once, so device and inode alone cannot tell the judged lock from a new lock a live Git made at the same path; the size, birth time and modification time are compared too.
stale_ref_lock_judge
How one reference lock is judged. What: How one reference lock is judged: too new, held, stale on a proof, or unproven. Why: Git writes no holder into a lock, so only a few facts prove a holder gone; everything else is left alone. The order of the checks and the reason for each are in SPEC.md, “Reference locks left by a Git that was killed”. A time stamp is not among the proofs: a correction of the wall clock moves every time that a lock and the boot are compared by (finding S-2 of the cross-vendor review of 2026-11-03T11:33:50-05:00).
stale_ref_lock_message
What the wrapper prints about reference locks, and the quoted command that clears one it will not remove. What: The words the wrapper prints about reference locks: what it removed, what it left and why, and the exact command that clears a lock it will not remove. Why: A lock that is not proven stale must never be removed, so the person is told what was found and given a command that is safe to paste. Path names come from the file system, so the command quotes every byte a shell or a terminal could read as something other than part of the name.
stale_ref_lock_nested_numbers
The processes that carry a number inside a PID namespace nested in the reader’s, which a pid file may name. What: Which processes of the table carry a given process number inside a PID namespace nested in the reader’s. Why: A pid file holds the number its writer had in the writer’s own PID namespace; Git records no namespace. A reader in the initial namespace sees every process of the machine, but a writer in a nested namespace is listed under a number of the reader’s namespace, and the number in the pid file is a different one. “No process has this number” therefore proves the writer gone only when no process of the table also has the number as its number inside a nested namespace. The kernel lists every number a process has, outermost first, on the NSpid line of /proc/<pid>/status (proc_pid_status(5)).
stale_ref_lock_retire
Removal of a proven stale lock with its companions. What: Judge one candidate and, when it is proven stale, set it aside, check that it is the file that was judged, and remove it with its companions. Why: Two wrappers may judge the same lock at the same moment, and a live Git may create a new lock at the same path the instant the old one is gone. A plain unlink by path would then remove the live Git’s lock. One rename to a private name takes the file atomically; what was taken is then checked against what was judged, and put back if it differs. Nothing here removes a path after a separate look at it: the lock, its pid file and the packed-references staging file all go through that one step, each against the identity it had when it was judged. No lock of the wrapper’s own is taken (Use as few locks as possible.).
stale_ref_lock_walk
Which files below a Git directory are reference locks. What: Which files below a Git directory are reference locks, and which files go with one. Why: Only the locks of references are judged here. index.lock, config.lock, shallow.lock, object and pack locks and every ~pid.lock file belong to other protocols or to the lock they accompany. The walk lists directory entries by their own file type, so a symbolic link is never followed and a link named like a lock is never a candidate.
stale_ref_locks
The reference locks a killed Git left behind: judged one by one, removed on a proof, otherwise described. What: The reference locks a killed Git left behind: found, judged one by one, removed when their holder is proven gone, and otherwise described with the command that clears them. Why: A Git killed in the middle of a ref update (a terminal closed mid-fetch, the out-of-memory killer, a reboot) leaves an empty <ref>.lock or packed-refs.lock, and every later fetch or push of the same reference then fails with “Unable to create … File exists”. Git writes no process number into a lock and removes none it did not create, and a live Git holds its locks with no descriptor open (refs/packed-backend.c:1254-1261, refs/files-backend.c:2888-2897), so a lock is removed only on a proof (SPEC.md, “Reference locks left by a Git that was killed”): the process its pid file names is gone or was reused, or the lock predates the boot. Everything else is left alone and reported. This module extends the sweep of a stopped child’s locks (stopped_git_locks.rs) to locks no wrapper-started child explains; it reads files and /proc in process and starts no Git.
stopped_git_locks
The lock files a stopped Git child leaves behind, found and removed once the child and what it started are gone. What: The reference locks a stopped Git child may have left behind, judged and reported once the child and what it started are gone. Why: Git removes its .lock files when it ends normally or on a catchable signal, but a child that is killed, or that is in an uninterruptible call when the stop arrives, cannot. On 2026-11-03 two pushes were stopped at their deadline; about 37 seconds later, in the same instant, empty refs/remotes/origin/*.lock files appeared for exactly their two tracking refs, with no pid file beside them, and every later git fetch of those refs failed with “Unable to create … File exists”. The service had started those pushes without core.lockfilePid, so no pid file was written. That the creators were the stopped pushes, released by the kernel late, is the inference that fits these facts; nothing observed the processes themselves. Releases 0.5.23 to 0.5.26 removed two kinds of file after the stop: a lock whose pid file named an ended member of the stopped tree, and an empty lock without a pid file that was born after the child started and stayed empty for a settle time. A cross-vendor review of 2026-11-03T11:33:50-05:00 (finding S-1) showed that neither is an ownership proof: a live writer of another Git that does not write pid files holds its lock empty with no descriptor open, for as long as its transaction lasts, and a pid file older than a lock names an earlier holder, so both rules could delete the lock of a Git that is running, and two writers could then update one reference. The sweep removes nothing on those rules. It hands the repository to the same judgement that runs before every fetch and push (stale_ref_locks.rs), which removes a reference lock only on a proof and through a conditional removal, and it says what it found and which command clears a lock it will not remove. The judgement is given one proof of its own, which a container’s view of the machine cannot give it: the numbers of the processes of the stopped tree that have ended. A pid file born at or after a lock that was itself born after the child started, naming one of them, names a process the wrapper started and saw end, in its own namespace (the container gate found the sweep leaving its own child’s locks without it). Locks of other protocols (the index, the shallow file, object and pack locks) are not touched or mentioned.