Skip to content

Commit d99f4ab

Browse files
sapklafriks
authored andcommitted
Git LFS lock api (#2938)
* Implement routes * move to api/sdk and create model * Implement add + list * List return 200 empty list no 404 * Add verify lfs lock api * Add delete and start implementing auth control * Revert to code.gitea.io/sdk/gitea vendor * Apply needed check for all lfs locks route * Add simple tests * fix lint * Improve tests * Add delete test + fix * Add lfs ascii header * Various fixes from review + remove useless code + add more corner case testing * Remove repo link since only id is needed. Save a little of memory and cpu time. * Improve tests * Use TEXT column format for path + test * fix mispell * Use NewRequestWithJSON for POST tests * Clean path * Improve DB format * Revert uniquess repoid+path * (Re)-setup uniqueness + max path length * Fixed TEXT in place of VARCHAR * Settle back to maximum VARCHAR(3072) * Let place for repoid in key * Let place for repoid in key * Let place for repoid in key * Revert back
1 parent 6ad4990 commit d99f4ab

File tree

9 files changed

+638
-16
lines changed

9 files changed

+638
-16
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"testing"
11+
"time"
12+
13+
"code.gitea.io/gitea/models"
14+
"code.gitea.io/gitea/modules/setting"
15+
api "code.gitea.io/sdk/gitea"
16+
17+
"github.com/stretchr/testify/assert"
18+
)
19+
20+
func TestAPILFSLocksNotStarted(t *testing.T) {
21+
prepareTestEnv(t)
22+
setting.LFS.StartServer = false
23+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
24+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
25+
26+
req := NewRequestf(t, "GET", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
27+
MakeRequest(t, req, http.StatusNotFound)
28+
req = NewRequestf(t, "POST", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
29+
MakeRequest(t, req, http.StatusNotFound)
30+
req = NewRequestf(t, "GET", "/%s/%s/info/lfs/locks/verify", user.Name, repo.Name)
31+
MakeRequest(t, req, http.StatusNotFound)
32+
req = NewRequestf(t, "GET", "/%s/%s/info/lfs/locks/10/unlock", user.Name, repo.Name)
33+
MakeRequest(t, req, http.StatusNotFound)
34+
}
35+
36+
func TestAPILFSLocksNotLogin(t *testing.T) {
37+
prepareTestEnv(t)
38+
setting.LFS.StartServer = true
39+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
40+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
41+
42+
req := NewRequestf(t, "GET", "/%s/%s/info/lfs/locks", user.Name, repo.Name)
43+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
44+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
45+
resp := MakeRequest(t, req, http.StatusForbidden)
46+
var lfsLockError api.LFSLockError
47+
DecodeJSON(t, resp, &lfsLockError)
48+
assert.Equal(t, "You must have pull access to list locks : User undefined doesn't have rigth to list for lfs lock [rid: 1]", lfsLockError.Message)
49+
}
50+
51+
func TestAPILFSLocksLogged(t *testing.T) {
52+
prepareTestEnv(t)
53+
setting.LFS.StartServer = true
54+
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) //in org 3
55+
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) //in org 3
56+
57+
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
58+
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // own by org 3
59+
60+
tests := []struct {
61+
user *models.User
62+
repo *models.Repository
63+
path string
64+
httpResult int
65+
addTime []int
66+
}{
67+
{user: user2, repo: repo1, path: "foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{0}},
68+
{user: user2, repo: repo1, path: "path/test", httpResult: http.StatusCreated, addTime: []int{0}},
69+
{user: user2, repo: repo1, path: "path/test", httpResult: http.StatusConflict},
70+
{user: user2, repo: repo1, path: "Foo/BaR.zip", httpResult: http.StatusConflict},
71+
{user: user2, repo: repo1, path: "Foo/Test/../subFOlder/../Relative/../BaR.zip", httpResult: http.StatusConflict},
72+
{user: user4, repo: repo1, path: "FoO/BaR.zip", httpResult: http.StatusForbidden},
73+
{user: user4, repo: repo1, path: "path/test-user4", httpResult: http.StatusForbidden},
74+
{user: user2, repo: repo1, path: "patH/Test-user4", httpResult: http.StatusCreated, addTime: []int{0}},
75+
{user: user2, repo: repo1, path: "some/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/long/path", httpResult: http.StatusCreated, addTime: []int{0}},
76+
77+
{user: user2, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusCreated, addTime: []int{1, 2}},
78+
{user: user4, repo: repo3, path: "test/foo/bar.zip", httpResult: http.StatusConflict},
79+
{user: user4, repo: repo3, path: "test/foo/bar.bin", httpResult: http.StatusCreated, addTime: []int{1, 2}},
80+
}
81+
82+
resultsTests := []struct {
83+
user *models.User
84+
repo *models.Repository
85+
totalCount int
86+
oursCount int
87+
theirsCount int
88+
locksOwners []*models.User
89+
locksTimes []time.Time
90+
}{
91+
{user: user2, repo: repo1, totalCount: 4, oursCount: 4, theirsCount: 0, locksOwners: []*models.User{user2, user2, user2, user2}, locksTimes: []time.Time{}},
92+
{user: user2, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*models.User{user2, user4}, locksTimes: []time.Time{}},
93+
{user: user4, repo: repo3, totalCount: 2, oursCount: 1, theirsCount: 1, locksOwners: []*models.User{user2, user4}, locksTimes: []time.Time{}},
94+
}
95+
96+
deleteTests := []struct {
97+
user *models.User
98+
repo *models.Repository
99+
lockID string
100+
}{}
101+
102+
//create locks
103+
for _, test := range tests {
104+
session := loginUser(t, test.user.Name)
105+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path})
106+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
107+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
108+
session.MakeRequest(t, req, test.httpResult)
109+
if len(test.addTime) > 0 {
110+
for _, id := range test.addTime {
111+
resultsTests[id].locksTimes = append(resultsTests[id].locksTimes, time.Now())
112+
}
113+
}
114+
}
115+
116+
//check creation
117+
for _, test := range resultsTests {
118+
session := loginUser(t, test.user.Name)
119+
req := NewRequestf(t, "GET", "/%s/info/lfs/locks", test.repo.FullName())
120+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
121+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
122+
resp := session.MakeRequest(t, req, http.StatusOK)
123+
var lfsLocks api.LFSLockList
124+
DecodeJSON(t, resp, &lfsLocks)
125+
assert.Len(t, lfsLocks.Locks, test.totalCount)
126+
for i, lock := range lfsLocks.Locks {
127+
assert.EqualValues(t, test.locksOwners[i].DisplayName(), lock.Owner.Name)
128+
assert.WithinDuration(t, test.locksTimes[i], lock.LockedAt, 1*time.Second)
129+
}
130+
131+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/info/lfs/locks/verify", test.repo.FullName()), map[string]string{})
132+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
133+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
134+
resp = session.MakeRequest(t, req, http.StatusOK)
135+
var lfsLocksVerify api.LFSLockListVerify
136+
DecodeJSON(t, resp, &lfsLocksVerify)
137+
assert.Len(t, lfsLocksVerify.Ours, test.oursCount)
138+
assert.Len(t, lfsLocksVerify.Theirs, test.theirsCount)
139+
for _, lock := range lfsLocksVerify.Ours {
140+
assert.EqualValues(t, test.user.DisplayName(), lock.Owner.Name)
141+
deleteTests = append(deleteTests, struct {
142+
user *models.User
143+
repo *models.Repository
144+
lockID string
145+
}{test.user, test.repo, lock.ID})
146+
}
147+
for _, lock := range lfsLocksVerify.Theirs {
148+
assert.NotEqual(t, test.user.DisplayName(), lock.Owner.Name)
149+
}
150+
}
151+
152+
//remove all locks
153+
for _, test := range deleteTests {
154+
session := loginUser(t, test.user.Name)
155+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{})
156+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
157+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
158+
resp := session.MakeRequest(t, req, http.StatusOK)
159+
var lfsLockRep api.LFSLockResponse
160+
DecodeJSON(t, resp, &lfsLockRep)
161+
assert.Equal(t, test.lockID, lfsLockRep.Lock.ID)
162+
assert.Equal(t, test.user.DisplayName(), lfsLockRep.Lock.Owner.Name)
163+
}
164+
165+
// check that we don't have any lock
166+
for _, test := range resultsTests {
167+
session := loginUser(t, test.user.Name)
168+
req := NewRequestf(t, "GET", "/%s/info/lfs/locks", test.repo.FullName())
169+
req.Header.Set("Accept", "application/vnd.git-lfs+json")
170+
req.Header.Set("Content-Type", "application/vnd.git-lfs+json")
171+
resp := session.MakeRequest(t, req, http.StatusOK)
172+
var lfsLocks api.LFSLockList
173+
DecodeJSON(t, resp, &lfsLocks)
174+
assert.Len(t, lfsLocks.Locks, 0)
175+
}
176+
}

