git-lfs/commands/lockverifier.go
Chris Darroch b8e7ad6b6d commands,git,lfs: rename left/right RefUpdate vars
Since at least PR #2706, and indeed earlier, the local and remote
refs which are part of the RefUpdate structure have been referred
to as the "left" and "right" refs, terminology which stems from
the origin of this structure in the "git lfs pre-push" hook command,
where each line of input contains commit information in the form:

  <local ref> <local sha1> <remote ref> <remote sha1>

However, outside of this context, "left" and "right" are ambiguous
terms, and may potentially be confused with the left and right
refs specified in a Git-style two-dot range notation.  We therefore
replace these terms with "local" and "remote", which should help
clarify their purpose throughout the codebase.
2022-02-06 21:32:27 -08:00

246 lines
6.3 KiB
Go

package commands
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/git-lfs/git-lfs/v3/config"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/git"
"github.com/git-lfs/git-lfs/v3/lfshttp"
"github.com/git-lfs/git-lfs/v3/locking"
"github.com/git-lfs/git-lfs/v3/tq"
"github.com/git-lfs/git-lfs/v3/tr"
)
type verifyState byte
const (
verifyStateUnknown verifyState = iota
verifyStateEnabled
verifyStateDisabled
)
func verifyLocksForUpdates(lv *lockVerifier, updates []*git.RefUpdate) {
for _, update := range updates {
lv.Verify(update.RemoteRef())
}
}
// 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(tr.Tr.Get("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(tr.Tr.Get("warning: Authentication error: %s", err))
} else if lv.verifyState == verifyStateEnabled {
Exit(tr.Tr.Get("error: Authentication error: %s", err))
}
} else {
Print(tr.Tr.Get("Remote %q does not support the Git 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(tr.Tr.Get("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(tr.Tr.Get("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
}