commit
61a359f3de
@ -32,7 +32,7 @@ func locksCommand(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
for _, lock := range locks {
|
||||
Print("%s\t%s", lock.Path, lock.Committer)
|
||||
Print("%s\t%s", lock.Path, lock.Owner)
|
||||
lockCount++
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,7 @@ func (c *uploadContext) Await() {
|
||||
|
||||
Print("Unable to push %d locked file(s):", ul)
|
||||
for _, unowned := range c.unownedLocks {
|
||||
Print("* %s - %s", unowned.Path, unowned.Committer)
|
||||
Print("* %s - %s", unowned.Path, unowned.Owner)
|
||||
}
|
||||
} else if len(c.ownedLocks) > 0 {
|
||||
Print("Consider unlocking your own locked file(s): (`git lfs unlock <path>`)")
|
||||
|
@ -6,8 +6,15 @@ goes through looks like this:
|
||||
|
||||
1. [Discover the LFS Server to use](./server-discovery.md).
|
||||
2. [Apply Authentication](./authentication.md).
|
||||
3. [Request the Batch API](./batch.md) to upload or download objects.
|
||||
4. The Batch API's response dictates how the client will transfer the objects.
|
||||
3. Make the request. See the Batch and File Locking API sections.
|
||||
|
||||
## Batch API
|
||||
|
||||
The Batch API is used to request the ability to transfer LFS objects with the
|
||||
LFS server.
|
||||
|
||||
API Specification:
|
||||
* [Batch API](./batch.md)
|
||||
|
||||
Current transfer adapters include:
|
||||
* [Basic](./basic-transfers.md)
|
||||
@ -15,3 +22,11 @@ Current transfer adapters include:
|
||||
Experimental transfer adapters include:
|
||||
* Tus.io (upload only)
|
||||
* [Custom](../custom-transfers.md)
|
||||
|
||||
## File Locking API
|
||||
|
||||
The File Locking API is used to create, list, and delete locks, as well as
|
||||
verify that locks are respected in Git pushes.
|
||||
|
||||
API Specification:
|
||||
* [File Locking API](./locking.md)
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Git LFS Batch API
|
||||
|
||||
Added: v0.6
|
||||
|
||||
The Batch API is used to request the ability to transfer LFS objects with the
|
||||
LFS server. The Batch URL is built by adding `/objects/batch` to the LFS server
|
||||
URL.
|
||||
@ -172,7 +174,7 @@ errors.
|
||||
|
||||
{
|
||||
"message": "Not found",
|
||||
"documentation_url": "https://git-lfs-server.com/docs/errors",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
@ -189,7 +191,7 @@ a custom header key so it does not trigger password prompts in browsers.
|
||||
|
||||
{
|
||||
"message": "Credentials needed",
|
||||
"documentation_url": "https://git-lfs-server.com/docs/errors",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
436
docs/api/locking.md
Normal file
436
docs/api/locking.md
Normal file
@ -0,0 +1,436 @@
|
||||
# Git LFS File Locking API
|
||||
|
||||
Added: v2.0
|
||||
|
||||
The File Locking API is used to create, list, and delete locks, as well as
|
||||
verify that locks are respected in Git pushes. The locking URLs are built
|
||||
by adding a suffix to the LFS Server URL.
|
||||
|
||||
Git remote: https://git-server.com/foo/bar
|
||||
LFS server: https://git-server.com/foo/bar.git/info/lfs
|
||||
Locks API: https://git-server.com/foo/bar.git/info/lfs/locks
|
||||
|
||||
See the [Server Discovery doc](./server-discovery.md) for more info on how LFS
|
||||
builds the LFS server URL.
|
||||
|
||||
All File Locking requests require the following HTTP headers:
|
||||
|
||||
Accept: application/vnd.git-lfs+json
|
||||
Content-Type: application/vnd.git-lfs+json
|
||||
|
||||
See the [Authentication doc](./authentication.md) for more info on how LFS
|
||||
gets authorizes Batch API requests.
|
||||
|
||||
Note: This is the first version of the File Locking API, supporting only the
|
||||
simplest use case: single branch locking. The API is designed to be extensible
|
||||
as we experiment with more advanced locking scenarios, as defined in the
|
||||
[original proposal](/docs/proposals/locking.md).
|
||||
|
||||
## Create Lock
|
||||
|
||||
The client sends the following to create a lock by sending a `POST` to `/locks`
|
||||
(appended to the LFS server url, as described above).
|
||||
|
||||
* `path` - String path name of the file that is locked. This should be
|
||||
relative to the root of the repository working directory.
|
||||
|
||||
```js
|
||||
// POST https://lfs-server.com/locks
|
||||
// Accept: application/vnd.git-lfs+json
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
// Authorization: Basic ...
|
||||
{
|
||||
"path": "foo/bar.zip",
|
||||
}
|
||||
```
|
||||
|
||||
### Successful Response
|
||||
|
||||
Successful responses return the created lock:
|
||||
|
||||
* `id` - String ID of the Lock. Git LFS doesn't enforce what type of ID is used,
|
||||
as long as it's returned a string.
|
||||
* `path` - String path name of the locked file. This should be relative to the
|
||||
root of the repository working directory.
|
||||
* `locked_at` - The string ISO 8601 formatted timestamp the lock was created.
|
||||
* `owner` - The name of the user that created the Lock. This should be set from
|
||||
the user credentials posted when creating the lock.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 201 Created
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"lock": {
|
||||
"id": "some-uuid",
|
||||
"path": "/path/to/file",
|
||||
"locked_at": "2016-05-17T15:49:06+00:00",
|
||||
"owner": {
|
||||
"name": "Jane Doe",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Bad Response: Lock Exists
|
||||
|
||||
Lock services should reject lock creations if one already exists for the given
|
||||
path on the current repository.
|
||||
|
||||
* `lock` - The existing Lock that clashes with the request.
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 409 Conflict
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"lock": {
|
||||
// details of existing lock
|
||||
},
|
||||
"message": "already created lock",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized Response
|
||||
|
||||
Lock servers should require that users have push access to the repository before
|
||||
they can create locks.
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 403 Forbidden
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "You must have push access to create a lock",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 500 Internal server error
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "already created lock",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
## List Locks
|
||||
|
||||
The client can request the current active locks for a repository by sending a
|
||||
`GET` to `/locks` (appended to the LFS server url, as described above). The
|
||||
properties are sent as URI query values, instead of through a JSON body:
|
||||
|
||||
* `path` - Optional string path to match against locks on the server.
|
||||
* `id` - Optional string ID to match against a lock on the server.
|
||||
* `cursor` - The optional string value to continue listing locks. This value
|
||||
should be the `next_cursor` from a previous request.
|
||||
* `limit` - The integer limit of the number of locks to return. The server
|
||||
should have its own upper and lower bounds on the supported limits.
|
||||
|
||||
```js
|
||||
// GET https://lfs-server.com/locks?path=&id=&cursor=&limit=
|
||||
// Accept: application/vnd.git-lfs+json
|
||||
// Authorization: Basic ... (if needed)
|
||||
```
|
||||
|
||||
### Successful Response
|
||||
|
||||
A successful response will list the matching locks:
|
||||
|
||||
* `locks` - Array of matching Lock objects. See the "Create Lock" successful
|
||||
response section to see what Lock properties are possible.
|
||||
* `next_cursor` - Optional string cursor that the server can return if there
|
||||
are more locks matching the given filters. The client will re-do the request,
|
||||
setting the `?cursor` query value with this `next_cursor` value.
|
||||
|
||||
Note: If the server has no locks, it must return an empty `locks` array.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 200 Ok
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"locks": [
|
||||
{
|
||||
"id": "some-uuid",
|
||||
"path": "/path/to/file",
|
||||
"locked_at": "2016-05-17T15:49:06+00:00",
|
||||
"owner": {
|
||||
"name": "Jane Doe"
|
||||
}
|
||||
}
|
||||
],
|
||||
"next_cursor": "optional next ID",
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized Response
|
||||
|
||||
Lock servers should require that users have pull access to the repository before
|
||||
they can list locks.
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 403 Forbidden
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "You must have pull access to list locks",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 500 Internal server error
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "unable to list locks",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
## List Locks for Verification
|
||||
|
||||
The client can use the Lock Verification endpoint to check for active locks
|
||||
that can affect a Git push. For a caller, this endpoint is very similar to the
|
||||
"List Locks" endpoint above, except:
|
||||
|
||||
* Verification requires a `POST` request.
|
||||
* The `cursor` and `limit` values are sent as properties in the json request
|
||||
body.
|
||||
* The response includes locks partitioned into `ours` and `theirs` properties.
|
||||
|
||||
Clients send the following to list locks for verification by sending a `POST`
|
||||
to `/locks/verify` (appended to the LFS server url, as described above):
|
||||
|
||||
* `cursor`
|
||||
* `limit`
|
||||
|
||||
```js
|
||||
// POST https://lfs-server.com/locks/verify
|
||||
// Accept: application/vnd.git-lfs+json
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
// Authorization: Basic ...
|
||||
{
|
||||
"cursor": "optional cursor",
|
||||
"limit": 100 // also optional
|
||||
}
|
||||
```
|
||||
|
||||
Note: As more advanced locking workflows are implemented, more details will
|
||||
likely be added to this request body in future iterations.
|
||||
|
||||
### Successful Response
|
||||
|
||||
A successful response will list the relevant locks:
|
||||
|
||||
* `ours` - Array of Lock objects currently owned by the authenticated user.
|
||||
modify.
|
||||
* `theirs` - Array of Lock objects currently owned by other users.
|
||||
* `next_cursor` - Optional string cursor that the server can return if there
|
||||
are more locks matching the given filters. The client will re-do the request,
|
||||
setting the `cursor` property with this `next_cursor` value.
|
||||
|
||||
If a Git push updates any files matching any of "our" locks, Git LFS will list
|
||||
them in the push output, in case the user will want to unlock them after the
|
||||
push. However, any updated files matching one of "their" locks will halt the
|
||||
push. At this point, it is up to the user to resolve the lock conflict with
|
||||
their team.
|
||||
|
||||
Note: If the server has no locks, it must return an empty array in the `ours` or
|
||||
`theirs` properties.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 200 Ok
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"ours": [
|
||||
{
|
||||
"id": "some-uuid",
|
||||
"path": "/path/to/file",
|
||||
"locked_at": "2016-05-17T15:49:06+00:00",
|
||||
"owner": {
|
||||
"name": "Jane Doe"
|
||||
}
|
||||
}
|
||||
],
|
||||
"theirs": [],
|
||||
"next_cursor": "optional next ID",
|
||||
}
|
||||
```
|
||||
|
||||
### Not Found Response
|
||||
|
||||
By default, an LFS server that doesn't implement any locking endpoints should
|
||||
return 404. This response will not halt any Git pushes.
|
||||
|
||||
Any 404 will do, but Git LFS will show a better error message with a json
|
||||
response.
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 404 Not found
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "Not found",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized Response
|
||||
|
||||
Lock servers should require that users have push access to the repository before
|
||||
they can get a list of locks to verify a Git push.
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 403 Forbidden
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "You must have push access to verify locks",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 500 Internal server error
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "unable to list locks",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
## Delete Lock
|
||||
|
||||
The client can delete a lock, given its ID, by sending a `POST` to
|
||||
`/locks/:id/unlock` (appended to the LFS server url, as described above):
|
||||
|
||||
* `force` - Optional boolean specifying that the user is deleting another user's
|
||||
lock.
|
||||
|
||||
```js
|
||||
// POST https://lfs-server.com/locks/:id/unlock
|
||||
// Accept: application/vnd.git-lfs+json
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
// Authorization: Basic ...
|
||||
|
||||
{
|
||||
"force": true
|
||||
}
|
||||
```
|
||||
|
||||
### Successful Response
|
||||
|
||||
Successful deletions return the deleted lock. See the "Create Lock" successful
|
||||
response section to see what Lock properties are possible.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 200 Ok
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"lock": {
|
||||
"id": "some-uuid",
|
||||
"path": "/path/to/file",
|
||||
"locked_at": "2016-05-17T15:49:06+00:00",
|
||||
"owner": {
|
||||
"name": "Jane Doe"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized Response
|
||||
|
||||
Lock servers should require that users have push access to the repository before
|
||||
they can delete locks. Also, if the `force` parameter is omitted, or false,
|
||||
the user should only be allowed to delete locks that they created.
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 403 Forbidden
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "You must have push access to verify locks",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
||||
|
||||
### Error response
|
||||
|
||||
* `message` - String error message.
|
||||
* `request_id` - Optional String unique identifier for the request. Useful for
|
||||
debugging.
|
||||
* `documentation_url` - Optional String to give the user a place to report
|
||||
errors.
|
||||
|
||||
```js
|
||||
// HTTP/1.1 500 Internal server error
|
||||
// Content-Type: application/vnd.git-lfs+json
|
||||
{
|
||||
"message": "already deleting lock",
|
||||
"documentation_url": "https://lfs-server.com/docs/errors",
|
||||
"request_id": "123"
|
||||
}
|
||||
```
|
@ -20,6 +20,16 @@ func IsHTTP(err error) (*http.Response, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func ClientErrorMessage(msg, docURL, reqID string) string {
|
||||
if len(docURL) > 0 {
|
||||
msg += "\nDocs: " + docURL
|
||||
}
|
||||
if len(reqID) > 0 {
|
||||
msg += "\nRequest ID: " + reqID
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
type ClientError struct {
|
||||
Message string `json:"message"`
|
||||
DocumentationUrl string `json:"documentation_url,omitempty"`
|
||||
@ -32,14 +42,7 @@ func (e *ClientError) HTTPResponse() *http.Response {
|
||||
}
|
||||
|
||||
func (e *ClientError) Error() string {
|
||||
msg := e.Message
|
||||
if len(e.DocumentationUrl) > 0 {
|
||||
msg += "\nDocs: " + e.DocumentationUrl
|
||||
}
|
||||
if len(e.RequestId) > 0 {
|
||||
msg += "\nRequest ID: " + e.RequestId
|
||||
}
|
||||
return msg
|
||||
return ClientErrorMessage(e.Message, e.DocumentationUrl, e.RequestId)
|
||||
}
|
||||
|
||||
func (c *Client) handleResponse(res *http.Response) error {
|
||||
|
@ -17,12 +17,6 @@ type lockClient struct {
|
||||
type lockRequest struct {
|
||||
// Path is the path that the client would like to obtain a lock against.
|
||||
Path string `json:"path"`
|
||||
// LatestRemoteCommit is the SHA of the last known commit from the
|
||||
// remote that we are trying to create the lock against, as found in
|
||||
// `.git/refs/origin/<name>`.
|
||||
LatestRemoteCommit string `json:"latest_remote_commit"`
|
||||
// Committer is the individual that wishes to obtain the lock.
|
||||
Committer *Committer `json:"committer"`
|
||||
}
|
||||
|
||||
// LockResponse encapsulates the information sent over the API in response to
|
||||
@ -41,12 +35,12 @@ type lockResponse struct {
|
||||
// If an error was experienced in creating this lock, then the
|
||||
// zero-value of Lock should be sent here instead.
|
||||
Lock *Lock `json:"lock"`
|
||||
// CommitNeeded holds the minimum commit SHA that client must have to
|
||||
// obtain the lock.
|
||||
CommitNeeded string `json:"commit_needed,omitempty"`
|
||||
// Err is the optional error that was encountered while trying to create
|
||||
|
||||
// Message is the optional error that was encountered while trying to create
|
||||
// the above lock.
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c *lockClient) Lock(remote string, lockReq *lockRequest) (*lockResponse, *http.Response, error) {
|
||||
@ -67,9 +61,6 @@ func (c *lockClient) Lock(remote string, lockReq *lockRequest) (*lockResponse, *
|
||||
|
||||
// UnlockRequest encapsulates the data sent in an API request to remove a lock.
|
||||
type unlockRequest struct {
|
||||
// Id is the Id of the lock that the user wishes to unlock.
|
||||
Id string `json:"id"`
|
||||
|
||||
// Force determines whether or not the lock should be "forcibly"
|
||||
// unlocked; that is to say whether or not a given individual should be
|
||||
// able to break a different individual's lock.
|
||||
@ -83,15 +74,18 @@ type unlockResponse struct {
|
||||
// `UnlockPayload` (see above). If no matching lock was found, this
|
||||
// field will take the zero-value of Lock, and Err will be non-nil.
|
||||
Lock *Lock `json:"lock"`
|
||||
// Err is an optional field which holds any error that was experienced
|
||||
|
||||
// Message is an optional field which holds any error that was experienced
|
||||
// while removing the lock.
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c *lockClient) Unlock(remote, id string, force bool) (*unlockResponse, *http.Response, error) {
|
||||
e := c.Endpoints.Endpoint("upload", remote)
|
||||
suffix := fmt.Sprintf("locks/%s/unlock", id)
|
||||
req, err := c.NewRequest("POST", e, suffix, &unlockRequest{Id: id, Force: force})
|
||||
req, err := c.NewRequest("POST", e, suffix, &unlockRequest{Force: force})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -158,10 +152,12 @@ type lockList struct {
|
||||
// cursor to, if there are multiple pages of results for a particular
|
||||
// `LockListRequest`.
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
// Err populates any error that was encountered during the search. If no
|
||||
// Message populates any error that was encountered during the search. If no
|
||||
// error was encountered and the operation was succesful, then a value
|
||||
// of nil will be passed here.
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c *lockClient) Search(remote string, searchReq *lockSearchRequest) (*lockList, *http.Response, error) {
|
||||
@ -219,10 +215,12 @@ type lockVerifiableList struct {
|
||||
// cursor to, if there are multiple pages of results for a particular
|
||||
// `LockListRequest`.
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
// Err populates any error that was encountered during the search. If no
|
||||
// Message populates any error that was encountered during the search. If no
|
||||
// error was encountered and the operation was succesful, then a value
|
||||
// of nil will be passed here.
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
|
||||
func (c *lockClient) SearchVerifiable(remote string, vreq *lockVerifiableRequest) (*lockVerifiableList, *http.Response, error) {
|
||||
@ -245,22 +243,18 @@ func (c *lockClient) SearchVerifiable(remote string, vreq *lockVerifiableRequest
|
||||
return locks, res, err
|
||||
}
|
||||
|
||||
// Committer represents a "First Last <email@domain.com>" pair.
|
||||
type Committer struct {
|
||||
// User represents the owner of a lock.
|
||||
type User struct {
|
||||
// Name is the name of the individual who would like to obtain the
|
||||
// lock, for instance: "Rick Olson".
|
||||
// lock, for instance: "Rick Sanchez".
|
||||
Name string `json:"name"`
|
||||
// Email is the email assopsicated with the individual who would
|
||||
// like to obtain the lock, for instance: "rick@github.com".
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func NewCommitter(name, email string) *Committer {
|
||||
return &Committer{Name: name, Email: email}
|
||||
func NewUser(name string) *User {
|
||||
return &User{Name: name}
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface by returning a string
|
||||
// representation of the Committer in the format "First Last <email>".
|
||||
func (c *Committer) String() string {
|
||||
return fmt.Sprintf("%s <%s>", c.Name, c.Email)
|
||||
// String implements the fmt.Stringer interface.
|
||||
func (u *User) String() string {
|
||||
return u.Name
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func TestAPILock(t *testing.T) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, lfsapi.MediaType, r.Header.Get("Accept"))
|
||||
assert.Equal(t, lfsapi.MediaType, r.Header.Get("Content-Type"))
|
||||
assert.Equal(t, "61", r.Header.Get("Content-Length"))
|
||||
assert.Equal(t, "18", r.Header.Get("Content-Length"))
|
||||
|
||||
lockReq := &lockRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(lockReq)
|
||||
@ -68,7 +68,6 @@ func TestAPIUnlock(t *testing.T) {
|
||||
err := json.NewDecoder(r.Body).Decode(unlockReq)
|
||||
r.Body.Close()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "123", unlockReq.Id)
|
||||
assert.True(t, unlockReq.Force)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/filepathfilter"
|
||||
"github.com/git-lfs/git-lfs/git"
|
||||
"github.com/git-lfs/git-lfs/lfsapi"
|
||||
"github.com/git-lfs/git-lfs/tools"
|
||||
"github.com/git-lfs/git-lfs/tools/kv"
|
||||
@ -89,25 +88,18 @@ func (c *Client) Close() error {
|
||||
// path must be relative to the root of the repository
|
||||
// Returns the lock id if successful, or an error
|
||||
func (c *Client) LockFile(path string) (Lock, error) {
|
||||
// TODO: this is not really the constraint we need to avoid merges, improve as per proposal
|
||||
latest, err := git.CurrentRemoteRef()
|
||||
if err != nil {
|
||||
return Lock{}, err
|
||||
}
|
||||
|
||||
lockReq := &lockRequest{
|
||||
Path: path,
|
||||
LatestRemoteCommit: latest.Sha,
|
||||
Committer: NewCommitter(c.client.CurrentUser()),
|
||||
}
|
||||
|
||||
lockRes, _, err := c.client.Lock(c.Remote, lockReq)
|
||||
lockRes, _, err := c.client.Lock(c.Remote, &lockRequest{Path: path})
|
||||
if err != nil {
|
||||
return Lock{}, errors.Wrap(err, "api")
|
||||
}
|
||||
|
||||
if len(lockRes.Err) > 0 {
|
||||
return Lock{}, fmt.Errorf("Server unable to create lock: %v", lockRes.Err)
|
||||
if len(lockRes.Message) > 0 {
|
||||
return Lock{}, fmt.Errorf("Server unable to create lock: %s",
|
||||
lfsapi.ClientErrorMessage(
|
||||
lockRes.Message,
|
||||
lockRes.DocumentationURL,
|
||||
lockRes.RequestID,
|
||||
))
|
||||
}
|
||||
|
||||
lock := *lockRes.Lock
|
||||
@ -153,8 +145,13 @@ func (c *Client) UnlockFileById(id string, force bool) error {
|
||||
return errors.Wrap(err, "api")
|
||||
}
|
||||
|
||||
if len(unlockRes.Err) > 0 {
|
||||
return fmt.Errorf("Server unable to unlock: %s", unlockRes.Err)
|
||||
if len(unlockRes.Message) > 0 {
|
||||
return fmt.Errorf("Server unable to unlock: %s",
|
||||
lfsapi.ClientErrorMessage(
|
||||
unlockRes.Message,
|
||||
unlockRes.DocumentationURL,
|
||||
unlockRes.RequestID,
|
||||
))
|
||||
}
|
||||
|
||||
if err := c.cache.RemoveById(id); err != nil {
|
||||
@ -172,9 +169,8 @@ type Lock struct {
|
||||
// Path is an absolute path to the file that is locked as a part of this
|
||||
// lock.
|
||||
Path string `json:"path"`
|
||||
// Committer is the identity of the person who holds the ownership of
|
||||
// this lock.
|
||||
Committer *Committer `json:"committer"`
|
||||
// Owner is the identity of the user that created this lock.
|
||||
Owner *User `json:"owner"`
|
||||
// LockedAt is the time at which this lock was acquired.
|
||||
LockedAt time.Time `json:"locked_at"`
|
||||
}
|
||||
@ -203,8 +199,13 @@ func (c *Client) VerifiableLocks(limit int) (ourLocks, theirLocks []Lock, err er
|
||||
return ourLocks, theirLocks, err
|
||||
}
|
||||
|
||||
if list.Err != "" {
|
||||
return ourLocks, theirLocks, errors.New(list.Err)
|
||||
if list.Message != "" {
|
||||
return ourLocks, theirLocks, fmt.Errorf("Server error searching locks: %s",
|
||||
lfsapi.ClientErrorMessage(
|
||||
list.Message,
|
||||
list.DocumentationURL,
|
||||
list.RequestID,
|
||||
))
|
||||
}
|
||||
|
||||
for _, l := range list.Ours {
|
||||
@ -266,8 +267,13 @@ func (c *Client) searchRemoteLocks(filter map[string]string, limit int) ([]Lock,
|
||||
return locks, errors.Wrap(err, "locking")
|
||||
}
|
||||
|
||||
if list.Err != "" {
|
||||
return locks, errors.Wrap(err, "locking")
|
||||
if list.Message != "" {
|
||||
return locks, fmt.Errorf("Server error searching for locks: %s",
|
||||
lfsapi.ClientErrorMessage(
|
||||
list.Message,
|
||||
list.DocumentationURL,
|
||||
list.RequestID,
|
||||
))
|
||||
}
|
||||
|
||||
for _, l := range list.Locks {
|
||||
@ -318,33 +324,26 @@ func (c *Client) lockIdFromPath(path string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch locked files for the current committer and cache them locally
|
||||
// Fetch locked files for the current user and cache them locally
|
||||
// This can be used to sync up locked files when moving machines
|
||||
func (c *Client) refreshLockCache() error {
|
||||
// TODO: filters don't seem to currently define how to search for a
|
||||
// committer's email. Is it "committer.email"? For now, just iterate
|
||||
locks, err := c.SearchLocks(nil, 0, false)
|
||||
ourLocks, _, err := c.VerifiableLocks(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We're going to overwrite the entire local cache
|
||||
c.cache.Clear()
|
||||
|
||||
_, email := c.client.CurrentUser()
|
||||
for _, l := range locks {
|
||||
if l.Committer.Email == email {
|
||||
c.cache.Add(l)
|
||||
}
|
||||
for _, l := range ourLocks {
|
||||
c.cache.Add(l)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsFileLockedByCurrentCommitter returns whether a file is locked by the
|
||||
// current committer, as cached locally
|
||||
// current user, as cached locally
|
||||
func (c *Client) IsFileLockedByCurrentCommitter(path string) bool {
|
||||
|
||||
filter := map[string]string{"path": path}
|
||||
locks, err := c.searchCachedLocks(filter, 1)
|
||||
if err != nil {
|
||||
|
@ -26,17 +26,19 @@ func TestRefreshCache(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, "/api/locks", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/api/locks/verify", r.URL.Path)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(lockList{
|
||||
Locks: []Lock{
|
||||
Lock{Id: "99", Path: "folder/test3.dat", Committer: &Committer{Name: "Alice", Email: "alice@wonderland.com"}},
|
||||
Lock{Id: "101", Path: "folder/test1.dat", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}},
|
||||
Lock{Id: "102", Path: "folder/test2.dat", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}},
|
||||
Lock{Id: "103", Path: "root.dat", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}},
|
||||
Lock{Id: "199", Path: "other/test1.dat", Committer: &Committer{Name: "Charles", Email: "charles@incharge.com"}},
|
||||
err = json.NewEncoder(w).Encode(lockVerifiableList{
|
||||
Theirs: []Lock{
|
||||
Lock{Id: "99", Path: "folder/test3.dat", Owner: &User{Name: "Alice"}},
|
||||
Lock{Id: "199", Path: "other/test1.dat", Owner: &User{Name: "Charles"}},
|
||||
},
|
||||
Ours: []Lock{
|
||||
Lock{Id: "101", Path: "folder/test1.dat", Owner: &User{Name: "Fred"}},
|
||||
Lock{Id: "102", Path: "folder/test2.dat", Owner: &User{Name: "Fred"}},
|
||||
Lock{Id: "103", Path: "root.dat", Owner: &User{Name: "Fred"}},
|
||||
},
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -74,9 +76,9 @@ func TestRefreshCache(t *testing.T) {
|
||||
// Sort locks for stable comparison
|
||||
sort.Sort(LocksById(locks))
|
||||
assert.Equal(t, []Lock{
|
||||
Lock{Path: "folder/test1.dat", Id: "101", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}, LockedAt: zeroTime},
|
||||
Lock{Path: "folder/test2.dat", Id: "102", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}, LockedAt: zeroTime},
|
||||
Lock{Path: "root.dat", Id: "103", Committer: &Committer{Name: "Fred", Email: "fred@bloggs.com"}, LockedAt: zeroTime},
|
||||
Lock{Path: "folder/test1.dat", Id: "101", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
Lock{Path: "folder/test2.dat", Id: "102", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
Lock{Path: "root.dat", Id: "103", Owner: &User{Name: "Fred"}, LockedAt: zeroTime},
|
||||
}, locks)
|
||||
}
|
||||
|
||||
|
@ -757,46 +757,39 @@ func redirect307Handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(307)
|
||||
}
|
||||
|
||||
type Committer struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Lock struct {
|
||||
Id string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Committer Committer `json:"committer"`
|
||||
CommitSHA string `json:"commit_sha"`
|
||||
LockedAt time.Time `json:"locked_at"`
|
||||
UnlockedAt time.Time `json:"unlocked_at,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Owner User `json:"owner"`
|
||||
LockedAt time.Time `json:"locked_at"`
|
||||
}
|
||||
|
||||
type LockRequest struct {
|
||||
Path string `json:"path"`
|
||||
LatestRemoteCommit string `json:"latest_remote_commit"`
|
||||
Committer Committer `json:"committer"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type LockResponse struct {
|
||||
Lock *Lock `json:"lock"`
|
||||
CommitNeeded string `json:"commit_needed,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Lock *Lock `json:"lock"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type UnlockRequest struct {
|
||||
Id string `json:"id"`
|
||||
Force bool `json:"force"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
type UnlockResponse struct {
|
||||
Lock *Lock `json:"lock"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Lock *Lock `json:"lock"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type LockList struct {
|
||||
Locks []Lock `json:"locks"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type VerifiableLockRequest struct {
|
||||
@ -808,7 +801,7 @@ type VerifiableLockList struct {
|
||||
Ours []Lock `json:"ours"`
|
||||
Theirs []Lock `json:"theirs"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
@ -908,7 +901,10 @@ func (c LocksByCreatedAt) Len() int { return len(c) }
|
||||
func (c LocksByCreatedAt) Less(i, j int) bool { return c[i].LockedAt.Before(c[j].LockedAt) }
|
||||
func (c LocksByCreatedAt) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
|
||||
var lockRe = regexp.MustCompile(`/locks/?$`)
|
||||
var (
|
||||
lockRe = regexp.MustCompile(`/locks/?$`)
|
||||
unlockRe = regexp.MustCompile(`locks/([^/]+)/unlock\z`)
|
||||
)
|
||||
|
||||
func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
dec := json.NewDecoder(r.Body)
|
||||
@ -936,7 +932,7 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
r.FormValue("limit"))
|
||||
|
||||
if err != nil {
|
||||
ll.Err = err.Error()
|
||||
ll.Message = err.Error()
|
||||
} else {
|
||||
ll.Locks = locks
|
||||
ll.NextCursor = nextCursor
|
||||
@ -948,21 +944,25 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if strings.HasSuffix(r.URL.Path, "unlock") {
|
||||
var unlockRequest UnlockRequest
|
||||
|
||||
var lockId string
|
||||
if matches := unlockRe.FindStringSubmatch(r.URL.Path); len(matches) > 1 {
|
||||
lockId = matches[1]
|
||||
}
|
||||
|
||||
if len(lockId) == 0 {
|
||||
enc.Encode(&UnlockResponse{Message: "Invalid lock"})
|
||||
}
|
||||
|
||||
if err := dec.Decode(&unlockRequest); err != nil {
|
||||
enc.Encode(&UnlockResponse{
|
||||
Err: err.Error(),
|
||||
})
|
||||
enc.Encode(&UnlockResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if l := delLock(repo, unlockRequest.Id); l != nil {
|
||||
enc.Encode(&UnlockResponse{
|
||||
Lock: l,
|
||||
})
|
||||
if l := delLock(repo, lockId); l != nil {
|
||||
enc.Encode(&UnlockResponse{Lock: l})
|
||||
} else {
|
||||
enc.Encode(&UnlockResponse{
|
||||
Err: "unable to find lock",
|
||||
})
|
||||
enc.Encode(&UnlockResponse{Message: "unable to find lock"})
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -993,7 +993,7 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
reqBody.Cursor,
|
||||
strconv.Itoa(reqBody.Limit))
|
||||
if err != nil {
|
||||
ll.Err = err.Error()
|
||||
ll.Message = err.Error()
|
||||
} else {
|
||||
ll.NextCursor = nextCursor
|
||||
|
||||
@ -1013,16 +1013,12 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
if strings.HasSuffix(r.URL.Path, "/locks") {
|
||||
var lockRequest LockRequest
|
||||
if err := dec.Decode(&lockRequest); err != nil {
|
||||
enc.Encode(&LockResponse{
|
||||
Err: err.Error(),
|
||||
})
|
||||
enc.Encode(&LockResponse{Message: err.Error()})
|
||||
}
|
||||
|
||||
for _, l := range getLocks(repo) {
|
||||
if l.Path == lockRequest.Path {
|
||||
enc.Encode(&LockResponse{
|
||||
Err: "lock already created",
|
||||
})
|
||||
enc.Encode(&LockResponse{Message: "lock already created"})
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -1031,11 +1027,10 @@ func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
|
||||
rand.Read(id[:])
|
||||
|
||||
lock := &Lock{
|
||||
Id: fmt.Sprintf("%x", id[:]),
|
||||
Path: lockRequest.Path,
|
||||
Committer: lockRequest.Committer,
|
||||
CommitSHA: lockRequest.LatestRemoteCommit,
|
||||
LockedAt: time.Now(),
|
||||
Id: fmt.Sprintf("%x", id[:]),
|
||||
Path: lockRequest.Path,
|
||||
Owner: User{Name: "Git LFS Tests"},
|
||||
LockedAt: time.Now(),
|
||||
}
|
||||
|
||||
addLocks(repo, *lock)
|
||||
|
@ -17,7 +17,7 @@ begin_test "list a single lock"
|
||||
GITLFSLOCKSENABLED=1 git lfs locks --path "f.dat" | tee locks.log
|
||||
grep "1 lock(s) matched query" locks.log
|
||||
grep "f.dat" locks.log
|
||||
grep "Git LFS Tests <git-lfs@example.com>" locks.log
|
||||
grep "Git LFS Tests" locks.log
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -35,7 +35,7 @@ begin_test "list a single lock (--json)"
|
||||
|
||||
GITLFSLOCKSENABLED=1 git lfs locks --json --path "f_json.dat" | tee locks.log
|
||||
grep "\"path\":\"f_json.dat\"" locks.log
|
||||
grep "\"committer\":{\"name\":\"Git LFS Tests\",\"email\":\"git-lfs@example.com\"}" locks.log
|
||||
grep "\"owner\":{\"name\":\"Git LFS Tests\"}" locks.log
|
||||
)
|
||||
end_test
|
||||
|
||||
|
@ -529,10 +529,6 @@ begin_test "pre-push with their lock"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
# Use a different Git persona so the locks are owned by a different person
|
||||
git config --local user.name "Example Locker"
|
||||
git config --local user.email "locker@example.com"
|
||||
|
||||
git lfs track "*.dat"
|
||||
git add .gitattributes
|
||||
git commit -m "initial commit"
|
||||
@ -563,7 +559,7 @@ begin_test "pre-push with their lock"
|
||||
git push origin master 2>&1 | tee push.log
|
||||
|
||||
grep "Unable to push 1 locked file(s)" push.log
|
||||
grep "* locked_theirs.dat - Example Locker <locker@example.com>" push.log
|
||||
grep "* locked_theirs.dat - Git LFS Tests" push.log
|
||||
popd >/dev/null
|
||||
)
|
||||
end_test
|
||||
|
Loading…
Reference in New Issue
Block a user