github.com/git-lfs/git-lfs@v2.5.2+incompatible/locking/locks.go (about)

     1  package locking
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/git-lfs/git-lfs/errors"
    12  	"github.com/git-lfs/git-lfs/filepathfilter"
    13  	"github.com/git-lfs/git-lfs/git"
    14  	"github.com/git-lfs/git-lfs/lfsapi"
    15  	"github.com/git-lfs/git-lfs/tools"
    16  	"github.com/git-lfs/git-lfs/tools/kv"
    17  	"github.com/rubyist/tracerx"
    18  )
    19  
    20  var (
    21  	// ErrNoMatchingLocks is an error returned when no matching locks were
    22  	// able to be resolved
    23  	ErrNoMatchingLocks = errors.New("lfs: no matching locks found")
    24  	// ErrLockAmbiguous is an error returned when multiple matching locks
    25  	// were found
    26  	ErrLockAmbiguous = errors.New("lfs: multiple locks found; ambiguous")
    27  )
    28  
    29  type LockCacher interface {
    30  	Add(l Lock) error
    31  	RemoveByPath(filePath string) error
    32  	RemoveById(id string) error
    33  	Locks() []Lock
    34  	Clear()
    35  	Save() error
    36  }
    37  
    38  // Client is the main interface object for the locking package
    39  type Client struct {
    40  	Remote    string
    41  	RemoteRef *git.Ref
    42  	client    *lockClient
    43  	cache     LockCacher
    44  
    45  	lockablePatterns []string
    46  	lockableFilter   *filepathfilter.Filter
    47  	lockableMutex    sync.Mutex
    48  
    49  	LocalWorkingDir          string
    50  	LocalGitDir              string
    51  	SetLockableFilesReadOnly bool
    52  }
    53  
    54  // NewClient creates a new locking client with the given configuration
    55  // You must call the returned object's `Close` method when you are finished with
    56  // it
    57  func NewClient(remote string, lfsClient *lfsapi.Client) (*Client, error) {
    58  	return &Client{
    59  		Remote: remote,
    60  		client: &lockClient{Client: lfsClient},
    61  		cache:  &nilLockCacher{},
    62  	}, nil
    63  }
    64  
    65  func (c *Client) SetupFileCache(path string) error {
    66  	stat, err := os.Stat(path)
    67  	if err != nil {
    68  		return errors.Wrap(err, "init lock cache")
    69  	}
    70  
    71  	lockFile := path
    72  	if stat.IsDir() {
    73  		lockFile = filepath.Join(path, "lockcache.db")
    74  	}
    75  
    76  	cache, err := NewLockCache(lockFile)
    77  	if err != nil {
    78  		return errors.Wrap(err, "init lock cache")
    79  	}
    80  
    81  	c.cache = cache
    82  	return nil
    83  }
    84  
    85  // Close this client instance; must be called to dispose of resources
    86  func (c *Client) Close() error {
    87  	return c.cache.Save()
    88  }
    89  
    90  // LockFile attempts to lock a file on the current remote
    91  // path must be relative to the root of the repository
    92  // Returns the lock id if successful, or an error
    93  func (c *Client) LockFile(path string) (Lock, error) {
    94  	lockRes, _, err := c.client.Lock(c.Remote, &lockRequest{
    95  		Path: path,
    96  		Ref:  &lockRef{Name: c.RemoteRef.Refspec()},
    97  	})
    98  	if err != nil {
    99  		return Lock{}, errors.Wrap(err, "api")
   100  	}
   101  
   102  	if len(lockRes.Message) > 0 {
   103  		if len(lockRes.RequestID) > 0 {
   104  			tracerx.Printf("Server Request ID: %s", lockRes.RequestID)
   105  		}
   106  		return Lock{}, fmt.Errorf("Server unable to create lock: %s", lockRes.Message)
   107  	}
   108  
   109  	lock := *lockRes.Lock
   110  	if err := c.cache.Add(lock); err != nil {
   111  		return Lock{}, errors.Wrap(err, "lock cache")
   112  	}
   113  
   114  	abs, err := getAbsolutePath(path)
   115  	if err != nil {
   116  		return Lock{}, errors.Wrap(err, "make lockpath absolute")
   117  	}
   118  
   119  	// Ensure writeable on return
   120  	if err := tools.SetFileWriteFlag(abs, true); err != nil {
   121  		return Lock{}, err
   122  	}
   123  
   124  	return lock, nil
   125  }
   126  
   127  // getAbsolutePath takes a repository-relative path and makes it absolute.
   128  //
   129  // For instance, given a repository in /usr/local/src/my-repo and a file called
   130  // dir/foo/bar.txt, getAbsolutePath will return:
   131  //
   132  //   /usr/local/src/my-repo/dir/foo/bar.txt
   133  func getAbsolutePath(p string) (string, error) {
   134  	root, err := git.RootDir()
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  
   139  	return filepath.Join(root, p), nil
   140  }
   141  
   142  // UnlockFile attempts to unlock a file on the current remote
   143  // path must be relative to the root of the repository
   144  // Force causes the file to be unlocked from other users as well
   145  func (c *Client) UnlockFile(path string, force bool) error {
   146  	id, err := c.lockIdFromPath(path)
   147  	if err != nil {
   148  		return fmt.Errorf("Unable to get lock id: %v", err)
   149  	}
   150  
   151  	return c.UnlockFileById(id, force)
   152  }
   153  
   154  // UnlockFileById attempts to unlock a lock with a given id on the current remote
   155  // Force causes the file to be unlocked from other users as well
   156  func (c *Client) UnlockFileById(id string, force bool) error {
   157  	unlockRes, _, err := c.client.Unlock(c.RemoteRef, c.Remote, id, force)
   158  	if err != nil {
   159  		return errors.Wrap(err, "api")
   160  	}
   161  
   162  	if len(unlockRes.Message) > 0 {
   163  		if len(unlockRes.RequestID) > 0 {
   164  			tracerx.Printf("Server Request ID: %s", unlockRes.RequestID)
   165  		}
   166  		return fmt.Errorf("Server unable to unlock: %s", unlockRes.Message)
   167  	}
   168  
   169  	if err := c.cache.RemoveById(id); err != nil {
   170  		return fmt.Errorf("Error caching unlock information: %v", err)
   171  	}
   172  
   173  	if unlockRes.Lock != nil {
   174  		abs, err := getAbsolutePath(unlockRes.Lock.Path)
   175  		if err != nil {
   176  			return errors.Wrap(err, "make lockpath absolute")
   177  		}
   178  
   179  		// Make non-writeable if required
   180  		if c.SetLockableFilesReadOnly && c.IsFileLockable(unlockRes.Lock.Path) {
   181  			return tools.SetFileWriteFlag(abs, false)
   182  		}
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  // Lock is a record of a locked file
   189  type Lock struct {
   190  	// Id is the unique identifier corresponding to this particular Lock. It
   191  	// must be consistent with the local copy, and the server's copy.
   192  	Id string `json:"id"`
   193  	// Path is an absolute path to the file that is locked as a part of this
   194  	// lock.
   195  	Path string `json:"path"`
   196  	// Owner is the identity of the user that created this lock.
   197  	Owner *User `json:"owner,omitempty"`
   198  	// LockedAt is the time at which this lock was acquired.
   199  	LockedAt time.Time `json:"locked_at"`
   200  }
   201  
   202  // SearchLocks returns a channel of locks which match the given name/value filter
   203  // If limit > 0 then search stops at that number of locks
   204  // If localOnly = true, don't query the server & report only own local locks
   205  func (c *Client) SearchLocks(filter map[string]string, limit int, localOnly bool) ([]Lock, error) {
   206  	if localOnly {
   207  		return c.searchCachedLocks(filter, limit)
   208  	} else {
   209  		return c.searchRemoteLocks(filter, limit)
   210  	}
   211  }
   212  
   213  func (c *Client) VerifiableLocks(ref *git.Ref, limit int) (ourLocks, theirLocks []Lock, err error) {
   214  	if ref == nil {
   215  		ref = c.RemoteRef
   216  	}
   217  
   218  	ourLocks = make([]Lock, 0, limit)
   219  	theirLocks = make([]Lock, 0, limit)
   220  	body := &lockVerifiableRequest{
   221  		Ref:   &lockRef{Name: ref.Refspec()},
   222  		Limit: limit,
   223  	}
   224  
   225  	c.cache.Clear()
   226  
   227  	for {
   228  		list, res, err := c.client.SearchVerifiable(c.Remote, body)
   229  		if res != nil {
   230  			switch res.StatusCode {
   231  			case http.StatusNotFound, http.StatusNotImplemented:
   232  				return ourLocks, theirLocks, errors.NewNotImplementedError(err)
   233  			case http.StatusForbidden:
   234  				return ourLocks, theirLocks, errors.NewAuthError(err)
   235  			}
   236  		}
   237  
   238  		if err != nil {
   239  			return ourLocks, theirLocks, err
   240  		}
   241  
   242  		if list.Message != "" {
   243  			if len(list.RequestID) > 0 {
   244  				tracerx.Printf("Server Request ID: %s", list.RequestID)
   245  			}
   246  			return ourLocks, theirLocks, fmt.Errorf("Server error searching locks: %s", list.Message)
   247  		}
   248  
   249  		for _, l := range list.Ours {
   250  			c.cache.Add(l)
   251  			ourLocks = append(ourLocks, l)
   252  			if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
   253  				return ourLocks, theirLocks, nil
   254  			}
   255  		}
   256  
   257  		for _, l := range list.Theirs {
   258  			c.cache.Add(l)
   259  			theirLocks = append(theirLocks, l)
   260  			if limit > 0 && (len(ourLocks)+len(theirLocks)) >= limit {
   261  				return ourLocks, theirLocks, nil
   262  			}
   263  		}
   264  
   265  		if list.NextCursor != "" {
   266  			body.Cursor = list.NextCursor
   267  		} else {
   268  			break
   269  		}
   270  	}
   271  
   272  	return ourLocks, theirLocks, nil
   273  }
   274  
   275  func (c *Client) searchCachedLocks(filter map[string]string, limit int) ([]Lock, error) {
   276  	cachedlocks := c.cache.Locks()
   277  	path, filterByPath := filter["path"]
   278  	id, filterById := filter["id"]
   279  	lockCount := 0
   280  	locks := make([]Lock, 0, len(cachedlocks))
   281  	for _, l := range cachedlocks {
   282  		// Manually filter by Path/Id
   283  		if (filterByPath && path != l.Path) ||
   284  			(filterById && id != l.Id) {
   285  			continue
   286  		}
   287  		locks = append(locks, l)
   288  		lockCount++
   289  		if limit > 0 && lockCount >= limit {
   290  			break
   291  		}
   292  	}
   293  	return locks, nil
   294  }
   295  
   296  func (c *Client) searchRemoteLocks(filter map[string]string, limit int) ([]Lock, error) {
   297  	locks := make([]Lock, 0, limit)
   298  
   299  	apifilters := make([]lockFilter, 0, len(filter))
   300  	for k, v := range filter {
   301  		apifilters = append(apifilters, lockFilter{Property: k, Value: v})
   302  	}
   303  
   304  	query := &lockSearchRequest{
   305  		Filters: apifilters,
   306  		Limit:   limit,
   307  		Refspec: c.RemoteRef.Refspec(),
   308  	}
   309  
   310  	for {
   311  		list, _, err := c.client.Search(c.Remote, query)
   312  		if err != nil {
   313  			return locks, errors.Wrap(err, "locking")
   314  		}
   315  
   316  		if list.Message != "" {
   317  			if len(list.RequestID) > 0 {
   318  				tracerx.Printf("Server Request ID: %s", list.RequestID)
   319  			}
   320  			return locks, fmt.Errorf("Server error searching for locks: %s", list.Message)
   321  		}
   322  
   323  		for _, l := range list.Locks {
   324  			locks = append(locks, l)
   325  			if limit > 0 && len(locks) >= limit {
   326  				// Exit outer loop too
   327  				return locks, nil
   328  			}
   329  		}
   330  
   331  		if list.NextCursor != "" {
   332  			query.Cursor = list.NextCursor
   333  		} else {
   334  			break
   335  		}
   336  	}
   337  
   338  	return locks, nil
   339  }
   340  
   341  // lockIdFromPath makes a call to the LFS API and resolves the ID for the locked
   342  // locked at the given path.
   343  //
   344  // If the API call failed, an error will be returned. If multiple locks matched
   345  // the given path (should not happen during real-world usage), an error will be
   346  // returnd. If no locks matched the given path, an error will be returned.
   347  //
   348  // If the API call is successful, and only one lock matches the given filepath,
   349  // then its ID will be returned, along with a value of "nil" for the error.
   350  func (c *Client) lockIdFromPath(path string) (string, error) {
   351  	list, _, err := c.client.Search(c.Remote, &lockSearchRequest{
   352  		Filters: []lockFilter{
   353  			{Property: "path", Value: path},
   354  		},
   355  	})
   356  
   357  	if err != nil {
   358  		return "", err
   359  	}
   360  
   361  	switch len(list.Locks) {
   362  	case 0:
   363  		return "", ErrNoMatchingLocks
   364  	case 1:
   365  		return list.Locks[0].Id, nil
   366  	default:
   367  		return "", ErrLockAmbiguous
   368  	}
   369  }
   370  
   371  // IsFileLockedByCurrentCommitter returns whether a file is locked by the
   372  // current user, as cached locally
   373  func (c *Client) IsFileLockedByCurrentCommitter(path string) bool {
   374  	filter := map[string]string{"path": path}
   375  	locks, err := c.searchCachedLocks(filter, 1)
   376  	if err != nil {
   377  		tracerx.Printf("Error searching cached locks: %s\nForcing remote search", err)
   378  		locks, _ = c.searchRemoteLocks(filter, 1)
   379  	}
   380  	return len(locks) > 0
   381  }
   382  
   383  func init() {
   384  	kv.RegisterTypeForStorage(&Lock{})
   385  }
   386  
   387  type nilLockCacher struct{}
   388  
   389  func (c *nilLockCacher) Add(l Lock) error {
   390  	return nil
   391  }
   392  func (c *nilLockCacher) RemoveByPath(filePath string) error {
   393  	return nil
   394  }
   395  func (c *nilLockCacher) RemoveById(id string) error {
   396  	return nil
   397  }
   398  func (c *nilLockCacher) Locks() []Lock {
   399  	return nil
   400  }
   401  func (c *nilLockCacher) Clear() {}
   402  func (c *nilLockCacher) Save() error {
   403  	return nil
   404  }