package gitx import ( "context" "fmt" "sync" ) // spaceLocks holds one buffered channel per space directory, used as a // context-aware mutex. It is process-wide on purpose: a space may be opened // many times (once per request is normal), and per-handle locking would not // exclude anything. The key is the cleaned absolute repository directory, which // Create and Open both guarantee. // // This guards only the daemon's own writes. Human pushes go through native // receive-pack in another process and take git's own ref locks, whose // interoperation with go-git's is not verified — which is why every ref move // here is additionally a compare-and-swap that retries. var spaceLocks = struct { mu sync.Mutex m map[string]chan struct{} }{m: make(map[string]chan struct{})} func spaceLock(dir string) chan struct{} { spaceLocks.mu.Lock() defer spaceLocks.mu.Unlock() ch, ok := spaceLocks.m[dir] if !ok { ch = make(chan struct{}, 1) spaceLocks.m[dir] = ch } return ch } // lock acquires the space's write lock, honouring ctx's deadline. The returned // function releases it and must be called exactly once. // // Entries are never removed from the registry. A space is a long-lived object // and the entry is one channel; reclaiming them would need a refcount whose // only purpose is to free a few dozen bytes. func (r *Repo) lock(ctx context.Context) (func(), error) { ch := spaceLock(r.dir) select { case ch <- struct{}{}: var once sync.Once return func() { once.Do(func() { <-ch }) }, nil case <-ctx.Done(): return nil, fmt.Errorf("gitx: acquiring the write lock for %s: %w", r.ref, ctx.Err()) } } // WithLock runs fn holding the space's write lock, so a caller that has to make // several git writes look like one operation (a merge plus its bookkeeping, the // reconciler repairing a branch) can do so without reaching into this package's // internals. // // The lock is not reentrant: fn must not call an exported write method on the // same space, which would deadlock. func (r *Repo) WithLock(ctx context.Context, fn func(context.Context) error) error { ctx, cancel := r.withTimeout(ctx) defer cancel() unlock, err := r.lock(ctx) if err != nil { return err } defer unlock() return fn(ctx) }