On this page
Package lock provides ref-lock primitives for concurrent ref updates using atomic lock file creation (link(2) of a fully-written temporary sibling) and exponential backoff polling.
#internal/lock
#internal/lock
Package lock provides ref-lock primitives for concurrent ref updates using atomic lock file creation (link(2) of a fully-written temporary sibling) and exponential backoff polling. PID liveness checks detect and clean up stale locks left by crashed processes.
#RewriteRef
const RewriteRef = "safegit/rewrite"RewriteRef is the repository-wide history-rewrite lock. It lives in the SHARED safegit directory, so every worktree of the repository contends on the same file: a rewrite changes object names for all of them. Taken by scrub file, scrub match, scrub run and author rewrite.
#OperationRef
const OperationRef = "safegit/operation"OperationRef is the worktree operation lock. It lives in the WORKTREE-LOCAL safegit directory, so two worktrees of the same repository operate independently while two processes in one worktree serialize. Taken by every command that mutates this worktree: the guarded passthroughs (switch, pull, merge, rebase, reset, bisect, cherry-pick, revert) and the commit pipeline's three entry points plus undo.
It is what makes a commit's in-flight-operation check meaningful. Without it a passthrough could create sequencer state (a conflicted merge, a stopped cherry-pick) in the window between a commit reading that state and updating the ref, and the commit would build its tree against a repository git considers mid-operation.
#RefLock
type RefLock structRefLock represents an acquired lock on a git ref.
#TimeoutError
type TimeoutError structTimeoutError is what Acquire returns when the timeout expired with a live holder still owning the lock. It is a distinct type because the exit code safegit reports for that outcome (exitcode.LockTimeout) is distinct: a caller decides between "someone else is working here, wait or investigate" and every other reason a lock could not be taken by asking errors.As for this type, never by matching the message text.
The message names the ref and the holder record, so a caller that surfaces the error verbatim tells the operator which process to look at.
#ReleasePending
func ReleasePending()ReleasePending removes every lock this process still holds.
It exists for the exit paths that do not unwind. A deferred Release covers a function that returns; it does not cover os.Exit, which safegit's die() and several handlers reach on a refusal. Without this, a command that acquired a lock and then died on an unrelated error would leave its lock file behind for the next contender to wait out and for doctor to report -- recoverable, since the holder is dead and the lock is therefore stale, but noise the process itself can prevent.
It is safe to call when no lock is held, and safe to call twice.
Each removal is identity-checked exactly as RefLock.Release is: a lock this process was force-released out of, and which another process has since re-taken, is left alone rather than deleted out from under its new holder.
#IsTimeout
func IsTimeout(err error) boolIsTimeout reports whether err is, or wraps, a lock acquisition timeout.
#Path
func Path(locksBaseDir, ref string) string { return lockPath(locksBaseDir, ref) }Path returns where the lock file for ref lives under locksBaseDir. Callers that inspect or remove a lock by name -- safegit unlock, doctor -- resolve it here rather than rebuilding the path themselves.
#NameFromPath
func NameFromPath(locksBaseDir, path string) stringNameFromPath is Path's inverse: it turns an absolute lock-file path under locksBaseDir back into the ref (or pseudo-ref) it locks, for display. A path outside that subtree, or one that is not a lock file, yields "".
#Acquire
func Acquire(locksBaseDir, safegitDir, ref, op string, timeout time.Duration) (*RefLock, error)Acquire attempts to acquire a lock on the given ref. locksBaseDir is the safegit directory whose "locks/" subtree holds lock files; for worktrees this should be the shared (common) safegit dir so that all worktrees serialize on the same lock. safegitDir is the worktree-local safegit dir used for oplog writes (stale-lock recovery events). Creation is atomic, so exactly one caller wins it. If the lock is held by a dead process, it is automatically replaced -- see the reclamation rules in reclaim.go, which are what keep two contenders facing the same stale lock from both deciding they reclaimed it. Uses exponential backoff polling bounded by timeout.
#IsLockFile
func IsLockFile(name string) boolIsLockFile reports whether name (a bare file name, not a path) names a lock. It is the predicate every scan of a locks/ subtree uses.
#IsPublicationTemp
func IsPublicationTemp(name string) boolIsPublicationTemp reports whether name (a bare file name, not a path) is a lock-publication temporary sibling rather than a lock.
#IsStale
func IsStale(path string) boolIsStale reports whether the process that holds the lock file is dead, which is the only condition under which the lock may be reclaimed. An unreadable, corrupt or zero-length lock file (no parseable PID) is stale: it is what a crash mid-create leaves behind.
There is no error return. Every condition this function can meet is already a verdict -- a lock it cannot read is stale, a comparison it cannot make fails closed and the lock is left alone -- so a caller has nothing to decide from an error that the boolean does not already say.
Reclaiming a lock whose holder is still running lets two operations mutate the same ref at once, so every check beyond plain PID liveness must have positive evidence before it declares a lock stale: - If the lock contains a host= field that differs from the local hostname, refuse to reclaim (the PID belongs to a different machine's namespace). - PID reuse is decided by comparing the start identity recorded at acquire time against the current start time of whatever now holds that PID: a mismatch means the recorded holder is gone and an unrelated process inherited its PID. Nothing else -- and in particular no file timestamp -- is evidence of reuse. When either side of the comparison is missing (no start= field, or a platform that cannot report start times) the check fails closed and the lock is left alone.
#ForceRelease
func ForceRelease(locksBaseDir, ref string, remove func(string) error) errorForceRelease unconditionally removes the lock file for a ref. locksBaseDir is the safegit directory whose "locks/" subtree holds lock files; for worktrees this should be the shared (common) safegit dir.
remove performs the unlink, and it is a parameter for the same reason ReclaimIfStale's is: safegit unlock mints the removal through the effects handle, so releasing a lock is visible in machine mode and a preview records it instead of performing it. A remover that treats a missing path as success turns "there was no lock" into "the lock was released", so the caller's remover must report the absence -- os.Remove does.
#ParsePID
func ParsePID(lockPath string) (int, error)ParsePID reads the lock file and extracts the pid= value.
#ReclaimIfStale
func ReclaimIfStale(path string, remove func(string) error) boolReclaimIfStale removes the lock file at path if, and only if, its holder is genuinely gone -- the same judgement Acquire makes, under the same flock and the same inode identity re-check. It reports whether the file was removed.
It is the authority for every unattended removal of a lock nobody asked about by name: safegit doctor --action fix sweeps the locks subtree through it rather than judging staleness and then calling os.Remove, because between those two steps another process can reclaim the same stale lock and publish its own live one at that path, and the bare remove would delete THAT.
A false result is never a verdict that the lock is live: it also covers "the path is already gone", "another contender holds the flock right now" and "this filesystem cannot flock". All of those mean leave it alone and look again later, which is the fail-closed direction.
remove is how the file is unlinked, and it is a parameter because doctor's sweep mints its removals through the effects handle -- so a preview records them and machine mode carries them -- while the acquire path unlinks directly. It MUST report a missing path as an error: reclaimLocked reads a nil error as "the stale lock was removed", so a remover with RemoveAll semantics would turn every vanished path into a reclamation this process claims to have performed.
#TimeoutError.Error
func (e *TimeoutError) Error() string#RefLock.Release
func (l *RefLock) Release() errorRelease removes the lock file this process published, and nothing else.