1package locking 2 3import ( 4 "fmt" 5 "net/http" 6 "strconv" 7 8 "github.com/git-lfs/git-lfs/v3/git" 9 "github.com/git-lfs/git-lfs/v3/lfsapi" 10 "github.com/git-lfs/git-lfs/v3/lfshttp" 11) 12 13type lockClient interface { 14 Lock(remote string, lockReq *lockRequest) (*lockResponse, int, error) 15 Unlock(ref *git.Ref, remote, id string, force bool) (*unlockResponse, int, error) 16 Search(remote string, searchReq *lockSearchRequest) (*lockList, int, error) 17 SearchVerifiable(remote string, vreq *lockVerifiableRequest) (*lockVerifiableList, int, error) 18} 19 20type httpLockClient struct { 21 *lfsapi.Client 22} 23 24type lockRef struct { 25 Name string `json:"name,omitempty"` 26} 27 28// LockRequest encapsulates the payload sent across the API when a client would 29// like to obtain a lock against a particular path on a given remote. 30type lockRequest struct { 31 // Path is the path that the client would like to obtain a lock against. 32 Path string `json:"path"` 33 Ref *lockRef `json:"ref,omitempty"` 34} 35 36// LockResponse encapsulates the information sent over the API in response to 37// a `LockRequest`. 38type lockResponse struct { 39 // Lock is the Lock that was optionally created in response to the 40 // payload that was sent (see above). If the lock already exists, then 41 // the existing lock is sent in this field instead, and the author of 42 // that lock remains the same, meaning that the client failed to obtain 43 // that lock. An HTTP status of "409 - Conflict" is used here. 44 // 45 // If the lock was unable to be created, this field will hold the 46 // zero-value of Lock and the Err field will provide a more detailed set 47 // of information. 48 // 49 // If an error was experienced in creating this lock, then the 50 // zero-value of Lock should be sent here instead. 51 Lock *Lock `json:"lock"` 52 53 // Message is the optional error that was encountered while trying to create 54 // the above lock. 55 Message string `json:"message,omitempty"` 56 DocumentationURL string `json:"documentation_url,omitempty"` 57 RequestID string `json:"request_id,omitempty"` 58} 59 60func (c *httpLockClient) Lock(remote string, lockReq *lockRequest) (*lockResponse, int, error) { 61 e := c.Endpoints.Endpoint("upload", remote) 62 req, err := c.NewRequest("POST", e, "locks", lockReq) 63 if err != nil { 64 return nil, 0, err 65 } 66 67 req = c.Client.LogRequest(req, "lfs.locks.lock") 68 res, err := c.DoAPIRequestWithAuth(remote, req) 69 if err != nil { 70 if res != nil { 71 return nil, res.StatusCode, err 72 } 73 return nil, 0, err 74 } 75 76 lockRes := &lockResponse{} 77 err = lfshttp.DecodeJSON(res, lockRes) 78 if err != nil { 79 return nil, res.StatusCode, err 80 } 81 if lockRes.Lock == nil && len(lockRes.Message) == 0 { 82 return nil, res.StatusCode, fmt.Errorf("invalid server response") 83 } 84 return lockRes, res.StatusCode, nil 85} 86 87// UnlockRequest encapsulates the data sent in an API request to remove a lock. 88type unlockRequest struct { 89 // Force determines whether or not the lock should be "forcibly" 90 // unlocked; that is to say whether or not a given individual should be 91 // able to break a different individual's lock. 92 Force bool `json:"force"` 93 Ref *lockRef `json:"ref,omitempty"` 94} 95 96// UnlockResponse is the result sent back from the API when asked to remove a 97// lock. 98type unlockResponse struct { 99 // Lock is the lock corresponding to the asked-about lock in the 100 // `UnlockPayload` (see above). If no matching lock was found, this 101 // field will take the zero-value of Lock, and Err will be non-nil. 102 Lock *Lock `json:"lock"` 103 104 // Message is an optional field which holds any error that was experienced 105 // while removing the lock. 106 Message string `json:"message,omitempty"` 107 DocumentationURL string `json:"documentation_url,omitempty"` 108 RequestID string `json:"request_id,omitempty"` 109} 110 111func (c *httpLockClient) Unlock(ref *git.Ref, remote, id string, force bool) (*unlockResponse, int, error) { 112 e := c.Endpoints.Endpoint("upload", remote) 113 suffix := fmt.Sprintf("locks/%s/unlock", id) 114 req, err := c.NewRequest("POST", e, suffix, &unlockRequest{ 115 Force: force, 116 Ref: &lockRef{Name: ref.Refspec()}, 117 }) 118 if err != nil { 119 return nil, 0, err 120 } 121 122 req = c.Client.LogRequest(req, "lfs.locks.unlock") 123 res, err := c.DoAPIRequestWithAuth(remote, req) 124 if err != nil { 125 if res != nil { 126 return nil, res.StatusCode, err 127 } 128 return nil, 0, err 129 } 130 131 unlockRes := &unlockResponse{} 132 err = lfshttp.DecodeJSON(res, unlockRes) 133 if err != nil { 134 return nil, res.StatusCode, err 135 } 136 if unlockRes.Lock == nil && len(unlockRes.Message) == 0 { 137 return nil, res.StatusCode, fmt.Errorf("invalid server response") 138 } 139 return unlockRes, res.StatusCode, nil 140} 141 142// Filter represents a single qualifier to apply against a set of locks. 143type lockFilter struct { 144 // Property is the property to search against. 145 // Value is the value that the property must take. 146 Property, Value string 147} 148 149// LockSearchRequest encapsulates the request sent to the server when the client 150// would like a list of locks that match the given criteria. 151type lockSearchRequest struct { 152 // Filters is the set of filters to query against. If the client wishes 153 // to obtain a list of all locks, an empty array should be passed here. 154 Filters []lockFilter 155 // Cursor is an optional field used to tell the server which lock was 156 // seen last, if scanning through multiple pages of results. 157 // 158 // Servers must return a list of locks sorted in reverse chronological 159 // order, so the Cursor provides a consistent method of viewing all 160 // locks, even if more were created between two requests. 161 Cursor string 162 // Limit is the maximum number of locks to return in a single page. 163 Limit int 164 165 Refspec string 166} 167 168func (r *lockSearchRequest) QueryValues() map[string]string { 169 q := make(map[string]string) 170 for _, filter := range r.Filters { 171 q[filter.Property] = filter.Value 172 } 173 174 if len(r.Cursor) > 0 { 175 q["cursor"] = r.Cursor 176 } 177 178 if r.Limit > 0 { 179 q["limit"] = strconv.Itoa(r.Limit) 180 } 181 182 if len(r.Refspec) > 0 { 183 q["refspec"] = r.Refspec 184 } 185 186 return q 187} 188 189// LockList encapsulates a set of Locks. 190type lockList struct { 191 // Locks is the set of locks returned back, typically matching the query 192 // parameters sent in the LockListRequest call. If no locks were matched 193 // from a given query, then `Locks` will be represented as an empty 194 // array. 195 Locks []Lock `json:"locks"` 196 // NextCursor returns the Id of the Lock the client should update its 197 // cursor to, if there are multiple pages of results for a particular 198 // `LockListRequest`. 199 NextCursor string `json:"next_cursor,omitempty"` 200 // Message populates any error that was encountered during the search. If no 201 // error was encountered and the operation was successful, then a value 202 // of nil will be passed here. 203 Message string `json:"message,omitempty"` 204 DocumentationURL string `json:"documentation_url,omitempty"` 205 RequestID string `json:"request_id,omitempty"` 206} 207 208func (c *httpLockClient) Search(remote string, searchReq *lockSearchRequest) (*lockList, int, error) { 209 e := c.Endpoints.Endpoint("download", remote) 210 req, err := c.NewRequest("GET", e, "locks", nil) 211 if err != nil { 212 return nil, 0, err 213 } 214 215 q := req.URL.Query() 216 for key, value := range searchReq.QueryValues() { 217 q.Add(key, value) 218 } 219 req.URL.RawQuery = q.Encode() 220 221 req = c.Client.LogRequest(req, "lfs.locks.search") 222 res, err := c.DoAPIRequestWithAuth(remote, req) 223 if err != nil { 224 if res != nil { 225 return nil, res.StatusCode, err 226 } 227 return nil, 0, err 228 } 229 230 locks := &lockList{} 231 if res.StatusCode == http.StatusOK { 232 err = lfshttp.DecodeJSON(res, locks) 233 } 234 235 return locks, res.StatusCode, err 236} 237 238// lockVerifiableRequest encapsulates the request sent to the server when the 239// client would like a list of locks to verify a Git push. 240type lockVerifiableRequest struct { 241 Ref *lockRef `json:"ref,omitempty"` 242 243 // Cursor is an optional field used to tell the server which lock was 244 // seen last, if scanning through multiple pages of results. 245 // 246 // Servers must return a list of locks sorted in reverse chronological 247 // order, so the Cursor provides a consistent method of viewing all 248 // locks, even if more were created between two requests. 249 Cursor string `json:"cursor,omitempty"` 250 // Limit is the maximum number of locks to return in a single page. 251 Limit int `json:"limit,omitempty"` 252} 253 254// lockVerifiableList encapsulates a set of Locks to verify a Git push. 255type lockVerifiableList struct { 256 // Ours is the set of locks returned back matching filenames that the user 257 // is allowed to edit. 258 Ours []Lock `json:"ours"` 259 260 // Their is the set of locks returned back matching filenames that the user 261 // is NOT allowed to edit. Any edits matching these files should reject 262 // the Git push. 263 Theirs []Lock `json:"theirs"` 264 265 // NextCursor returns the Id of the Lock the client should update its 266 // cursor to, if there are multiple pages of results for a particular 267 // `LockListRequest`. 268 NextCursor string `json:"next_cursor,omitempty"` 269 // Message populates any error that was encountered during the search. If no 270 // error was encountered and the operation was successful, then a value 271 // of nil will be passed here. 272 Message string `json:"message,omitempty"` 273 DocumentationURL string `json:"documentation_url,omitempty"` 274 RequestID string `json:"request_id,omitempty"` 275} 276 277func (c *httpLockClient) SearchVerifiable(remote string, vreq *lockVerifiableRequest) (*lockVerifiableList, int, error) { 278 e := c.Endpoints.Endpoint("upload", remote) 279 req, err := c.NewRequest("POST", e, "locks/verify", vreq) 280 if err != nil { 281 return nil, 0, err 282 } 283 284 req = c.Client.LogRequest(req, "lfs.locks.verify") 285 res, err := c.DoAPIRequestWithAuth(remote, req) 286 if err != nil { 287 if res != nil { 288 return nil, res.StatusCode, err 289 } 290 return nil, 0, err 291 } 292 293 locks := &lockVerifiableList{} 294 if res.StatusCode == http.StatusOK { 295 err = lfshttp.DecodeJSON(res, locks) 296 } 297 298 return locks, res.StatusCode, err 299} 300 301// User represents the owner of a lock. 302type User struct { 303 // Name is the name of the individual who would like to obtain the 304 // lock, for instance: "Rick Sanchez". 305 Name string `json:"name"` 306} 307 308func NewUser(name string) *User { 309 return &User{Name: name} 310} 311 312// String implements the fmt.Stringer interface. 313func (u *User) String() string { 314 return u.Name 315} 316 317type lockClientInfo struct { 318 remote string 319 operation string 320} 321 322type genericLockClient struct { 323 client *lfsapi.Client 324 lclients map[lockClientInfo]lockClient 325} 326 327func newGenericLockClient(client *lfsapi.Client) *genericLockClient { 328 return &genericLockClient{ 329 client: client, 330 lclients: make(map[lockClientInfo]lockClient), 331 } 332} 333 334func (c *genericLockClient) getClient(remote, operation string) lockClient { 335 info := lockClientInfo{ 336 remote: remote, 337 operation: operation, 338 } 339 if client := c.lclients[info]; client != nil { 340 return client 341 } 342 transfer := c.client.SSHTransfer(operation, remote) 343 var lclient lockClient 344 if transfer != nil { 345 lclient = &sshLockClient{transfer: transfer, Client: c.client} 346 } else { 347 lclient = &httpLockClient{Client: c.client} 348 } 349 c.lclients[info] = lclient 350 return lclient 351} 352 353func (c *genericLockClient) Lock(remote string, lockReq *lockRequest) (*lockResponse, int, error) { 354 return c.getClient(remote, "upload").Lock(remote, lockReq) 355} 356 357func (c *genericLockClient) Unlock(ref *git.Ref, remote, id string, force bool) (*unlockResponse, int, error) { 358 return c.getClient(remote, "upload").Unlock(ref, remote, id, force) 359} 360 361func (c *genericLockClient) Search(remote string, searchReq *lockSearchRequest) (*lockList, int, error) { 362 return c.getClient(remote, "download").Search(remote, searchReq) 363} 364 365func (c *genericLockClient) SearchVerifiable(remote string, vreq *lockVerifiableRequest) (*lockVerifiableList, int, error) { 366 return c.getClient(remote, "upload").SearchVerifiable(remote, vreq) 367} 368