Files
kandev/apps/backend/internal/worktree/target_path_lock.go
T
2026-07-23 20:21:23 +01:00

86 lines
1.8 KiB
Go

package worktree
import (
"context"
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
)
const windowsGOOS = "windows"
type targetPathLockEntry struct {
token chan struct{}
refs int
}
type targetPathLockRegistry struct {
mu sync.Mutex
entries map[string]*targetPathLockEntry
}
var worktreeTargetPathLocks = targetPathLockRegistry{
entries: make(map[string]*targetPathLockEntry),
}
func acquireWorktreeTargetPath(ctx context.Context, path string) (func(), error) {
key, err := normalizedWorktreeTargetPath(path)
if err != nil {
return nil, err
}
entry := worktreeTargetPathLocks.reference(key)
select {
case <-entry.token:
var once sync.Once
return func() {
once.Do(func() {
entry.token <- struct{}{}
worktreeTargetPathLocks.unreference(key, entry)
})
}, nil
case <-ctx.Done():
worktreeTargetPathLocks.unreference(key, entry)
return nil, ctx.Err()
}
}
func normalizedWorktreeTargetPath(path string) (string, error) {
return normalizedWorktreeTargetPathForOS(path, runtime.GOOS)
}
func normalizedWorktreeTargetPathForOS(path, goos string) (string, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("resolve worktree target path: %w", err)
}
key := filepath.Clean(absPath)
if goos == windowsGOOS {
key = strings.ToLower(key)
}
return key, nil
}
func (r *targetPathLockRegistry) reference(key string) *targetPathLockEntry {
r.mu.Lock()
defer r.mu.Unlock()
entry := r.entries[key]
if entry == nil {
entry = &targetPathLockEntry{token: make(chan struct{}, 1)}
entry.token <- struct{}{}
r.entries[key] = entry
}
entry.refs++
return entry
}
func (r *targetPathLockRegistry) unreference(key string, entry *targetPathLockEntry) {
r.mu.Lock()
defer r.mu.Unlock()
entry.refs--
if entry.refs == 0 && r.entries[key] == entry {
delete(r.entries, key)
}
}