Skip to content

Commit

Permalink
feat: git lfs locks
Browse files Browse the repository at this point in the history
Implement git lfs locks endpoints
  • Loading branch information
aymanbagabas committed Jul 25, 2023
1 parent be83043 commit dff4fdb
Show file tree
Hide file tree
Showing 13 changed files with 681 additions and 85 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ require (

require (
github.com/caarlos0/env/v8 v8.0.0
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230725143853-5dd0632f9245
github.com/charmbracelet/keygen v0.4.3
github.com/charmbracelet/log v0.2.3-0.20230713155356-557335e40e35
github.com/charmbracelet/log v0.2.3-0.20230725142510-280c4e3f1ef2
github.com/charmbracelet/ssh v0.0.0-20230720143903-5bdd92839155
github.com/go-jose/go-jose/v3 v3.0.0
github.com/gobwas/glob v0.2.3
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e h1:PoAjCrpdHDShzxaV8Aa9wXJ71jbA0Ji4rDPlV3uoycA=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230724210439-640f4fb1e14e/go.mod h1:eXJuVicxnjRgRMokmutZdistxoMRjBjjfqvrYq7bCIU=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230725143853-5dd0632f9245 h1:PeGKqKX84IAFhFSWjTyPGiLzzEPcv94C9qKsYBk2nbQ=
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20230725143853-5dd0632f9245/go.mod h1:eXJuVicxnjRgRMokmutZdistxoMRjBjjfqvrYq7bCIU=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/keygen v0.4.3 h1:ywOZRwkDlpmkawl0BgLTxaYWDSqp6Y4nfVVmgyyO1Mg=
Expand All @@ -38,6 +38,8 @@ github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZ
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/charmbracelet/log v0.2.3-0.20230713155356-557335e40e35 h1:VXEaJ1iM2L5N8T2WVbv4y631pzCD3O9s75dONqK+87g=
github.com/charmbracelet/log v0.2.3-0.20230713155356-557335e40e35/go.mod h1:ZApwwzDbbETVTIRTk7724yQRJAXIktt98yGVMMaa3y8=
github.com/charmbracelet/log v0.2.3-0.20230725142510-280c4e3f1ef2 h1:0O3FNIElGsbl/nnUpeUVHqET7ZETJz6cUQocn/CKhoU=
github.com/charmbracelet/log v0.2.3-0.20230725142510-280c4e3f1ef2/go.mod h1:ZApwwzDbbETVTIRTk7724yQRJAXIktt98yGVMMaa3y8=
github.com/charmbracelet/ssh v0.0.0-20230720143903-5bdd92839155 h1:vJqYhlL0doAWQPz+EX/hK5x/ZYguoua773oRz77zYKo=
github.com/charmbracelet/ssh v0.0.0-20230720143903-5bdd92839155/go.mod h1:F1vgddWsb/Yr/OZilFeRZEh5sE/qU0Dt1mKkmke6Zvg=
github.com/charmbracelet/wish v1.1.1 h1:KdICASKd2oh2JPvk1Z4CJtAi97cFErXF7NKienPICO4=
Expand Down
4 changes: 4 additions & 0 deletions server/db/migrate/0002_create_lfs_tables_postgres.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ CREATE TABLE IF NOT EXISTS lfs_locks (
FOREIGN KEY(repo_id) REFERENCES repos(id)
ON DELETE CASCADE
ON UPDATE CASCADE
CONSTRAINT user_id_fk
FOREIGN KEY(user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
4 changes: 4 additions & 0 deletions server/db/migrate/0002_create_lfs_tables_sqlite.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ CREATE TABLE IF NOT EXISTS lfs_locks (
CONSTRAINT repo_id_fk
FOREIGN KEY(repo_id) REFERENCES repos(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT user_id_fk
FOREIGN KEY(user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
1 change: 1 addition & 0 deletions server/db/migrate/0003_password_tokens_sqlite.down.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
DROP TABLE IF EXISTS access_tokens;
DROP TABLE IF EXISTS users_old;

ALTER TABLE users RENAME TO users_old;

Expand Down
72 changes: 55 additions & 17 deletions server/git/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func LFSTransfer(ctx context.Context, cmd ServiceCommand) error {
}

// Batch implements transfer.Backend.
func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ ...string) ([]transfer.BatchItem, error) {
func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ map[string]string) ([]transfer.BatchItem, error) {
items := make([]transfer.BatchItem, 0)
for _, p := range pointers {
obj, err := t.store.GetLFSObjectByOid(t.ctx, t.dbx, t.repo.ID(), p.Oid)
Expand Down Expand Up @@ -130,7 +130,7 @@ func (t *lfsTransfer) Batch(_ string, pointers []transfer.Pointer, _ ...string)
}

// Download implements transfer.Backend.
func (t *lfsTransfer) Download(oid string, _ ...string) (fs.File, error) {
func (t *lfsTransfer) Download(oid string, args map[string]string) (fs.File, error) {
cfg := config.FromContext(t.ctx)
strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs"))
pointer := transfer.Pointer{Oid: oid}
Expand All @@ -144,7 +144,7 @@ type uploadObject struct {
}

// StartUpload implements transfer.Backend.
func (t *lfsTransfer) StartUpload(oid string, r io.Reader, _ ...string) (interface{}, error) {
func (t *lfsTransfer) StartUpload(oid string, r io.Reader, args map[string]string) (interface{}, error) {
if r == nil {
return nil, fmt.Errorf("no reader: %w", transfer.ErrMissingData)
}
Expand Down Expand Up @@ -180,7 +180,7 @@ func (t *lfsTransfer) StartUpload(oid string, r io.Reader, _ ...string) (interfa
}

// FinishUpload implements transfer.Backend.
func (t *lfsTransfer) FinishUpload(state interface{}, args ...string) error {
func (t *lfsTransfer) FinishUpload(state interface{}, args map[string]string) error {
upl, ok := state.(uploadObject)
if !ok {
return errors.New("invalid state")
Expand Down Expand Up @@ -251,20 +251,21 @@ func (t *lfsTransfer) Verify(oid string, args map[string]string) (transfer.Statu

type lfsLockBackend struct {
*lfsTransfer
args map[string]string
user proto.User
}

var _ transfer.LockBackend = (*lfsLockBackend)(nil)

// LockBackend implements transfer.Backend.
func (t *lfsTransfer) LockBackend() transfer.LockBackend {
func (t *lfsTransfer) LockBackend(args map[string]string) transfer.LockBackend {
user := proto.UserFromContext(t.ctx)
if user == nil {
t.logger.Errorf("no user in context while creating lock backend, repo %s", t.repo.Name())
return nil
}

return &lfsLockBackend{t, user}
return &lfsLockBackend{t, args, user}
}

// Create implements transfer.LockBackend.
Expand Down Expand Up @@ -300,21 +301,24 @@ func (l *lfsLockBackend) Create(path string, refname string) (transfer.Lock, err
// FromID implements transfer.LockBackend.
func (l *lfsLockBackend) FromID(id string) (transfer.Lock, error) {
var lock LFSLock
user := proto.UserFromContext(l.ctx)
if user == nil {
return nil, errors.New("no user in context")
iid, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return nil, err
}

if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
var err error
lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, user.ID(), id)
lock.lock, err = l.store.GetLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), iid)
if err != nil {
return db.WrapError(err)
}

lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
return db.WrapError(err)
}); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return nil, transfer.ErrNotFound
}
l.logger.Errorf("error getting lock: %v", err)
return nil, err
}
Expand All @@ -338,6 +342,9 @@ func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
lock.owner, err = l.store.GetUserByID(l.ctx, tx, lock.lock.UserID)
return db.WrapError(err)
}); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return nil, transfer.ErrNotFound
}
l.logger.Errorf("error getting lock: %v", err)
return nil, err
}
Expand All @@ -348,15 +355,32 @@ func (l *lfsLockBackend) FromPath(path string) (transfer.Lock, error) {
}

// Range implements transfer.LockBackend.
func (l *lfsLockBackend) Range(fn func(transfer.Lock) error) error {
func (l *lfsLockBackend) Range(cursor string, limit int, fn func(transfer.Lock) error) (string, error) {
var nextCursor string
var locks []*LFSLock

page, _ := strconv.Atoi(cursor)
if page <= 0 {
page = 1
}

if limit <= 0 {
limit = lfs.DefaultLocksLimit
} else if limit > 100 {
limit = 100
}

if err := l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID())
l.logger.Debug("getting locks", "limit", limit, "page", page)
mlocks, err := l.store.GetLFSLocks(l.ctx, tx, l.repo.ID(), page, limit)
if err != nil {
return db.WrapError(err)
}

if len(mlocks) == limit {
nextCursor = strconv.Itoa(page + 1)
}

users := make(map[int64]models.User, 0)
for _, mlock := range mlocks {
owner, ok := users[mlock.UserID]
Expand All @@ -374,25 +398,39 @@ func (l *lfsLockBackend) Range(fn func(transfer.Lock) error) error {

return nil
}); err != nil {
return err
return "", err
}

for _, lock := range locks {
if err := fn(lock); err != nil {
return err
return "", err
}
}

return nil
return nextCursor, nil
}

// Unlock implements transfer.LockBackend.
func (l *lfsLockBackend) Unlock(lock transfer.Lock) error {
return l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
id, err := strconv.ParseInt(lock.ID(), 10, 64)
if err != nil {
return err
}

err = l.dbx.TransactionContext(l.ctx, func(tx *db.Tx) error {
return db.WrapError(
l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.user.ID(), lock.ID()),
l.store.DeleteLFSLockForUserByID(l.ctx, tx, l.repo.ID(), l.user.ID(), id),
)
})
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return transfer.ErrNotFound
}
l.logger.Error("error unlocking lock", "err", err)
return err
}

return nil
}

// LFSLock is a Git LFS lock object.
Expand Down
29 changes: 21 additions & 8 deletions server/lfs/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ const (

// ActionVerify is the action name for a verify request.
ActionVerify = "verify"

// DefaultLocksLimit is the default number of locks to return in a single
// request.
DefaultLocksLimit = 20
)

// Pointer contains LFS pointer data
Expand Down Expand Up @@ -102,19 +106,22 @@ type AuthenticateResponse struct {
// https://github.com/git-lfs/git-lfs/blob/main/locking/schemas/http-lock-create-request-schema.json
type LockCreateRequest struct {
Path string `json:"path"`
Ref Reference `json:"ref"`
Ref Reference `json:"ref,omitempty"`
}

// Owner contains the owner data for a lock.
type Owner struct {
Name string `json:"name"`
}

// Lock contains the response data for creating a lock.
// https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md
// https://github.com/git-lfs/git-lfs/blob/main/locking/schemas/http-lock-create-response-schema.json
type Lock struct {
ID string `json:"id"`
Path string `json:"path"`
LockedAt string `json:"locked_at"`
Owner struct {
Name string `json:"name"`
} `json:"owner,omitempty"`
ID string `json:"id"`
Path string `json:"path"`
LockedAt time.Time `json:"locked_at"`
Owner Owner `json:"owner,omitempty"`
}

// LockDeleteRequest contains the request data for deleting a lock.
Expand All @@ -135,7 +142,7 @@ type LockListResponse struct {

// LockVerifyRequest contains the request data for verifying a lock.
type LockVerifyRequest struct {
Ref Reference `json:"ref"`
Ref Reference `json:"ref,omitempty"`
Cursor string `json:"cursor,omitempty"`
Limit int `json:"limit,omitempty"`
}
Expand All @@ -148,3 +155,9 @@ type LockVerifyResponse struct {
Theirs []Lock `json:"theirs"`
NextCursor string `json:"next_cursor,omitempty"`
}

// LockResponse contains the response data for a lock.
type LockResponse struct {
Lock Lock `json:"lock"`
ErrorResponse
}
4 changes: 0 additions & 4 deletions server/ssh/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,6 @@ func handleGit(s ssh.Session) {

return
case git.LFSTransferService, git.LFSAuthenticateService:
if service == git.LFSTransferService {
return
}

if accessLevel < access.ReadWriteAccess {
sshFatal(s, git.ErrNotAuthed)
return
Expand Down
Loading

0 comments on commit dff4fdb

Please sign in to comment.