2021-05-07 16:53:17 +00:00
|
|
|
package locking
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2021-08-10 06:18:38 +00:00
|
|
|
"github.com/git-lfs/git-lfs/v2/git"
|
|
|
|
"github.com/git-lfs/git-lfs/v2/lfsapi"
|
|
|
|
"github.com/git-lfs/git-lfs/v2/ssh"
|
2021-05-07 16:53:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type sshLockClient struct {
|
|
|
|
transfer *ssh.SSHTransfer
|
|
|
|
*lfsapi.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) connection() *ssh.PktlineConnection {
|
|
|
|
return c.transfer.Connection(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) parseLockResponse(status int, args []string, lines []string) (*Lock, string, error) {
|
|
|
|
var lock *Lock
|
|
|
|
var message string
|
|
|
|
var err error
|
|
|
|
seen := make(map[string]struct{})
|
|
|
|
if status >= 200 && status <= 299 || status == 409 {
|
|
|
|
lock = &Lock{}
|
|
|
|
for _, entry := range args {
|
|
|
|
if strings.HasPrefix(entry, "id=") {
|
|
|
|
lock.Id = entry[3:]
|
|
|
|
seen["id"] = struct{}{}
|
|
|
|
} else if strings.HasPrefix(entry, "path=") {
|
|
|
|
lock.Path = entry[5:]
|
|
|
|
seen["path"] = struct{}{}
|
|
|
|
} else if strings.HasPrefix(entry, "ownername=") {
|
|
|
|
lock.Owner = &User{}
|
|
|
|
lock.Owner.Name = entry[10:]
|
|
|
|
seen["ownername"] = struct{}{}
|
|
|
|
} else if strings.HasPrefix(entry, "locked-at=") {
|
|
|
|
lock.LockedAt, err = time.Parse(time.RFC3339, entry[10:])
|
|
|
|
if err != nil {
|
|
|
|
return lock, "", fmt.Errorf("lock response: invalid locked-at: %s", entry)
|
|
|
|
}
|
|
|
|
seen["locked-at"] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(seen) != 4 {
|
|
|
|
return nil, "", fmt.Errorf("incomplete fields for lock")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if status > 299 && len(lines) > 0 {
|
|
|
|
message = lines[0]
|
|
|
|
}
|
|
|
|
return lock, message, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type owner string
|
|
|
|
|
|
|
|
const (
|
|
|
|
ownerOurs = owner("ours")
|
|
|
|
ownerTheirs = owner("theirs")
|
|
|
|
ownerUnknown = owner("")
|
|
|
|
)
|
|
|
|
|
|
|
|
type lockData struct {
|
|
|
|
lock Lock
|
|
|
|
who owner
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) lockDataIsIncomplete(data *lockData) bool {
|
|
|
|
return data.lock.Path == "" || data.lock.Owner == nil || data.lock.LockedAt.IsZero()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) parseListLockResponse(status int, args []string, lines []string) (all []Lock, ours []Lock, theirs []Lock, nextCursor string, message string, err error) {
|
|
|
|
locks := make(map[string]*lockData)
|
|
|
|
var last *lockData
|
|
|
|
if status >= 200 && status <= 299 {
|
|
|
|
for _, entry := range args {
|
|
|
|
if strings.HasPrefix(entry, "next-cursor=") {
|
|
|
|
if len(nextCursor) > 0 {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: multiple next-cursor responses")
|
|
|
|
}
|
|
|
|
nextCursor = entry[12:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, entry := range lines {
|
|
|
|
values := strings.SplitN(entry, " ", 3)
|
|
|
|
var cmd string
|
|
|
|
if len(values) > 0 {
|
|
|
|
cmd = values[0]
|
|
|
|
}
|
|
|
|
if cmd == "lock" {
|
|
|
|
if len(values) != 2 {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: invalid response: %q", entry)
|
|
|
|
} else if last != nil && c.lockDataIsIncomplete(last) {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: incomplete lock data")
|
|
|
|
}
|
|
|
|
id := values[1]
|
|
|
|
last = &lockData{who: ownerUnknown}
|
|
|
|
last.lock.Id = id
|
|
|
|
locks[id] = last
|
|
|
|
} else if len(values) != 3 {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: invalid response: %q", entry)
|
|
|
|
} else if last == nil || last.lock.Id != values[1] {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: interspersed response: %q", entry)
|
|
|
|
} else {
|
|
|
|
switch cmd {
|
|
|
|
case "path":
|
|
|
|
last.lock.Path = values[2]
|
|
|
|
case "owner":
|
|
|
|
last.who = owner(values[2])
|
|
|
|
case "ownername":
|
|
|
|
last.lock.Owner = &User{}
|
|
|
|
last.lock.Owner.Name = values[2]
|
|
|
|
case "locked-at":
|
|
|
|
last.lock.LockedAt, err = time.Parse(time.RFC3339, values[2])
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: invalid locked-at: %s", entry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if last != nil && c.lockDataIsIncomplete(last) {
|
|
|
|
return nil, nil, nil, "", "", fmt.Errorf("lock response: incomplete lock data")
|
|
|
|
}
|
|
|
|
for _, lock := range locks {
|
|
|
|
all = append(all, lock.lock)
|
|
|
|
if lock.who == ownerOurs {
|
|
|
|
ours = append(ours, lock.lock)
|
|
|
|
} else if lock.who == ownerTheirs {
|
|
|
|
theirs = append(theirs, lock.lock)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if status > 299 && len(lines) > 0 {
|
|
|
|
message = lines[0]
|
|
|
|
}
|
|
|
|
return all, ours, theirs, nextCursor, message, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) Lock(remote string, lockReq *lockRequest) (*lockResponse, int, error) {
|
|
|
|
args := make([]string, 0, 3)
|
|
|
|
args = append(args, fmt.Sprintf("path=%s", lockReq.Path))
|
|
|
|
if lockReq.Ref != nil {
|
|
|
|
args = append(args, fmt.Sprintf("refname=%s", lockReq.Ref.Name))
|
|
|
|
}
|
|
|
|
conn := c.connection()
|
|
|
|
conn.Lock()
|
|
|
|
defer conn.Unlock()
|
|
|
|
err := conn.SendMessage("lock", args)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
status, args, lines, err := conn.ReadStatusWithLines()
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
|
|
|
|
}
|
|
|
|
var lock lockResponse
|
|
|
|
lock.Lock, lock.Message, err = c.parseLockResponse(status, args, lines)
|
|
|
|
return &lock, status, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) Unlock(ref *git.Ref, remote, id string, force bool) (*unlockResponse, int, error) {
|
|
|
|
args := make([]string, 0, 3)
|
|
|
|
if ref != nil {
|
|
|
|
args = append(args, fmt.Sprintf("refname=%s", ref.Name))
|
|
|
|
}
|
|
|
|
conn := c.connection()
|
|
|
|
conn.Lock()
|
|
|
|
defer conn.Unlock()
|
|
|
|
err := conn.SendMessage(fmt.Sprintf("unlock %s", id), args)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
status, args, lines, err := conn.ReadStatusWithLines()
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
|
|
|
|
}
|
|
|
|
var lock unlockResponse
|
|
|
|
lock.Lock, lock.Message, err = c.parseLockResponse(status, args, lines)
|
|
|
|
return &lock, status, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) Search(remote string, searchReq *lockSearchRequest) (*lockList, int, error) {
|
|
|
|
values := searchReq.QueryValues()
|
|
|
|
args := make([]string, 0, len(values))
|
|
|
|
for key, value := range values {
|
|
|
|
args = append(args, fmt.Sprintf("%s=%s", key, value))
|
|
|
|
}
|
|
|
|
conn := c.connection()
|
|
|
|
conn.Lock()
|
|
|
|
defer conn.Unlock()
|
|
|
|
err := conn.SendMessage("list-lock", args)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
status, args, lines, err := conn.ReadStatusWithLines()
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
}
|
|
|
|
locks, _, _, nextCursor, message, err := c.parseListLockResponse(status, args, lines)
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
}
|
|
|
|
list := &lockList{
|
|
|
|
Locks: locks,
|
|
|
|
NextCursor: nextCursor,
|
|
|
|
Message: message,
|
|
|
|
}
|
|
|
|
return list, status, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshLockClient) SearchVerifiable(remote string, vreq *lockVerifiableRequest) (*lockVerifiableList, int, error) {
|
|
|
|
args := make([]string, 0, 3)
|
|
|
|
if vreq.Ref != nil {
|
|
|
|
args = append(args, fmt.Sprintf("refname=%s", vreq.Ref.Name))
|
|
|
|
}
|
|
|
|
if len(vreq.Cursor) > 0 {
|
|
|
|
args = append(args, fmt.Sprintf("cursor=%s", vreq.Cursor))
|
|
|
|
}
|
|
|
|
if vreq.Limit > 0 {
|
|
|
|
args = append(args, fmt.Sprintf("limit=%d", vreq.Limit))
|
|
|
|
}
|
|
|
|
conn := c.connection()
|
|
|
|
conn.Lock()
|
|
|
|
defer conn.Unlock()
|
|
|
|
err := conn.SendMessage("list-locks", args)
|
|
|
|
if err != nil {
|
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
status, args, lines, err := conn.ReadStatusWithLines()
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
}
|
|
|
|
_, ours, theirs, nextCursor, message, err := c.parseListLockResponse(status, args, lines)
|
|
|
|
if err != nil {
|
|
|
|
return nil, status, err
|
|
|
|
}
|
|
|
|
list := &lockVerifiableList{
|
|
|
|
Ours: ours,
|
|
|
|
Theirs: theirs,
|
|
|
|
NextCursor: nextCursor,
|
|
|
|
Message: message,
|
|
|
|
}
|
|
|
|
return list, status, nil
|
|
|
|
}
|