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