integrations/mysql.ini.tmpl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ HTTP_PORT = 3001
2727
ROOT_URL = http://localhost:3001/
2828
DISABLE_SSH = false
2929
SSH_PORT = 22
30-
LFS_START_SERVER = false
30+
LFS_START_SERVER = true
3131
OFFLINE_MODE = false
3232

3333
[mailer]
@@ -65,4 +65,3 @@ LEVEL = Debug
6565
INSTALL_LOCK = true
6666
SECRET_KEY = 9pCviYTWSb
6767
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
68-

integrations/pgsql.ini.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ HTTP_PORT = 3002
2727
ROOT_URL = http://localhost:3002/
2828
DISABLE_SSH = false
2929
SSH_PORT = 22
30-
LFS_START_SERVER = false
30+
LFS_START_SERVER = true
3131
OFFLINE_MODE = false
3232

3333
[mailer]

integrations/sqlite.ini

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ APP_NAME = Gitea: Git with a cup of tea
22
RUN_MODE = prod
33

44
[database]
5-
DB_TYPE = sqlite3
6-
PATH = :memory:
5+
DB_TYPE = sqlite3
6+
PATH = :memory:
77

88
[indexer]
9-
ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve
9+
ISSUE_INDEXER_PATH = integrations/indexers-sqlite/issues.bleve
1010
REPO_INDEXER_ENABLED = true
11-
REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve
11+
REPO_INDEXER_PATH = integrations/indexers-sqlite/repos.bleve
1212

