github.com/2lambda123/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 }