As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Unverified Commit 019bed12 authored by Ash McKenzie's avatar Ash McKenzie Committed by GitLab
Browse files

Merge branch 'git-lfs-transfer-lock-unlock' into 'main'

git-lfs-transfer: Add support for lock and unlocking of files

See merge request https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/1071



Merged-by: default avatarAsh McKenzie <amckenzie@gitlab.com>
Approved-by: default avatarAsh McKenzie <amckenzie@gitlab.com>
Reviewed-by: default avatarAsh McKenzie <amckenzie@gitlab.com>
Co-authored-by: default avatarJoseph Snyder <joe.snyder@kitware.com>
Co-authored-by: default avatarKyle Edwards <kyle.edwards@kitware.com>
parents 0152fb7d 2540bbf7
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -263,11 +263,26 @@ type gitlabLock struct {
}
func (l *gitlabLock) Unlock() error {
return newErrUnsupported("unlock")
lock, err := l.gitlabLockBackend.client.Unlock(l.id, l.gitlabLockBackend.args["force"] == "true", l.gitlabLockBackend.args["refname"])
if err != nil {
return err
}
l.id = lock.ID
l.path = lock.Path
l.timestamp = lock.LockedAt
if lock.Owner != nil {
l.owner = lock.Owner.Name
}
return nil
}
func (l *gitlabLock) AsArguments() []string {
return nil
return []string{
fmt.Sprintf("id=%s", l.id),
fmt.Sprintf("path=%s", l.path),
fmt.Sprintf("locked-at=%s", l.timestamp.Format(time.RFC3339)),
fmt.Sprintf("ownername=%s", l.owner),
}
}
func (l *gitlabLock) AsLockSpec(useOwnerID bool) ([]string, error) {
Loading
Loading
@@ -305,8 +320,19 @@ type gitlabLockBackend struct {
args map[string]string
}
func (b *gitlabLockBackend) Create(_ string, _ string) (transfer.Lock, error) {
return nil, newErrUnsupported("lock")
func (b *gitlabLockBackend) Create(path string, refname string) (transfer.Lock, error) {
l, err := b.client.Lock(path, refname)
var lock *gitlabLock
if l != nil {
lock = &gitlabLock{
gitlabLockBackend: b,
id: l.ID,
path: l.Path,
timestamp: l.LockedAt,
owner: l.Owner.Name,
}
}
return lock, err
}
func (b *gitlabLockBackend) Unlock(_ transfer.Lock) error {
Loading
Loading
Loading
Loading
@@ -19,6 +19,7 @@ import (
var (
capabilities = []string{
"version=1",
"locking",
}
)
Loading
Loading
Loading
Loading
@@ -109,6 +109,7 @@ func readCapabilities(t *testing.T, pl *pktline.Pktline) {
}
require.Equal(t, []string{
"version=1",
"locking",
}, caps)
}
Loading
Loading
@@ -756,14 +757,55 @@ func TestLfsTransferLock(t *testing.T) {
wg := setupWaitGroupForExecute(t, cmd)
negotiateVersion(t, pl)
writeCommandArgs(t, pl, "lock", []string{"path=large/file"})
writeCommandArgs(t, pl, "lock", []string{"path=/large/file/1"})
status, args, data := readStatusArgsAndTextData(t, pl)
require.Equal(t, "status 405", status)
require.Equal(t, "status 409", status)
require.Equal(t, []string{
"id=lock1",
"path=/large/file/1",
"locked-at=2023-10-03T13:56:20Z",
"ownername=johndoe",
}, args)
require.Equal(t, []string{
"conflict",
}, data)
writeCommandArgs(t, pl, "lock", []string{"path=/large/file/2"})
status, args, data = readStatusArgsAndTextData(t, pl)
require.Equal(t, "status 403", status)
require.Empty(t, args)
require.Equal(t, []string{
"error: forbidden",
}, data)
writeCommandArgs(t, pl, "lock", []string{"path=/large/file/3"})
status, args, data = readStatusArgsAndTextData(t, pl)
require.Equal(t, "status 500", status)
require.Empty(t, args)
require.Equal(t, []string{
"error: lock is not yet supported by git-lfs-transfer. See https://gitlab.com/groups/gitlab-org/-/epics/11872 to track progress.",
"internal error",
}, data)
writeCommandArgs(t, pl, "lock", []string{"path=/large/file/4"})
status, args = readStatusArgs(t, pl)
require.Equal(t, "status 201", status)
require.Equal(t, []string{
"id=lock4",
"path=/large/file/4",
"locked-at=2023-10-03T13:56:20Z",
"ownername=johndoe",
}, args)
writeCommandArgs(t, pl, "lock", []string{"path=/large/file/5", "refname=refs/heads/main"})
status, args = readStatusArgs(t, pl)
require.Equal(t, "status 201", status)
require.Equal(t, []string{
"id=lock5",
"path=/large/file/5",
"locked-at=2023-10-03T13:56:20Z",
"ownername=johndoe",
}, args)
quit(t, pl)
wg.Wait()
}
Loading
Loading
@@ -773,12 +815,40 @@ func TestLfsTransferUnlock(t *testing.T) {
wg := setupWaitGroupForExecute(t, cmd)
negotiateVersion(t, pl)
writeCommand(t, pl, "unlock lock1")
writeCommandArgs(t, pl, "unlock lock1", []string{"refname=refs/heads/main"})
status, args := readStatusArgs(t, pl)
require.Equal(t, "status 200", status)
require.Equal(t, []string{
"id=lock1",
"path=/large/file/1",
"locked-at=2023-10-03T13:56:20Z",
"ownername=johndoe",
}, args)
writeCommandArgs(t, pl, "unlock lock2", []string{"force=true"})
status, args = readStatusArgs(t, pl)
require.Equal(t, "status 200", status)
require.Equal(t, []string{
"id=lock2",
"path=/large/file/2",
"locked-at=1955-11-12T22:04:00Z",
"ownername=marty",
}, args)
writeCommand(t, pl, "unlock lock3")
status, args, data := readStatusArgsAndTextData(t, pl)
require.Equal(t, "status 405", status)
require.Equal(t, "status 403", status)
require.Empty(t, args)
require.Equal(t, []string{
"error: forbidden",
}, data)
writeCommand(t, pl, "unlock lock4")
status, args, data = readStatusArgsAndTextData(t, pl)
require.Equal(t, "status 404", status)
require.Empty(t, args)
require.Equal(t, []string{
"error: unlock is not yet supported by git-lfs-transfer. See https://gitlab.com/groups/gitlab-org/-/epics/11872 to track progress.",
"error: not found",
}, data)
quit(t, pl)
Loading
Loading
@@ -1243,9 +1313,154 @@ func setup(t *testing.T, keyID string, repo string, op string) (string, *Command
limit = l
}
bodyJSON.Locks, bodyJSON.NextCursor = listLocks(r.URL.Query().Get("cursor"), limit, r.URL.Query().Get("refspec"), r.URL.Query().Get("id"), r.URL.Query().Get("path"))
require.NoError(t, json.NewEncoder(w).Encode(bodyJSON))
case http.MethodPost:
var body map[string]interface{}
reader := json.NewDecoder(r.Body)
reader.Decode(&body)
var response map[string]interface{}
switch body["path"] {
case "/large/file/1":
response = map[string]interface{}{
"lock": map[string]interface{}{
"id": "lock1",
"path": "/large/file/1",
"locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
"owner": map[string]interface{}{
"name": "johndoe",
},
},
"message": "already created lock",
}
w.WriteHeader(http.StatusConflict)
case "/large/file/2":
response = map[string]interface{}{
"message": "no permission",
}
w.WriteHeader(http.StatusForbidden)
case "/large/file/4":
response = map[string]interface{}{
"lock": map[string]interface{}{
"id": "lock4",
"path": "/large/file/4",
"locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
"owner": map[string]interface{}{
"name": "johndoe",
},
},
}
w.WriteHeader(http.StatusCreated)
case "/large/file/5":
ref := body["ref"].(map[string]interface{})
require.Equal(t, "refs/heads/main", ref["name"])
response = map[string]interface{}{
"lock": map[string]interface{}{
"id": "lock5",
"path": "/large/file/5",
"locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
"owner": map[string]interface{}{
"name": "johndoe",
},
},
}
w.WriteHeader(http.StatusCreated)
default:
response = map[string]interface{}{
"message": "internal error",
}
w.WriteHeader(http.StatusInternalServerError)
}
writer := json.NewEncoder(w)
writer.Encode(response)
}
},
},
{
Path: "/group/repo/info/lfs/locks/lock1/unlock",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
var body map[string]interface{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.Equal(t, map[string]interface{}{
"ref": map[string]interface{}{
"name": "refs/heads/main",
},
"force": false,
}, body)
lock := map[string]interface{}{
"lock": map[string]interface{}{
"id": "lock1",
"path": "/large/file/1",
"locked_at": time.Date(2023, 10, 3, 13, 56, 20, 0, time.UTC).Format(time.RFC3339),
"owner": map[string]interface{}{
"name": "johndoe",
},
},
}
writer := json.NewEncoder(w)
writer.Encode(lock)
},
},
{
Path: "/group/repo/info/lfs/locks/lock2/unlock",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
var body map[string]interface{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.Equal(t, map[string]interface{}{
"force": true,
}, body)
lock := map[string]interface{}{
"lock": map[string]interface{}{
"id": "lock2",
"path": "/large/file/2",
"locked_at": time.Date(1955, 11, 12, 22, 4, 0, 0, time.UTC).Format(time.RFC3339),
"owner": map[string]interface{}{
"name": "marty",
},
},
}
writer := json.NewEncoder(w)
writer.Encode(lock)
},
},
{
Path: "/group/repo/info/lfs/locks/lock3/unlock",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
var body map[string]interface{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.Equal(t, map[string]interface{}{
"force": false,
}, body)
lock := map[string]interface{}{
"message": "forbidden",
}
w.WriteHeader(http.StatusForbidden)
writer := json.NewEncoder(w)
writer.Encode(lock)
},
},
{
Path: "/group/repo/info/lfs/locks/lock4/unlock",
Handler: func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
var body map[string]interface{}
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.Equal(t, map[string]interface{}{
"force": false,
}, body)
lock := map[string]interface{}{
"message": "not found",
}
w.WriteHeader(http.StatusNotFound)
writer := json.NewEncoder(w)
writer.Encode(lock)
},
},
}
Loading
Loading
Loading
Loading
@@ -101,6 +101,24 @@ func (f *downloadedFile) Stat() (fs.FileInfo, error) {
return &f.downloadedFileInfo, nil
}
type lockRequest struct {
Path string `json:"path"`
Ref *batchRef `json:"ref,omitempty"`
}
type lockResponse struct {
Lock *Lock `json:"lock"`
}
type unlockRequest struct {
Force bool `json:"force"`
Ref *batchRef `json:"ref,omitempty"`
}
type unlockResponse struct {
Lock *Lock `json:"lock"`
}
type listLocksVerifyRequest struct {
Cursor string `json:"cursor,omitempty"`
Limit int `json:"limit"`
Loading
Loading
@@ -243,6 +261,113 @@ func (c *Client) PutObject(oid, href string, headers map[string]string, r io.Rea
return nil
}
func (c *Client) Lock(path, refname string) (*Lock, error) {
var ref *batchRef
if refname != "" {
ref = &batchRef{
Name: refname,
}
}
body := &lockRequest{
Path: path,
Ref: ref,
}
jsonData, err := json.Marshal(body)
if err != nil {
return nil, err
}
jsonReader := bytes.NewReader(jsonData)
req, err := newHTTPRequest(http.MethodPost, c.href+"/locks", jsonReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
req.Header.Set("Authorization", c.auth)
client := newHTTPClient()
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() { _ = res.Body.Close() }()
switch {
case res.StatusCode >= 200 && res.StatusCode <= 299:
response := &lockResponse{}
if err := gitlabnet.ParseJSON(res, response); err != nil {
return nil, err
}
return response.Lock, nil
case res.StatusCode == http.StatusForbidden:
return nil, transfer.ErrForbidden
case res.StatusCode == http.StatusNotFound:
return nil, transfer.ErrNotFound
case res.StatusCode == http.StatusConflict:
response := &lockResponse{}
if err := gitlabnet.ParseJSON(res, response); err != nil {
return nil, err
}
return response.Lock, transfer.ErrConflict
default:
return nil, fmt.Errorf("internal error")
}
}
func (c *Client) Unlock(id string, force bool, refname string) (*Lock, error) {
var ref *batchRef
if refname != "" {
ref = &batchRef{
Name: refname,
}
}
body := &unlockRequest{
Force: force,
Ref: ref,
}
jsonData, err := json.Marshal(body)
if err != nil {
return nil, err
}
jsonReader := bytes.NewReader(jsonData)
req, err := newHTTPRequest(http.MethodPost, fmt.Sprintf("%s/locks/%s/unlock", c.href, id), jsonReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
req.Header.Set("Authorization", c.auth)
client := newHTTPClient()
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() { _ = res.Body.Close() }()
switch {
case res.StatusCode >= 200 && res.StatusCode <= 299:
response := &unlockResponse{}
if err := gitlabnet.ParseJSON(res, response); err != nil {
return nil, err
}
return response.Lock, nil
case res.StatusCode == http.StatusForbidden:
return nil, transfer.ErrForbidden
case res.StatusCode == http.StatusNotFound:
return nil, transfer.ErrNotFound
default:
return nil, fmt.Errorf("internal error")
}
}
func (c *Client) ListLocksVerify(path, id, cursor string, limit int, ref string) (*ListLocksVerifyResponse, error) {
url, err := url.Parse(c.href)
if err != nil {
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment