b8e7ad6b6d
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.
246 lines
6.3 KiB
Go
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
|
|
}
|