1313
[repository]
1414
ROOT = integrations/gitea-integration-sqlite/gitea-repositories
@@ -22,21 +22,22 @@ HTTP_PORT = 3003
2222
ROOT_URL = http://localhost:3003/
2323
DISABLE_SSH = false
2424
SSH_PORT = 22
25-
LFS_START_SERVER = false
25+
LFS_START_SERVER = true
2626
OFFLINE_MODE = false
27+
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
2728

2829
[mailer]
2930
ENABLED = false
3031

3132
[service]
32-
REGISTER_EMAIL_CONFIRM = false
33-
ENABLE_NOTIFY_MAIL = false
34-
DISABLE_REGISTRATION = false
35-
ENABLE_CAPTCHA = false
36-
REQUIRE_SIGNIN_VIEW = false
37-
DEFAULT_KEEP_EMAIL_PRIVATE = false
33+
REGISTER_EMAIL_CONFIRM = false
34+
ENABLE_NOTIFY_MAIL = false
35+
DISABLE_REGISTRATION = false
36+
ENABLE_CAPTCHA = false
37+
REQUIRE_SIGNIN_VIEW = false
38+
DEFAULT_KEEP_EMAIL_PRIVATE = false
3839
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
39-
NO_REPLY_ADDRESS = noreply.example.org
40+
NO_REPLY_ADDRESS = noreply.example.org
4041

4142
[picture]
4243
DISABLE_GRAVATAR = false
@@ -46,7 +47,7 @@ ENABLE_FEDERATED_AVATAR = false
4647
PROVIDER = file
4748

4849
[log]
49-
MODE = console,file
50+
MODE = console,file
5051
ROOT_PATH = sqlite-log
5152

5253
[log.console]

models/error.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,63 @@ func (err ErrLastOrgOwner) Error() string {
506506
return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
507507
}
508508

509+
//.____ ____________________
510+
//| | \_ _____/ _____/
511+
//| | | __) \_____ \
512+
//| |___| \ / \
513+
//|_______ \___ / /_______ /
514+
// \/ \/ \/
515+
516+
// ErrLFSLockNotExist represents a "LFSLockNotExist" kind of error.
517+
type ErrLFSLockNotExist struct {
518+
ID int64
519+
RepoID int64
520+
Path string
521+
}
522+
523+
// IsErrLFSLockNotExist checks if an error is a ErrLFSLockNotExist.
524+
func IsErrLFSLockNotExist(err error) bool {
525+
_, ok := err.(ErrLFSLockNotExist)
526+
return ok
527+
}
528+
529+
func (err ErrLFSLockNotExist) Error() string {
530+
return fmt.Sprintf("lfs lock does not exist [id: %d, rid: %d, path: %s]", err.ID, err.RepoID, err.Path)
531+
}
532+
533+
// ErrLFSLockUnauthorizedAction represents a "LFSLockUnauthorizedAction" kind of error.
534+
type ErrLFSLockUnauthorizedAction struct {
535+
RepoID int64
536+
UserName string
537+
Action string
538+
}
539+
540+
// IsErrLFSLockUnauthorizedAction checks if an error is a ErrLFSLockUnauthorizedAction.
541+
func IsErrLFSLockUnauthorizedAction(err error) bool {
542+
_, ok := err.(ErrLFSLockUnauthorizedAction)
543+
return ok
544+
}
545+
546+
func (err ErrLFSLockUnauthorizedAction) Error() string {
547+
return fmt.Sprintf("User %s doesn't have rigth to %s for lfs lock [rid: %d]", err.UserName, err.Action, err.RepoID)
548+
}
549+
550+
// ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
551+
type ErrLFSLockAlreadyExist struct {
552+
RepoID int64
553+
Path string
554+
}
555+
556+
// IsErrLFSLockAlreadyExist checks if an error is a ErrLFSLockAlreadyExist.
557+
func IsErrLFSLockAlreadyExist(err error) bool {
558+
_, ok := err.(ErrLFSLockAlreadyExist)
559+
return ok
560+
}
561+
562+
func (err ErrLFSLockAlreadyExist) Error() string {
563+
return fmt.Sprintf("lfs lock already exists [rid: %d, path: %s]", err.RepoID, err.Path)
564+
}
565+
509566
// __________ .__ __
510567
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
511568
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |

0 commit comments

Comments
 (0)