mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
295 lines
7.3 KiB
Go
295 lines
7.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package backend
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
lfslock "github.com/hanzoai/git/modules/structs"
|
|
|
|
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
|
)
|
|
|
|
var _ transfer.LockBackend = &gitLockBackend{}
|
|
|
|
type gitLockBackend struct {
|
|
ctx context.Context
|
|
g *GitBackend
|
|
server *url.URL
|
|
authToken string
|
|
internalAuth string
|
|
logger transfer.Logger
|
|
}
|
|
|
|
func newGitLockBackend(g *GitBackend) transfer.LockBackend {
|
|
server := g.server.JoinPath("locks")
|
|
return &gitLockBackend{ctx: g.ctx, g: g, server: server, authToken: g.authToken, internalAuth: g.internalAuth, logger: g.logger}
|
|
}
|
|
|
|
// Create implements transfer.LockBackend
|
|
func (g *gitLockBackend) Create(path, refname string) (transfer.Lock, error) {
|
|
reqBody := lfslock.LFSLockRequest{Path: path}
|
|
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
g.logger.Log("json marshal error", err)
|
|
return nil, err
|
|
}
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerInternalAuth: g.internalAuth,
|
|
headerAccept: mimeGitLFS,
|
|
headerContentType: mimeGitLFS,
|
|
}
|
|
req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
g.logger.Log("http request error", err)
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
g.logger.Log("http read error", err)
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusCreated {
|
|
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
|
return nil, statusCodeToErr(resp.StatusCode)
|
|
}
|
|
var respBody lfslock.LFSLockResponse
|
|
err = json.Unmarshal(respBytes, &respBody)
|
|
if err != nil {
|
|
g.logger.Log("json unmarshal error", err)
|
|
return nil, err
|
|
}
|
|
|
|
if respBody.Lock == nil {
|
|
g.logger.Log("api returned nil lock")
|
|
return nil, errors.New("api returned nil lock")
|
|
}
|
|
respLock := respBody.Lock
|
|
owner := userUnknown
|
|
if respLock.Owner != nil {
|
|
owner = respLock.Owner.Name
|
|
}
|
|
lock := newGitLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
|
return lock, nil
|
|
}
|
|
|
|
// Unlock implements transfer.LockBackend
|
|
func (g *gitLockBackend) Unlock(lock transfer.Lock) error {
|
|
reqBody := lfslock.LFSLockDeleteRequest{}
|
|
|
|
bodyBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
g.logger.Log("json marshal error", err)
|
|
return err
|
|
}
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerInternalAuth: g.internalAuth,
|
|
headerAccept: mimeGitLFS,
|
|
headerContentType: mimeGitLFS,
|
|
}
|
|
req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
g.logger.Log("http request error", err)
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
|
return statusCodeToErr(resp.StatusCode)
|
|
}
|
|
// no need to read response
|
|
|
|
return nil
|
|
}
|
|
|
|
// FromPath implements transfer.LockBackend
|
|
func (g *gitLockBackend) FromPath(path string) (transfer.Lock, error) {
|
|
v := url.Values{
|
|
argPath: []string{path},
|
|
}
|
|
|
|
respLocks, _, err := g.queryLocks(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(respLocks) == 0 {
|
|
return nil, transfer.ErrNotFound
|
|
}
|
|
return respLocks[0], nil
|
|
}
|
|
|
|
// FromID implements transfer.LockBackend
|
|
func (g *gitLockBackend) FromID(id string) (transfer.Lock, error) {
|
|
v := url.Values{
|
|
argID: []string{id},
|
|
}
|
|
|
|
respLocks, _, err := g.queryLocks(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(respLocks) == 0 {
|
|
return nil, transfer.ErrNotFound
|
|
}
|
|
return respLocks[0], nil
|
|
}
|
|
|
|
// Range implements transfer.LockBackend
|
|
func (g *gitLockBackend) Range(cursor string, limit int, iter func(transfer.Lock) error) (string, error) {
|
|
v := url.Values{
|
|
argLimit: []string{strconv.FormatInt(int64(limit), 10)},
|
|
}
|
|
if cursor != "" {
|
|
v[argCursor] = []string{cursor}
|
|
}
|
|
|
|
respLocks, cursor, err := g.queryLocks(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, lock := range respLocks {
|
|
err := iter(lock)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
return cursor, nil
|
|
}
|
|
|
|
func (g *gitLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
|
|
serverURLWithQuery := g.server.JoinPath() // get a copy
|
|
serverURLWithQuery.RawQuery = v.Encode()
|
|
headers := map[string]string{
|
|
headerAuthorization: g.authToken,
|
|
headerInternalAuth: g.internalAuth,
|
|
headerAccept: mimeGitLFS,
|
|
headerContentType: mimeGitLFS,
|
|
}
|
|
req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil)
|
|
resp, err := req.Response()
|
|
if err != nil {
|
|
g.logger.Log("http request error", err)
|
|
return nil, "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
respBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
g.logger.Log("http read error", err)
|
|
return nil, "", err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
|
|
return nil, "", statusCodeToErr(resp.StatusCode)
|
|
}
|
|
var respBody lfslock.LFSLockList
|
|
err = json.Unmarshal(respBytes, &respBody)
|
|
if err != nil {
|
|
g.logger.Log("json unmarshal error", err)
|
|
return nil, "", err
|
|
}
|
|
|
|
respLocks := make([]transfer.Lock, 0, len(respBody.Locks))
|
|
for _, respLock := range respBody.Locks {
|
|
owner := userUnknown
|
|
if respLock.Owner != nil {
|
|
owner = respLock.Owner.Name
|
|
}
|
|
lock := newGitLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
|
respLocks = append(respLocks, lock)
|
|
}
|
|
return respLocks, respBody.Next, nil
|
|
}
|
|
|
|
var _ transfer.Lock = &gitLock{}
|
|
|
|
type gitLock struct {
|
|
g *gitLockBackend
|
|
id string
|
|
path string
|
|
lockedAt time.Time
|
|
owner string
|
|
}
|
|
|
|
func newGitLock(g *gitLockBackend, id, path string, lockedAt time.Time, owner string) transfer.Lock {
|
|
return &gitLock{g: g, id: id, path: path, lockedAt: lockedAt, owner: owner}
|
|
}
|
|
|
|
// Unlock implements transfer.Lock
|
|
func (g *gitLock) Unlock() error {
|
|
return g.g.Unlock(g)
|
|
}
|
|
|
|
// ID implements transfer.Lock
|
|
func (g *gitLock) ID() string {
|
|
return g.id
|
|
}
|
|
|
|
// Path implements transfer.Lock
|
|
func (g *gitLock) Path() string {
|
|
return g.path
|
|
}
|
|
|
|
// FormattedTimestamp implements transfer.Lock
|
|
func (g *gitLock) FormattedTimestamp() string {
|
|
return g.lockedAt.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
// OwnerName implements transfer.Lock
|
|
func (g *gitLock) OwnerName() string {
|
|
return g.owner
|
|
}
|
|
|
|
func (g *gitLock) CurrentUser() (string, error) {
|
|
return userSelf, nil
|
|
}
|
|
|
|
// AsLockSpec implements transfer.Lock
|
|
func (g *gitLock) AsLockSpec(ownerID bool) ([]string, error) {
|
|
msgs := []string{
|
|
"lock " + g.ID(),
|
|
fmt.Sprintf("path %s %s", g.ID(), g.Path()),
|
|
fmt.Sprintf("locked-at %s %s", g.ID(), g.FormattedTimestamp()),
|
|
fmt.Sprintf("ownername %s %s", g.ID(), g.OwnerName()),
|
|
}
|
|
if ownerID {
|
|
user, err := g.CurrentUser()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting current user: %w", err)
|
|
}
|
|
who := "theirs"
|
|
if user == g.OwnerName() {
|
|
who = "ours"
|
|
}
|
|
msgs = append(msgs, fmt.Sprintf("owner %s %s", g.ID(), who))
|
|
}
|
|
return msgs, nil
|
|
}
|
|
|
|
// AsArguments implements transfer.Lock
|
|
func (g *gitLock) AsArguments() []string {
|
|
return []string{
|
|
"id=" + g.ID(),
|
|
"path=" + g.Path(),
|
|
"locked-at=" + g.FormattedTimestamp(),
|
|
"ownername=" + g.OwnerName(),
|
|
}
|
|
}
|