Merge pull request #1909 from git-lfs/locking/docs

Locking/docs
This commit is contained in:
risk danger olson 2017-02-09 09:17:22 -07:00 committed by GitHub
commit 61a359f3de
13 changed files with 591 additions and 150 deletions

@ -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

@ -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