package commands import ( "fmt" "sort" "strconv" "strings" "github.com/git-lfs/git-lfs/config" "github.com/git-lfs/git-lfs/errors" "github.com/git-lfs/git-lfs/git" "github.com/git-lfs/git-lfs/lfshttp" "github.com/git-lfs/git-lfs/locking" "github.com/git-lfs/git-lfs/tq" ) type verifyState byte const ( verifyStateUnknown verifyState = iota verifyStateEnabled verifyStateDisabled ) func verifyLocksForUpdates(lv *lockVerifier, updates []*git.RefUpdate) { for _, update := range updates { lv.Verify(update.Right()) } } // lockVerifier verifies locked files before updating one or more refs. type lockVerifier struct { endpoint lfshttp.Endpoint verifyState verifyState verifiedRefs map[string]bool // all existing locks ourLocks map[string]*refLock theirLocks map[string]*refLock // locks from ourLocks that have been modified ownedLocks []*refLock // locks from theirLocks that have been modified unownedLocks []*refLock } func (lv *lockVerifier) Verify(ref *git.Ref) { if ref == nil { panic("no ref specified for verification") } if lv.verifyState == verifyStateDisabled || lv.verifiedRefs[ref.Refspec()] { return } lockClient := newLockClient() lockClient.RemoteRef = ref ours, theirs, err := lockClient.SearchLocksVerifiable(0, false) if err != nil { if errors.IsNotImplementedError(err) { disableFor(lv.endpoint.Url) } else if lv.verifyState == verifyStateUnknown || lv.verifyState == verifyStateEnabled { if errors.IsAuthError(err) { if lv.verifyState == verifyStateUnknown { Error("WARNING: Authentication error: %s", err) } else if lv.verifyState == verifyStateEnabled { Exit("ERROR: Authentication error: %s", err) } } else { Print("Remote %q does not support the LFS locking API. Consider disabling it with:", cfg.PushRemote()) Print(" $ git config lfs.%s.locksverify false", lv.endpoint.Url) if lv.verifyState == verifyStateEnabled { ExitWithError(err) } } } } else if lv.verifyState == verifyStateUnknown { Print("Locking support detected on remote %q. Consider enabling it with:", cfg.PushRemote()) Print(" $ git config lfs.%s.locksverify true", lv.endpoint.Url) } lv.addLocks(ref, ours, lv.ourLocks) lv.addLocks(ref, theirs, lv.theirLocks) lv.verifiedRefs[ref.Refspec()] = true } func (lv *lockVerifier) addLocks(ref *git.Ref, locks []locking.Lock, set map[string]*refLock) { for _, l := range locks { if rl, ok := set[l.Path]; ok { if err := rl.Add(ref, l); err != nil { Error("WARNING: error adding %q lock for ref %q: %+v", l.Path, ref, err) } } else { set[l.Path] = lv.newRefLocks(ref, l) } } } // Determines if a filename is lockable. Implements lfs.GitScannerSet func (lv *lockVerifier) Contains(name string) bool { if lv == nil { return false } _, ok := lv.theirLocks[name] return ok } func (lv *lockVerifier) LockedByThem(name string) bool { if lock, ok := lv.theirLocks[name]; ok { lv.unownedLocks = append(lv.unownedLocks, lock) return true } return false } func (lv *lockVerifier) LockedByUs(name string) bool { if lock, ok := lv.ourLocks[name]; ok { lv.ownedLocks = append(lv.ownedLocks, lock) return true } return false } func (lv *lockVerifier) UnownedLocks() []*refLock { return lv.unownedLocks } func (lv *lockVerifier) HasUnownedLocks() bool { return len(lv.unownedLocks) > 0 } func (lv *lockVerifier) OwnedLocks() []*refLock { return lv.ownedLocks } func (lv *lockVerifier) HasOwnedLocks() bool { return len(lv.ownedLocks) > 0 } func (lv *lockVerifier) Enabled() bool { return lv.verifyState == verifyStateEnabled } func (lv *lockVerifier) newRefLocks(ref *git.Ref, l locking.Lock) *refLock { return &refLock{ allRefs: lv.verifiedRefs, path: l.Path, refs: map[*git.Ref]locking.Lock{ref: l}, } } func newLockVerifier(m *tq.Manifest) *lockVerifier { lv := &lockVerifier{ endpoint: getAPIClient().Endpoints.Endpoint("upload", cfg.PushRemote()), verifiedRefs: make(map[string]bool), ourLocks: make(map[string]*refLock), theirLocks: make(map[string]*refLock), } // Do not check locks for standalone transfer, because there is no LFS // server to ask. if m.IsStandaloneTransfer() { lv.verifyState = verifyStateDisabled } else { lv.verifyState = getVerifyStateFor(lv.endpoint.Url) } return lv } // refLock represents a unique locked file path, potentially across multiple // refs. It tracks each individual lock in case different users locked the // same path across multiple refs. type refLock struct { path string allRefs map[string]bool refs map[*git.Ref]locking.Lock } // Path returns the locked path. func (r *refLock) Path() string { return r.path } // Owners returns the list of owners that locked this file, including what // specific refs the files were locked in. If a user locked a file on all refs, // don't bother listing them. // // Example: technoweenie, bob (refs: foo) func (r *refLock) Owners() string { users := make(map[string][]string, len(r.refs)) for ref, lock := range r.refs { u := lock.Owner.Name if _, ok := users[u]; !ok { users[u] = make([]string, 0, len(r.refs)) } users[u] = append(users[u], ref.Name) } owners := make([]string, 0, len(users)) for name, refs := range users { seenRefCount := 0 for _, ref := range refs { if r.allRefs[ref] { seenRefCount++ } } if seenRefCount == len(r.allRefs) { // lock is included in all refs, so don't list them owners = append(owners, name) continue } sort.Strings(refs) owners = append(owners, fmt.Sprintf("%s (refs: %s)", name, strings.Join(refs, ", "))) } sort.Strings(owners) return strings.Join(owners, ", ") } func (r *refLock) Add(ref *git.Ref, l locking.Lock) error { r.refs[ref] = l return nil } // getVerifyStateFor returns whether or not lock verification is enabled for the // given url. If no state has been explicitly set, an "unknown" state will be // returned instead. func getVerifyStateFor(rawurl string) verifyState { uc := config.NewURLConfig(cfg.Git) v, ok := uc.Get("lfs", rawurl, "locksverify") if !ok { if supportsLockingAPI(rawurl) { return verifyStateEnabled } return verifyStateUnknown } if enabled, _ := strconv.ParseBool(v); enabled { return verifyStateEnabled } return verifyStateDisabled }