mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Merge branch 'main' into Badge
This commit is contained in:
@@ -18,8 +18,32 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
return fullStepsOfEmptySteps(task)
|
||||
}
|
||||
|
||||
firstStep := task.Steps[0]
|
||||
// firstStep is the first step that has run or running, not include preStep.
|
||||
// For example,
|
||||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): firstStep is step1.
|
||||
// 2. preStep(Success) -> step1(Skipped) -> step2(Success) -> postStep(Success): firstStep is step2.
|
||||
// 3. preStep(Success) -> step1(Running) -> step2(Waiting) -> postStep(Waiting): firstStep is step1.
|
||||
// 4. preStep(Success) -> step1(Skipped) -> step2(Skipped) -> postStep(Skipped): firstStep is nil.
|
||||
// 5. preStep(Success) -> step1(Cancelled) -> step2(Cancelled) -> postStep(Cancelled): firstStep is nil.
|
||||
var firstStep *actions_model.ActionTaskStep
|
||||
// lastHasRunStep is the last step that has run.
|
||||
// For example,
|
||||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
|
||||
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
|
||||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
|
||||
// So its Stopped is the Started of postStep when there are no more steps to run.
|
||||
var lastHasRunStep *actions_model.ActionTaskStep
|
||||
|
||||
var logIndex int64
|
||||
for _, step := range task.Steps {
|
||||
if firstStep == nil && (step.Status.HasRun() || step.Status.IsRunning()) {
|
||||
firstStep = step
|
||||
}
|
||||
if step.Status.HasRun() {
|
||||
lastHasRunStep = step
|
||||
}
|
||||
logIndex += step.LogLength
|
||||
}
|
||||
|
||||
preStep := &actions_model.ActionTaskStep{
|
||||
Name: preStepName,
|
||||
@@ -28,32 +52,17 @@ func FullSteps(task *actions_model.ActionTask) []*actions_model.ActionTaskStep {
|
||||
Status: actions_model.StatusRunning,
|
||||
}
|
||||
|
||||
if firstStep.Status.HasRun() || firstStep.Status.IsRunning() {
|
||||
// No step has run or is running, so preStep is equal to the task
|
||||
if firstStep == nil {
|
||||
preStep.Stopped = task.Stopped
|
||||
preStep.Status = task.Status
|
||||
} else {
|
||||
preStep.LogLength = firstStep.LogIndex
|
||||
preStep.Stopped = firstStep.Started
|
||||
preStep.Status = actions_model.StatusSuccess
|
||||
} else if task.Status.IsDone() {
|
||||
preStep.Stopped = task.Stopped
|
||||
preStep.Status = actions_model.StatusFailure
|
||||
if task.Status.IsSkipped() {
|
||||
preStep.Status = actions_model.StatusSkipped
|
||||
}
|
||||
}
|
||||
logIndex += preStep.LogLength
|
||||
|
||||
// lastHasRunStep is the last step that has run.
|
||||
// For example,
|
||||
// 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
|
||||
// 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
|
||||
// 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
|
||||
// So its Stopped is the Started of postStep when there are no more steps to run.
|
||||
var lastHasRunStep *actions_model.ActionTaskStep
|
||||
for _, step := range task.Steps {
|
||||
if step.Status.HasRun() {
|
||||
lastHasRunStep = step
|
||||
}
|
||||
logIndex += step.LogLength
|
||||
}
|
||||
if lastHasRunStep == nil {
|
||||
lastHasRunStep = preStep
|
||||
}
|
||||
|
||||
@@ -137,6 +137,25 @@ func TestFullSteps(t *testing.T) {
|
||||
{Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "first step is skipped",
|
||||
task: &actions_model.ActionTask{
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
|
||||
},
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: 10000,
|
||||
Stopped: 10100,
|
||||
LogLength: 100,
|
||||
},
|
||||
want: []*actions_model.ActionTaskStep{
|
||||
{Name: preStepName, Status: actions_model.StatusSuccess, LogIndex: 0, LogLength: 10, Started: 10000, Stopped: 10010},
|
||||
{Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0},
|
||||
{Status: actions_model.StatusSuccess, LogIndex: 10, LogLength: 80, Started: 10010, Stopped: 10090},
|
||||
{Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
Vendored
+8
-7
@@ -63,9 +63,9 @@ func (cc *cacheContext) isDiscard() bool {
|
||||
}
|
||||
|
||||
// cacheContextLifetime is the max lifetime of cacheContext.
|
||||
// Since cacheContext is used to cache data in a request level context, 10s is enough.
|
||||
// If a cacheContext is used more than 10s, it's probably misuse.
|
||||
const cacheContextLifetime = 10 * time.Second
|
||||
// Since cacheContext is used to cache data in a request level context, 5 minutes is enough.
|
||||
// If a cacheContext is used more than 5 minutes, it's probably misuse.
|
||||
const cacheContextLifetime = 5 * time.Minute
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
@@ -109,7 +109,8 @@ func WithCacheContext(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
return context.WithValue(ctx, cacheContextKey, &cacheContext{
|
||||
// FIXME: review the use of this nolint directive
|
||||
return context.WithValue(ctx, cacheContextKey, &cacheContext{ //nolint:staticcheck
|
||||
data: make(map[any]map[any]any),
|
||||
created: timeNow(),
|
||||
})
|
||||
@@ -131,7 +132,7 @@ func GetContextData(ctx context.Context, tp, key any) any {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return nil
|
||||
}
|
||||
return c.Get(tp, key)
|
||||
@@ -144,7 +145,7 @@ func SetContextData(ctx context.Context, tp, key, value any) {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Put(tp, key, value)
|
||||
@@ -157,7 +158,7 @@ func RemoveContextData(ctx context.Context, tp, key any) {
|
||||
if c.Expired() {
|
||||
// The warning means that the cache context is misused for long-life task,
|
||||
// it can be resolved with WithNoCacheContext(ctx).
|
||||
log.Warn("cache context is expired, may be misused for long-life tasks: %v", c)
|
||||
log.Warn("cache context is expired, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Delete(tp, key)
|
||||
|
||||
Vendored
+1
-1
@@ -45,7 +45,7 @@ func TestWithCacheContext(t *testing.T) {
|
||||
timeNow = now
|
||||
}()
|
||||
timeNow = func() time.Time {
|
||||
return now().Add(10 * time.Second)
|
||||
return now().Add(5 * time.Minute)
|
||||
}
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
|
||||
@@ -114,7 +114,7 @@ type LogNameStatusCommitData struct {
|
||||
// Next returns the next LogStatusCommitData
|
||||
func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int, changed []bool, maxpathlen int) (*LogNameStatusCommitData, error) {
|
||||
var err error
|
||||
if g.next == nil || len(g.next) == 0 {
|
||||
if len(g.next) == 0 {
|
||||
g.buffull = false
|
||||
g.next, err = g.rd.ReadSlice('\x00')
|
||||
if err != nil {
|
||||
|
||||
@@ -13,11 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// NewDialContext returns a DialContext for Transport, the DialContext will do allow/block list check
|
||||
func NewDialContext(usage string, allowList, blockList *HostMatchList) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return NewDialContextWithProxy(usage, allowList, blockList, nil)
|
||||
}
|
||||
|
||||
func NewDialContextWithProxy(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
func NewDialContext(usage string, allowList, blockList *HostMatchList, proxy *url.URL) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// How Go HTTP Client works with redirection:
|
||||
// transport.RoundTrip URL=http://domain.com, Host=domain.com
|
||||
// transport.DialContext addrOrHost=domain.com:80
|
||||
|
||||
@@ -75,7 +75,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
|
||||
w.Header().Set("Etag", etag)
|
||||
}
|
||||
if lastModified != nil && !lastModified.IsZero() {
|
||||
w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat))
|
||||
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||
w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
if len(etag) > 0 {
|
||||
|
||||
@@ -79,6 +79,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||
httpcache.SetCacheControlInHeader(header, duration)
|
||||
|
||||
if !opts.LastModified.IsZero() {
|
||||
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-10
@@ -52,11 +52,6 @@ func getRequestScheme(req *http.Request) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getForwardedHost(req *http.Request) string {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
|
||||
return req.Header.Get("X-Forwarded-Host")
|
||||
}
|
||||
|
||||
// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
|
||||
func GuessCurrentAppURL(ctx context.Context) string {
|
||||
return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
|
||||
@@ -81,11 +76,9 @@ func GuessCurrentHostURL(ctx context.Context) string {
|
||||
if reqScheme == "" {
|
||||
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||
}
|
||||
reqHost := getForwardedHost(req)
|
||||
if reqHost == "" {
|
||||
reqHost = req.Host
|
||||
}
|
||||
return reqScheme + "://" + reqHost
|
||||
// X-Forwarded-Host has many problems: non-standard, not well-defined (X-Forwarded-Port or not), conflicts with Host header.
|
||||
// So do not use X-Forwarded-Host, just use Host header directly.
|
||||
return reqScheme + "://" + req.Host
|
||||
}
|
||||
|
||||
// MakeAbsoluteURL tries to make a link to an absolute URL:
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
||||
"X-Forwarded-Proto": {"https"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "https://user-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
}
|
||||
|
||||
func TestIsCurrentGiteaSiteURL(t *testing.T) {
|
||||
@@ -119,5 +119,6 @@ func TestIsCurrentGiteaSiteURL(t *testing.T) {
|
||||
},
|
||||
})
|
||||
assert.True(t, IsCurrentGiteaSiteURL(ctx, "http://localhost:3000"))
|
||||
assert.True(t, IsCurrentGiteaSiteURL(ctx, "https://forwarded-host"))
|
||||
assert.True(t, IsCurrentGiteaSiteURL(ctx, "https://user-host"))
|
||||
assert.False(t, IsCurrentGiteaSiteURL(ctx, "https://forwarded-host"))
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
|
||||
searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
|
||||
}
|
||||
|
||||
searchRequest.SortBy([]string{"-_score", "UpdatedAt"})
|
||||
|
||||
result, err := b.inner.Indexer.SearchInContext(ctx, searchRequest)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
|
||||
inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
@@ -197,8 +198,33 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes indexes by ids
|
||||
// Delete entries by repoId
|
||||
func (b *Indexer) Delete(ctx context.Context, repoID int64) error {
|
||||
if err := b.doDelete(ctx, repoID); err != nil {
|
||||
// Maybe there is a conflict during the delete operation, so we should retry after a refresh
|
||||
log.Warn("Deletion of entries of repo %v within index %v was erroneus. Trying to refresh index before trying again", repoID, b.inner.VersionedIndexName(), err)
|
||||
if err := b.refreshIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.doDelete(ctx, repoID); err != nil {
|
||||
log.Error("Could not delete entries of repo %v within index %v", repoID, b.inner.VersionedIndexName())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Indexer) refreshIndex(ctx context.Context) error {
|
||||
if _, err := b.inner.Client.Refresh(b.inner.VersionedIndexName()).Do(ctx); err != nil {
|
||||
log.Error("Error while trying to refresh index %v", b.inner.VersionedIndexName(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete entries by repoId
|
||||
func (b *Indexer) doDelete(ctx context.Context, repoID int64) error {
|
||||
_, err := b.inner.Client.DeleteByQuery(b.inner.VersionedIndexName()).
|
||||
Query(elastic.NewTermsQuery("repo_id", repoID)).
|
||||
Do(ctx)
|
||||
@@ -318,7 +344,8 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
|
||||
NumOfFragments(0). // return all highting content on fragments
|
||||
HighlighterType("fvh"),
|
||||
).
|
||||
Sort("repo_id", true).
|
||||
Sort("_score", false).
|
||||
Sort("updated_at", true).
|
||||
From(start).Size(pageSize).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
@@ -349,7 +376,8 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
|
||||
NumOfFragments(0). // return all highting content on fragments
|
||||
HighlighterType("fvh"),
|
||||
).
|
||||
Sort("repo_id", true).
|
||||
Sort("_score", false).
|
||||
Sort("updated_at", true).
|
||||
From(start).Size(pageSize).
|
||||
Do(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -401,7 +401,7 @@ func (f *valuedField) Render() string {
|
||||
}
|
||||
|
||||
func (f *valuedField) Value() string {
|
||||
return strings.TrimSpace(f.Get(fmt.Sprintf("form-field-" + f.ID)))
|
||||
return strings.TrimSpace(f.Get(fmt.Sprintf("form-field-%s", f.ID)))
|
||||
}
|
||||
|
||||
func (f *valuedField) Options() []*valuedOption {
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
// Version is the git-lfs-transfer protocol version number.
|
||||
const Version = "1"
|
||||
|
||||
// Capabilities is a list of Git LFS capabilities supported by this package.
|
||||
var Capabilities = []string{
|
||||
"version=" + Version,
|
||||
"locking",
|
||||
}
|
||||
|
||||
var _ transfer.Backend = &GiteaBackend{}
|
||||
|
||||
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
|
||||
type GiteaBackend struct {
|
||||
ctx context.Context
|
||||
server *url.URL
|
||||
op string
|
||||
token string
|
||||
itoken string
|
||||
logger transfer.Logger
|
||||
}
|
||||
|
||||
func New(ctx context.Context, repo, op, token string, logger transfer.Logger) (transfer.Backend, error) {
|
||||
// runServ guarantees repo will be in form [owner]/[name].git
|
||||
server, err := url.Parse(setting.LocalURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server = server.JoinPath("api/internal/repo", repo, "info/lfs")
|
||||
return &GiteaBackend{ctx: ctx, server: server, op: op, token: token, itoken: fmt.Sprintf("Bearer %s", setting.InternalToken), logger: logger}, nil
|
||||
}
|
||||
|
||||
// Batch implements transfer.Backend
|
||||
func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args transfer.Args) ([]transfer.BatchItem, error) {
|
||||
reqBody := lfs.BatchRequest{Operation: g.op}
|
||||
if transfer, ok := args[argTransfer]; ok {
|
||||
reqBody.Transfers = []string{transfer}
|
||||
}
|
||||
if ref, ok := args[argRefname]; ok {
|
||||
reqBody.Ref = &lfs.Reference{Name: ref}
|
||||
}
|
||||
reqBody.Objects = make([]lfs.Pointer, len(pointers))
|
||||
for i := range pointers {
|
||||
reqBody.Objects[i].Oid = pointers[i].Oid
|
||||
reqBody.Objects[i].Size = pointers[i].Size
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
url := g.server.JoinPath("objects/batch").String()
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
g.logger.Log("http request 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)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
g.logger.Log("http read error", err)
|
||||
return nil, err
|
||||
}
|
||||
var respBody lfs.BatchResponse
|
||||
err = json.Unmarshal(respBytes, &respBody)
|
||||
if err != nil {
|
||||
g.logger.Log("json umarshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// rebuild slice, we can't rely on order in resp being the same as req
|
||||
pointers = pointers[:0]
|
||||
opNum := opMap[g.op]
|
||||
for _, obj := range respBody.Objects {
|
||||
pointer := transfer.Pointer{Oid: obj.Pointer.Oid, Size: obj.Pointer.Size}
|
||||
item := transfer.BatchItem{Pointer: pointer, Args: map[string]string{}}
|
||||
switch opNum {
|
||||
case opDownload:
|
||||
if action, ok := obj.Actions[actionDownload]; ok {
|
||||
item.Present = true
|
||||
idMap := obj.Actions
|
||||
idMapBytes, err := json.Marshal(idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
||||
item.Args[argID] = idMapStr
|
||||
if authHeader, ok := action.Header[headerAuthorisation]; ok {
|
||||
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
||||
item.Args[argToken] = authHeaderB64
|
||||
}
|
||||
if action.ExpiresAt != nil {
|
||||
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
||||
}
|
||||
} else {
|
||||
// must be an error, but the SSH protocol can't propagate individual errors
|
||||
g.logger.Log("object not found", obj.Pointer.Oid, obj.Pointer.Size)
|
||||
item.Present = false
|
||||
}
|
||||
case opUpload:
|
||||
if action, ok := obj.Actions[actionUpload]; ok {
|
||||
item.Present = false
|
||||
idMap := obj.Actions
|
||||
idMapBytes, err := json.Marshal(idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json marshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
idMapStr := base64.StdEncoding.EncodeToString(idMapBytes)
|
||||
item.Args[argID] = idMapStr
|
||||
if authHeader, ok := action.Header[headerAuthorisation]; ok {
|
||||
authHeaderB64 := base64.StdEncoding.EncodeToString([]byte(authHeader))
|
||||
item.Args[argToken] = authHeaderB64
|
||||
}
|
||||
if action.ExpiresAt != nil {
|
||||
item.Args[argExpiresAt] = action.ExpiresAt.String()
|
||||
}
|
||||
} else {
|
||||
item.Present = true
|
||||
}
|
||||
}
|
||||
pointers = append(pointers, item)
|
||||
}
|
||||
return pointers, nil
|
||||
}
|
||||
|
||||
// Download implements transfer.Backend. The returned reader must be closed by the
|
||||
// caller.
|
||||
func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return nil, 0, ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionDownload]
|
||||
if !exists {
|
||||
g.logger.Log("argument id incorrect")
|
||||
return nil, 0, transfer.ErrCorruptData
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeOctetStream,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
respSize := int64(len(respBytes))
|
||||
respBuf := io.NopCloser(bytes.NewBuffer(respBytes))
|
||||
return respBuf, respSize, nil
|
||||
}
|
||||
|
||||
// StartUpload implements transfer.Backend.
|
||||
func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionUpload]
|
||||
if !exists {
|
||||
g.logger.Log("argument id incorrect")
|
||||
return transfer.ErrCorruptData
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerContentType: mimeOctetStream,
|
||||
headerContentLength: strconv.FormatInt(size, 10),
|
||||
}
|
||||
reqBytes, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify implements transfer.Backend.
|
||||
func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (transfer.Status, error) {
|
||||
reqBody := lfs.Pointer{Oid: oid, Size: size}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
||||
}
|
||||
idMapStr, exists := args[argID]
|
||||
if !exists {
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "missing argument: id"), ErrMissingID
|
||||
}
|
||||
idMapBytes, err := base64.StdEncoding.DecodeString(idMapStr)
|
||||
if err != nil {
|
||||
g.logger.Log("base64 decode error", err)
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
||||
}
|
||||
idMap := map[string]*lfs.Link{}
|
||||
err = json.Unmarshal(idMapBytes, &idMap)
|
||||
if err != nil {
|
||||
g.logger.Log("json unmarshal error", err)
|
||||
return transfer.NewStatus(transfer.StatusBadRequest, "corrupt argument: id"), transfer.ErrCorruptData
|
||||
}
|
||||
action, exists := idMap[actionVerify]
|
||||
if !exists {
|
||||
// the server sent no verify action
|
||||
return transfer.SuccessStatus(), nil
|
||||
}
|
||||
url := action.Href
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
|
||||
resp, err := req.Response()
|
||||
if err != nil {
|
||||
return transfer.NewStatus(transfer.StatusInternalServerError), err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
|
||||
}
|
||||
return transfer.SuccessStatus(), nil
|
||||
}
|
||||
|
||||
// LockBackend implements transfer.Backend.
|
||||
func (g *GiteaBackend) LockBackend(_ transfer.Args) transfer.LockBackend {
|
||||
return newGiteaLockBackend(g)
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
lfslock "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
var _ transfer.LockBackend = &giteaLockBackend{}
|
||||
|
||||
type giteaLockBackend struct {
|
||||
ctx context.Context
|
||||
g *GiteaBackend
|
||||
server *url.URL
|
||||
token string
|
||||
itoken string
|
||||
logger transfer.Logger
|
||||
}
|
||||
|
||||
func newGiteaLockBackend(g *GiteaBackend) transfer.LockBackend {
|
||||
server := g.server.JoinPath("locks")
|
||||
return &giteaLockBackend{ctx: g.ctx, g: g, server: server, token: g.token, itoken: g.itoken, logger: g.logger}
|
||||
}
|
||||
|
||||
// Create implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) 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
|
||||
}
|
||||
url := g.server.String()
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, 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 umarshal error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if respBody.Lock == nil {
|
||||
g.logger.Log("api returned nil lock")
|
||||
return nil, fmt.Errorf("api returned nil lock")
|
||||
}
|
||||
respLock := respBody.Lock
|
||||
owner := userUnknown
|
||||
if respLock.Owner != nil {
|
||||
owner = respLock.Owner.Name
|
||||
}
|
||||
lock := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
||||
return lock, nil
|
||||
}
|
||||
|
||||
// Unlock implements transfer.LockBackend
|
||||
func (g *giteaLockBackend) 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
|
||||
}
|
||||
url := g.server.JoinPath(lock.ID(), "unlock").String()
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, 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 *giteaLockBackend) 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 *giteaLockBackend) 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 *giteaLockBackend) 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 *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
|
||||
urlq := g.server.JoinPath() // get a copy
|
||||
urlq.RawQuery = v.Encode()
|
||||
url := urlq.String()
|
||||
headers := map[string]string{
|
||||
headerAuthorisation: g.itoken,
|
||||
headerAuthX: g.token,
|
||||
headerAccept: mimeGitLFS,
|
||||
headerContentType: mimeGitLFS,
|
||||
}
|
||||
req := newInternalRequest(g.ctx, url, 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 umarshal 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 := newGiteaLock(g, respLock.ID, respLock.Path, respLock.LockedAt, owner)
|
||||
respLocks = append(respLocks, lock)
|
||||
}
|
||||
return respLocks, respBody.Next, nil
|
||||
}
|
||||
|
||||
var _ transfer.Lock = &giteaLock{}
|
||||
|
||||
type giteaLock struct {
|
||||
g *giteaLockBackend
|
||||
id string
|
||||
path string
|
||||
lockedAt time.Time
|
||||
owner string
|
||||
}
|
||||
|
||||
func newGiteaLock(g *giteaLockBackend, id, path string, lockedAt time.Time, owner string) transfer.Lock {
|
||||
return &giteaLock{g: g, id: id, path: path, lockedAt: lockedAt, owner: owner}
|
||||
}
|
||||
|
||||
// Unlock implements transfer.Lock
|
||||
func (g *giteaLock) Unlock() error {
|
||||
return g.g.Unlock(g)
|
||||
}
|
||||
|
||||
// ID implements transfer.Lock
|
||||
func (g *giteaLock) ID() string {
|
||||
return g.id
|
||||
}
|
||||
|
||||
// Path implements transfer.Lock
|
||||
func (g *giteaLock) Path() string {
|
||||
return g.path
|
||||
}
|
||||
|
||||
// FormattedTimestamp implements transfer.Lock
|
||||
func (g *giteaLock) FormattedTimestamp() string {
|
||||
return g.lockedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// OwnerName implements transfer.Lock
|
||||
func (g *giteaLock) OwnerName() string {
|
||||
return g.owner
|
||||
}
|
||||
|
||||
func (g *giteaLock) CurrentUser() (string, error) {
|
||||
return userSelf, nil
|
||||
}
|
||||
|
||||
// AsLockSpec implements transfer.Lock
|
||||
func (g *giteaLock) AsLockSpec(ownerID bool) ([]string, error) {
|
||||
msgs := []string{
|
||||
fmt.Sprintf("lock %s", 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 *giteaLock) AsArguments() []string {
|
||||
return []string{
|
||||
fmt.Sprintf("id=%s", g.ID()),
|
||||
fmt.Sprintf("path=%s", g.Path()),
|
||||
fmt.Sprintf("locked-at=%s", g.FormattedTimestamp()),
|
||||
fmt.Sprintf("ownername=%s", g.OwnerName()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/proxyprotocol"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
// HTTP headers
|
||||
const (
|
||||
headerAccept = "Accept"
|
||||
headerAuthorisation = "Authorization"
|
||||
headerAuthX = "X-Auth"
|
||||
headerContentType = "Content-Type"
|
||||
headerContentLength = "Content-Length"
|
||||
)
|
||||
|
||||
// MIME types
|
||||
const (
|
||||
mimeGitLFS = "application/vnd.git-lfs+json"
|
||||
mimeOctetStream = "application/octet-stream"
|
||||
)
|
||||
|
||||
// SSH protocol action keys
|
||||
const (
|
||||
actionDownload = "download"
|
||||
actionUpload = "upload"
|
||||
actionVerify = "verify"
|
||||
)
|
||||
|
||||
// SSH protocol argument keys
|
||||
const (
|
||||
argCursor = "cursor"
|
||||
argExpiresAt = "expires-at"
|
||||
argID = "id"
|
||||
argLimit = "limit"
|
||||
argPath = "path"
|
||||
argRefname = "refname"
|
||||
argToken = "token"
|
||||
argTransfer = "transfer"
|
||||
)
|
||||
|
||||
// Default username constants
|
||||
const (
|
||||
userSelf = "(self)"
|
||||
userUnknown = "(unknown)"
|
||||
)
|
||||
|
||||
// Operations enum
|
||||
const (
|
||||
opNone = iota
|
||||
opDownload
|
||||
opUpload
|
||||
)
|
||||
|
||||
var opMap = map[string]int{
|
||||
"download": opDownload,
|
||||
"upload": opUpload,
|
||||
}
|
||||
|
||||
var ErrMissingID = fmt.Errorf("%w: missing id arg", transfer.ErrMissingData)
|
||||
|
||||
func statusCodeToErr(code int) error {
|
||||
switch code {
|
||||
case http.StatusBadRequest:
|
||||
return transfer.ErrParseError
|
||||
case http.StatusConflict:
|
||||
return transfer.ErrConflict
|
||||
case http.StatusForbidden:
|
||||
return transfer.ErrForbidden
|
||||
case http.StatusNotFound:
|
||||
return transfer.ErrNotFound
|
||||
case http.StatusUnauthorized:
|
||||
return transfer.ErrUnauthorized
|
||||
default:
|
||||
return fmt.Errorf("server returned status %v: %v", code, http.StatusText(code))
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request {
|
||||
req := httplib.NewRequest(url, method).
|
||||
SetContext(ctx).
|
||||
SetTimeout(10*time.Second, 60*time.Second).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
if setting.Protocol == setting.HTTPUnix {
|
||||
req.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if setting.LocalUseProxyProtocol {
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
} else if setting.LocalUseProxyProtocol {
|
||||
req.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, err
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header(k, v)
|
||||
}
|
||||
|
||||
req.Body(body)
|
||||
|
||||
return req
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lfstransfer
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
var _ transfer.Logger = (*GiteaLogger)(nil)
|
||||
|
||||
// noop logger for passing into transfer
|
||||
type GiteaLogger struct{}
|
||||
|
||||
func newLogger() transfer.Logger {
|
||||
return &GiteaLogger{}
|
||||
}
|
||||
|
||||
// Log implements transfer.Logger
|
||||
func (g *GiteaLogger) Log(msg string, itms ...any) {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lfstransfer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/lfstransfer/backend"
|
||||
|
||||
"github.com/charmbracelet/git-lfs-transfer/transfer"
|
||||
)
|
||||
|
||||
func Main(ctx context.Context, repo, verb, token string) error {
|
||||
logger := newLogger()
|
||||
pktline := transfer.NewPktline(os.Stdin, os.Stdout, logger)
|
||||
giteaBackend, err := backend.New(ctx, repo, verb, token, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cap := range backend.Capabilities {
|
||||
if err := pktline.WritePacketText(cap); err != nil {
|
||||
logger.Log("error sending capability due to error:", err)
|
||||
}
|
||||
}
|
||||
if err := pktline.WriteFlush(); err != nil {
|
||||
logger.Log("error flushing capabilities:", err)
|
||||
}
|
||||
p := transfer.NewProcessor(pktline, giteaBackend, logger)
|
||||
defer logger.Log("done processing commands")
|
||||
switch verb {
|
||||
case "upload":
|
||||
return p.ProcessCommands(transfer.UploadOperation)
|
||||
case "download":
|
||||
return p.ProcessCommands(transfer.DownloadOperation)
|
||||
default:
|
||||
return fmt.Errorf("unknown operation %q", verb)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func camoHandleLink(link string) string {
|
||||
if setting.Camo.Enabled {
|
||||
lnkURL, err := url.Parse(link)
|
||||
if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) &&
|
||||
(setting.Camo.Allways || lnkURL.Scheme != "https") {
|
||||
(setting.Camo.Always || lnkURL.Scheme != "https") {
|
||||
return CamoEncode(link)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestCamoHandleLink(t *testing.T) {
|
||||
"https://image.proxy/eivin43gJwGVIjR9MiYYtFIk0mw/aHR0cDovL3Rlc3RpbWFnZXMub3JnL2ltZy5qcGc",
|
||||
camoHandleLink("http://testimages.org/img.jpg"))
|
||||
|
||||
setting.Camo.Allways = true
|
||||
setting.Camo.Always = true
|
||||
assert.Equal(t,
|
||||
"https://gitea.com/img.jpg",
|
||||
camoHandleLink("https://gitea.com/img.jpg"))
|
||||
|
||||
@@ -38,4 +38,7 @@ type MigrateOptions struct {
|
||||
ReleaseAssets bool
|
||||
MigrateToRepoID int64
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
|
||||
AWSAccessKeyID string
|
||||
AWSSecretAccessKey string
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type Metadata struct {
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
License Licenses `json:"license,omitempty"`
|
||||
Authors []Author `json:"authors,omitempty"`
|
||||
Bin []string `json:"bin,omitempty"`
|
||||
Autoload map[string]any `json:"autoload,omitempty"`
|
||||
AutoloadDev map[string]any `json:"autoload-dev,omitempty"`
|
||||
Extra map[string]any `json:"extra,omitempty"`
|
||||
|
||||
@@ -120,7 +120,7 @@ func (q *baseChannel) RemoveAll(ctx context.Context) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
for q.c != nil && len(q.c) > 0 {
|
||||
for len(q.c) > 0 {
|
||||
<-q.c
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type LicenseValues struct {
|
||||
func GetLicense(name string, values *LicenseValues) ([]byte, error) {
|
||||
data, err := options.License(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
|
||||
return nil, fmt.Errorf("GetLicense[%s]: %w", name, err)
|
||||
}
|
||||
return fillLicensePlaceholder(name, values, data), nil
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ func (c logCompression) IsValid() bool {
|
||||
}
|
||||
|
||||
func (c logCompression) IsNone() bool {
|
||||
return c == "" || strings.ToLower(string(c)) == "none"
|
||||
return strings.ToLower(string(c)) == "none"
|
||||
}
|
||||
|
||||
func (c logCompression) IsZstd() bool {
|
||||
return strings.ToLower(string(c)) == "zstd"
|
||||
return c == "" || strings.ToLower(string(c)) == "zstd"
|
||||
}
|
||||
|
||||
func loadActionsFrom(rootCfg ConfigProvider) error {
|
||||
|
||||
@@ -29,4 +29,6 @@ const (
|
||||
UserFeatureManageGPGKeys = "manage_gpg_keys"
|
||||
UserFeatureManageMFA = "manage_mfa"
|
||||
UserFeatureManageCredentials = "manage_credentials"
|
||||
UserFeatureChangeUsername = "change_username"
|
||||
UserFeatureChangeFullName = "change_full_name"
|
||||
)
|
||||
|
||||
+12
-2
@@ -3,18 +3,28 @@
|
||||
|
||||
package setting
|
||||
|
||||
import "code.gitea.io/gitea/modules/log"
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
var Camo = struct {
|
||||
Enabled bool
|
||||
ServerURL string `ini:"SERVER_URL"`
|
||||
HMACKey string `ini:"HMAC_KEY"`
|
||||
Allways bool
|
||||
Always bool
|
||||
}{}
|
||||
|
||||
func loadCamoFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "camo", &Camo)
|
||||
if Camo.Enabled {
|
||||
oldValue := rootCfg.Section("camo").Key("ALLWAYS").MustString("")
|
||||
if oldValue != "" {
|
||||
log.Warn("camo.ALLWAYS is deprecated, use camo.ALWAYS instead")
|
||||
Camo.Always, _ = strconv.ParseBool(oldValue)
|
||||
}
|
||||
|
||||
if Camo.ServerURL == "" || Camo.HMACKey == "" {
|
||||
log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
// LFS represents the configuration for Git LFS
|
||||
var LFS = struct {
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
AllowPureSSH bool `ini:"LFS_ALLOW_PURE_SSH"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
|
||||
@@ -37,6 +37,7 @@ var (
|
||||
DisableQueryAuthToken bool
|
||||
CSRFCookieName = "_csrf"
|
||||
CSRFCookieHTTPOnly = true
|
||||
RecordUserSignupMetadata = false
|
||||
)
|
||||
|
||||
// loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set
|
||||
@@ -164,6 +165,8 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
// TODO: default value should be true in future releases
|
||||
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
|
||||
|
||||
RecordUserSignupMetadata = sec.Key("RECORD_USER_SIGNUP_METADATA").MustBool(false)
|
||||
|
||||
// warn if the setting is set to false explicitly
|
||||
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
|
||||
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
|
||||
|
||||
@@ -114,7 +114,7 @@ func convertAzureBlobErr(err error) error {
|
||||
if !errors.As(err, &respErr) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf(respErr.ErrorCode)
|
||||
return fmt.Errorf("%s", respErr.ErrorCode)
|
||||
}
|
||||
|
||||
// NewAzureBlobStorage returns a azure blob storage
|
||||
|
||||
+17
-9
@@ -114,6 +114,7 @@ type Repository struct {
|
||||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||
Topics []string `json:"topics"`
|
||||
Licenses []string `json:"licenses"`
|
||||
}
|
||||
|
||||
// CreateRepoOption options when creating repository
|
||||
@@ -291,15 +292,16 @@ type GitServiceType int
|
||||
|
||||
// enumerate all GitServiceType
|
||||
const (
|
||||
NotMigrated GitServiceType = iota // 0 not migrated from external sites
|
||||
PlainGitService // 1 plain git service
|
||||
GithubService // 2 github.com
|
||||
GiteaService // 3 gitea service
|
||||
GitlabService // 4 gitlab service
|
||||
GogsService // 5 gogs service
|
||||
OneDevService // 6 onedev service
|
||||
GitBucketService // 7 gitbucket service
|
||||
CodebaseService // 8 codebase service
|
||||
NotMigrated GitServiceType = iota // 0 not migrated from external sites
|
||||
PlainGitService // 1 plain git service
|
||||
GithubService // 2 github.com
|
||||
GiteaService // 3 gitea service
|
||||
GitlabService // 4 gitlab service
|
||||
GogsService // 5 gogs service
|
||||
OneDevService // 6 onedev service
|
||||
GitBucketService // 7 gitbucket service
|
||||
CodebaseService // 8 codebase service
|
||||
CodeCommitService // 9 codecommit service
|
||||
)
|
||||
|
||||
// Name represents the service type's name
|
||||
@@ -325,6 +327,8 @@ func (gt GitServiceType) Title() string {
|
||||
return "GitBucket"
|
||||
case CodebaseService:
|
||||
return "Codebase"
|
||||
case CodeCommitService:
|
||||
return "CodeCommit"
|
||||
case PlainGitService:
|
||||
return "Git"
|
||||
}
|
||||
@@ -361,6 +365,9 @@ type MigrateRepoOptions struct {
|
||||
PullRequests bool `json:"pull_requests"`
|
||||
Releases bool `json:"releases"`
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
|
||||
AWSAccessKeyID string `json:"aws_access_key_id"`
|
||||
AWSSecretAccessKey string `json:"aws_secret_access_key"`
|
||||
}
|
||||
|
||||
// TokenAuth represents whether a service type supports token-based auth
|
||||
@@ -382,6 +389,7 @@ var SupportedFullGitService = []GitServiceType{
|
||||
OneDevService,
|
||||
GitBucketService,
|
||||
CodebaseService,
|
||||
CodeCommitService,
|
||||
}
|
||||
|
||||
// RepoTransfer represents a pending repo transfer
|
||||
|
||||
@@ -34,7 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML {
|
||||
name = "avatar"
|
||||
}
|
||||
|
||||
return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
||||
return template.HTML(`<img loading="lazy" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
|
||||
}
|
||||
|
||||
// Avatar renders user avatars. args: user, size (int), class (string)
|
||||
|
||||
Reference in New Issue
Block a user