Skip to content
internal/lock
Edit
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

Go go
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

Go go
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

Go go
type RefLock struct

RefLock represents an acquired lock on a git ref.

#TimeoutError

Go go
type TimeoutError struct

TimeoutError 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

Go go
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

Go go
func IsTimeout(err error) bool

IsTimeout reports whether err is, or wraps, a lock acquisition timeout.

#Path

Go go
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

Go go
func NameFromPath(locksBaseDir, path string) string

NameFromPath 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

Go go
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

Go go
func IsLockFile(name string) bool

IsLockFile 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

Go go
func IsPublicationTemp(name string) bool

IsPublicationTemp reports whether name (a bare file name, not a path) is a lock-publication temporary sibling rather than a lock.

#IsStale

Go go
func IsStale(path string) bool

IsStale 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

Go go
func ForceRelease(locksBaseDir, ref string, remove func(string) error) error

ForceRelease 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

Go go
func ParsePID(lockPath string) (int, error)

ParsePID reads the lock file and extracts the pid= value.

#ReclaimIfStale

Go go
func ReclaimIfStale(path string, remove func(string) error) bool

ReclaimIfStale 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

Go go
func (e *TimeoutError) Error() string

#RefLock.Release

Go go
func (l *RefLock) Release() error

Release removes the lock file this process published, and nothing else.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • claudewheel A TUI Claude Code Launcher that lets you have more than one profile, manage sessions lifecycle, pick the exact CC version, model to use (even older unlisted ones), pick which GitHub account to use, etc.
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search