mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-31 01:13:39 +02:00
Merge branch 'main' into admin-ip-info
This commit is contained in:
Generated
+5
@@ -294,6 +294,11 @@
|
||||
"path": "github.com/bmatcuk/doublestar/v4/LICENSE",
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Bob Matcuk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/bohde/codel",
|
||||
"path": "github.com/bohde/codel/LICENSE",
|
||||
"licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Rowan Bohde\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/boombuler/barcode",
|
||||
"path": "github.com/boombuler/barcode/LICENSE",
|
||||
|
||||
@@ -940,7 +940,29 @@ LEVEL = Info
|
||||
;;
|
||||
;; Disable the code explore page.
|
||||
;DISABLE_CODE_PAGE = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[qos]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; Enable request quality of service and overload protection.
|
||||
; ENABLED = false
|
||||
;;
|
||||
;; The maximum number of concurrent requests that the server will
|
||||
;; process before enqueueing new requests. Default is "CpuNum * 4".
|
||||
; MAX_INFLIGHT =
|
||||
;;
|
||||
;; The maximum number of requests that can be enqueued before new
|
||||
;; requests will be dropped.
|
||||
; MAX_WAITING = 100
|
||||
;;
|
||||
;; Target maximum wait time a request may be enqueued for. Requests
|
||||
;; that are enqueued for less than this amount of time will not be
|
||||
;; dropped. When wait times exceed this amount, a portion of requests
|
||||
;; will be dropped until wait times have decreased below this amount.
|
||||
; TARGET_WAIT_TIME = 250ms
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -1423,7 +1445,6 @@ LEVEL = Info
|
||||
;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets
|
||||
;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior.
|
||||
;MATH_CODE_BLOCK_DETECTION =
|
||||
;;
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -32,6 +32,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/codecommit v1.28.1
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.4.2
|
||||
github.com/bohde/codel v0.2.0
|
||||
github.com/buildkite/terminal-to-html/v3 v3.16.8
|
||||
github.com/caddyserver/certmagic v0.22.0
|
||||
github.com/charmbracelet/git-lfs-transfer v0.2.0
|
||||
|
||||
@@ -179,6 +179,9 @@ github.com/blevesearch/zapx/v16 v16.1.5 h1:b0sMcarqNFxuXvjoXsF8WtwVahnxyhEvBSRJi
|
||||
github.com/blevesearch/zapx/v16 v16.1.5/go.mod h1:J4mSF39w1QELc11EWRSBFkPeZuO7r/NPKkHzDCoiaI8=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E=
|
||||
github.com/bohde/codel v0.2.0/go.mod h1:Idb1IRvTdwkRjIjguLIo+FXhIBhcpGl94o7xra6ggWk=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -881,6 +884,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
@@ -1025,6 +1029,8 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
|
||||
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
|
||||
pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0=
|
||||
pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
|
||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/alpine"
|
||||
"code.gitea.io/gitea/modules/packages/arch"
|
||||
@@ -102,22 +103,26 @@ func (pd *PackageDescriptor) CalculateBlobSize() int64 {
|
||||
|
||||
// GetPackageDescriptor gets the package description for a version
|
||||
func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) {
|
||||
p, err := GetPackageByID(ctx, pv.PackageID)
|
||||
return getPackageDescriptor(ctx, pv, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) {
|
||||
p, err := cache.GetWithEphemeralCache(ctx, c, "package", pv.PackageID, GetPackageByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o, err := user_model.GetUserByID(ctx, p.OwnerID)
|
||||
o, err := cache.GetWithEphemeralCache(ctx, c, "user", p.OwnerID, user_model.GetUserByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var repository *repo_model.Repository
|
||||
if p.RepoID > 0 {
|
||||
repository, err = repo_model.GetRepositoryByID(ctx, p.RepoID)
|
||||
repository, err = cache.GetWithEphemeralCache(ctx, c, "repo", p.RepoID, repo_model.GetRepositoryByID)
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
creator, err := user_model.GetUserByID(ctx, pv.CreatorID)
|
||||
creator, err := cache.GetWithEphemeralCache(ctx, c, "user", pv.CreatorID, user_model.GetUserByID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
creator = user_model.NewGhostUser()
|
||||
@@ -145,9 +150,13 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfds, err := GetPackageFileDescriptors(ctx, pfs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
pfds := make([]*PackageFileDescriptor, 0, len(pfs))
|
||||
for _, pf := range pfs {
|
||||
pfd, err := getPackageFileDescriptor(ctx, pf, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pfds = append(pfds, pfd)
|
||||
}
|
||||
|
||||
var metadata any
|
||||
@@ -221,7 +230,11 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
|
||||
// GetPackageFileDescriptor gets a package file descriptor for a package file
|
||||
func GetPackageFileDescriptor(ctx context.Context, pf *PackageFile) (*PackageFileDescriptor, error) {
|
||||
pb, err := GetBlobByID(ctx, pf.BlobID)
|
||||
return getPackageFileDescriptor(ctx, pf, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageFileDescriptor(ctx context.Context, pf *PackageFile, c *cache.EphemeralCache) (*PackageFileDescriptor, error) {
|
||||
pb, err := cache.GetWithEphemeralCache(ctx, c, "package_file_blob", pf.BlobID, GetBlobByID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -251,9 +264,13 @@ func GetPackageFileDescriptors(ctx context.Context, pfs []*PackageFile) ([]*Pack
|
||||
|
||||
// GetPackageDescriptors gets the package descriptions for the versions
|
||||
func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*PackageDescriptor, error) {
|
||||
return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache())
|
||||
}
|
||||
|
||||
func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) {
|
||||
pds := make([]*PackageDescriptor, 0, len(pvs))
|
||||
for _, pv := range pvs {
|
||||
pd, err := GetPackageDescriptor(ctx, pv)
|
||||
pd, err := getPackageDescriptor(ctx, pv, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Vendored
+19
-156
@@ -5,176 +5,39 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// cacheContext is a context that can be used to cache data in a request level context
|
||||
// This is useful for caching data that is expensive to calculate and is likely to be
|
||||
// used multiple times in a request.
|
||||
type cacheContext struct {
|
||||
data map[any]map[any]any
|
||||
lock sync.RWMutex
|
||||
created time.Time
|
||||
discard bool
|
||||
}
|
||||
type cacheContextKeyType struct{}
|
||||
|
||||
func (cc *cacheContext) Get(tp, key any) any {
|
||||
cc.lock.RLock()
|
||||
defer cc.lock.RUnlock()
|
||||
return cc.data[tp][key]
|
||||
}
|
||||
var cacheContextKey = cacheContextKeyType{}
|
||||
|
||||
func (cc *cacheContext) Put(tp, key, value any) {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
|
||||
if cc.discard {
|
||||
return
|
||||
}
|
||||
|
||||
d := cc.data[tp]
|
||||
if d == nil {
|
||||
d = make(map[any]any)
|
||||
cc.data[tp] = d
|
||||
}
|
||||
d[key] = value
|
||||
}
|
||||
|
||||
func (cc *cacheContext) Delete(tp, key any) {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
delete(cc.data[tp], key)
|
||||
}
|
||||
|
||||
func (cc *cacheContext) Discard() {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
cc.data = nil
|
||||
cc.discard = true
|
||||
}
|
||||
|
||||
func (cc *cacheContext) isDiscard() bool {
|
||||
cc.lock.RLock()
|
||||
defer cc.lock.RUnlock()
|
||||
return cc.discard
|
||||
}
|
||||
|
||||
// cacheContextLifetime is the max lifetime of cacheContext.
|
||||
// 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
|
||||
|
||||
func (cc *cacheContext) Expired() bool {
|
||||
return timeNow().Sub(cc.created) > cacheContextLifetime
|
||||
}
|
||||
|
||||
var cacheContextKey = struct{}{}
|
||||
|
||||
/*
|
||||
Since there are both WithCacheContext and WithNoCacheContext,
|
||||
it may be confusing when there is nesting.
|
||||
|
||||
Some cases to explain the design:
|
||||
|
||||
When:
|
||||
- A, B or C means a cache context.
|
||||
- A', B' or C' means a discard cache context.
|
||||
- ctx means context.Backgrand().
|
||||
- A(ctx) means a cache context with ctx as the parent context.
|
||||
- B(A(ctx)) means a cache context with A(ctx) as the parent context.
|
||||
- With is alias of WithCacheContext.
|
||||
- WithNo is alias of WithNoCacheContext.
|
||||
|
||||
So:
|
||||
- With(ctx) -> A(ctx)
|
||||
- With(With(ctx)) -> A(ctx), not B(A(ctx)), always reuse parent cache context if possible.
|
||||
- With(With(With(ctx))) -> A(ctx), not C(B(A(ctx))), ditto.
|
||||
- WithNo(ctx) -> ctx, not A'(ctx), don't create new cache context if we don't have to.
|
||||
- WithNo(With(ctx)) -> A'(ctx)
|
||||
- WithNo(WithNo(With(ctx))) -> A'(ctx), not B'(A'(ctx)), don't create new cache context if we don't have to.
|
||||
- With(WithNo(With(ctx))) -> B(A'(ctx)), not A(ctx), never reuse a discard cache context.
|
||||
- WithNo(With(WithNo(With(ctx)))) -> B'(A'(ctx))
|
||||
- With(WithNo(With(WithNo(With(ctx))))) -> C(B'(A'(ctx))), so there's always only one not-discard cache context.
|
||||
*/
|
||||
// contextCacheLifetime is the max lifetime of context cache.
|
||||
// Since context cache is used to cache data in a request level context, 5 minutes is enough.
|
||||
// If a context cache is used more than 5 minutes, it's probably abused.
|
||||
const contextCacheLifetime = 5 * time.Minute
|
||||
|
||||
func WithCacheContext(ctx context.Context) context.Context {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
if !c.isDiscard() {
|
||||
// reuse parent context
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
|
||||
func WithNoCacheContext(ctx context.Context) context.Context {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
// The caller want to run long-life tasks, but the parent context is a cache context.
|
||||
// So we should disable and clean the cache data, or it will be kept in memory for a long time.
|
||||
c.Discard()
|
||||
if c := GetContextCache(ctx); c != nil {
|
||||
return ctx
|
||||
}
|
||||
|
||||
return ctx
|
||||
return context.WithValue(ctx, cacheContextKey, NewEphemeralCache(contextCacheLifetime))
|
||||
}
|
||||
|
||||
func GetContextData(ctx context.Context, tp, key any) any {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
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, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return nil
|
||||
}
|
||||
return c.Get(tp, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetContextData(ctx context.Context, tp, key, value any) {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
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, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Put(tp, key, value)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveContextData(ctx context.Context, tp, key any) {
|
||||
if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
|
||||
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, is highly likely to be misused for long-life tasks: %v", c)
|
||||
return
|
||||
}
|
||||
c.Delete(tp, key)
|
||||
}
|
||||
func GetContextCache(ctx context.Context) *EphemeralCache {
|
||||
c, _ := ctx.Value(cacheContextKey).(*EphemeralCache)
|
||||
return c
|
||||
}
|
||||
|
||||
// GetWithContextCache returns the cache value of the given key in the given context.
|
||||
// FIXME: in some cases, the "context cache" should not be used, because it has uncontrollable behaviors
|
||||
// For example, these calls:
|
||||
// * GetWithContextCache(TargetID) -> OtherCodeCreateModel(TargetID) -> GetWithContextCache(TargetID)
|
||||
// Will cause the second call is not able to get the correct created target.
|
||||
// UNLESS it is certain that the target won't be changed during the request, DO NOT use it.
|
||||
func GetWithContextCache[T, K any](ctx context.Context, groupKey string, targetKey K, f func(context.Context, K) (T, error)) (T, error) {
|
||||
v := GetContextData(ctx, groupKey, targetKey)
|
||||
if vv, ok := v.(T); ok {
|
||||
return vv, nil
|
||||
if c := GetContextCache(ctx); c != nil {
|
||||
return GetWithEphemeralCache(ctx, c, groupKey, targetKey, f)
|
||||
}
|
||||
t, err := f(ctx, targetKey)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
SetContextData(ctx, groupKey, targetKey, t)
|
||||
return t, nil
|
||||
return f(ctx, targetKey)
|
||||
}
|
||||
|
||||
Vendored
+15
-43
@@ -8,27 +8,29 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWithCacheContext(t *testing.T) {
|
||||
ctx := WithCacheContext(t.Context())
|
||||
|
||||
v := GetContextData(ctx, "empty_field", "my_config1")
|
||||
c := GetContextCache(ctx)
|
||||
v, _ := c.Get("empty_field", "my_config1")
|
||||
assert.Nil(t, v)
|
||||
|
||||
const field = "system_setting"
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
c.Put(field, "my_config1", 1)
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.NotNil(t, v)
|
||||
assert.Equal(t, 1, v.(int))
|
||||
|
||||
RemoveContextData(ctx, field, "my_config1")
|
||||
RemoveContextData(ctx, field, "my_config2") // remove a non-exist key
|
||||
c.Delete(field, "my_config1")
|
||||
c.Delete(field, "my_config2") // remove a non-exist key
|
||||
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
|
||||
vInt, err := GetWithContextCache(ctx, field, "my_config1", func(context.Context, string) (int, error) {
|
||||
@@ -37,42 +39,12 @@ func TestWithCacheContext(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, vInt)
|
||||
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.EqualValues(t, 1, v)
|
||||
|
||||
now := timeNow
|
||||
defer func() {
|
||||
timeNow = now
|
||||
}()
|
||||
timeNow = func() time.Time {
|
||||
return now().Add(5 * time.Minute)
|
||||
}
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
defer test.MockVariableValue(&timeNow, func() time.Time {
|
||||
return time.Now().Add(5 * time.Minute)
|
||||
})()
|
||||
v, _ = c.Get(field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestWithNoCacheContext(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
const field = "system_setting"
|
||||
|
||||
v := GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v) // still no cache
|
||||
|
||||
ctx = WithCacheContext(ctx)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.NotNil(t, v)
|
||||
|
||||
ctx = WithNoCacheContext(ctx)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v)
|
||||
SetContextData(ctx, field, "my_config1", 1)
|
||||
v = GetContextData(ctx, field, "my_config1")
|
||||
assert.Nil(t, v) // still no cache
|
||||
}
|
||||
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// EphemeralCache is a cache that can be used to store data in a request level context
|
||||
// This is useful for caching data that is expensive to calculate and is likely to be
|
||||
// used multiple times in a request.
|
||||
type EphemeralCache struct {
|
||||
data map[any]map[any]any
|
||||
lock sync.RWMutex
|
||||
created time.Time
|
||||
checkLifeTime time.Duration
|
||||
}
|
||||
|
||||
var timeNow = time.Now
|
||||
|
||||
func NewEphemeralCache(checkLifeTime ...time.Duration) *EphemeralCache {
|
||||
return &EphemeralCache{
|
||||
data: make(map[any]map[any]any),
|
||||
created: timeNow(),
|
||||
checkLifeTime: util.OptionalArg(checkLifeTime, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *EphemeralCache) checkExceededLifeTime(tp, key any) bool {
|
||||
if cc.checkLifeTime > 0 && timeNow().Sub(cc.created) > cc.checkLifeTime {
|
||||
log.Warn("EphemeralCache is expired, is highly likely to be abused for long-life tasks: %v, %v", tp, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (cc *EphemeralCache) Get(tp, key any) (any, bool) {
|
||||
if cc.checkExceededLifeTime(tp, key) {
|
||||
return nil, false
|
||||
}
|
||||
cc.lock.RLock()
|
||||
defer cc.lock.RUnlock()
|
||||
ret, ok := cc.data[tp][key]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (cc *EphemeralCache) Put(tp, key, value any) {
|
||||
if cc.checkExceededLifeTime(tp, key) {
|
||||
return
|
||||
}
|
||||
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
|
||||
d := cc.data[tp]
|
||||
if d == nil {
|
||||
d = make(map[any]any)
|
||||
cc.data[tp] = d
|
||||
}
|
||||
d[key] = value
|
||||
}
|
||||
|
||||
func (cc *EphemeralCache) Delete(tp, key any) {
|
||||
if cc.checkExceededLifeTime(tp, key) {
|
||||
return
|
||||
}
|
||||
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
delete(cc.data[tp], key)
|
||||
}
|
||||
|
||||
func GetWithEphemeralCache[T, K any](ctx context.Context, c *EphemeralCache, groupKey string, targetKey K, f func(context.Context, K) (T, error)) (T, error) {
|
||||
v, has := c.Get(groupKey, targetKey)
|
||||
if vv, ok := v.(T); has && ok {
|
||||
return vv, nil
|
||||
}
|
||||
t, err := f(ctx, targetKey)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
c.Put(groupKey, targetKey, t)
|
||||
return t, nil
|
||||
}
|
||||
@@ -13,9 +13,8 @@ import (
|
||||
// Package registry settings
|
||||
var (
|
||||
Packages = struct {
|
||||
Storage *Storage
|
||||
Enabled bool
|
||||
ChunkedUploadPath string
|
||||
Storage *Storage
|
||||
Enabled bool
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
@@ -65,13 +64,6 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if HasInstallLock(rootCfg) {
|
||||
Packages.ChunkedUploadPath, err = AppDataTempDir("package-upload").MkdirAllSub("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create chunked upload directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
|
||||
Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
|
||||
Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH")
|
||||
|
||||
@@ -5,6 +5,7 @@ package setting
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -98,6 +99,13 @@ var Service = struct {
|
||||
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
||||
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
||||
} `ini:"service.explore"`
|
||||
|
||||
QoS struct {
|
||||
Enabled bool
|
||||
MaxInFlightRequests int
|
||||
MaxWaitingRequests int
|
||||
TargetWaitTime time.Duration
|
||||
}
|
||||
}{
|
||||
AllowedUserVisibilityModesSlice: []bool{true, true, true},
|
||||
}
|
||||
@@ -255,6 +263,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "service.explore", &Service.Explore)
|
||||
|
||||
loadOpenIDSetting(rootCfg)
|
||||
loadQosSetting(rootCfg)
|
||||
}
|
||||
|
||||
func loadOpenIDSetting(rootCfg ConfigProvider) {
|
||||
@@ -276,3 +285,11 @@ func loadOpenIDSetting(rootCfg ConfigProvider) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadQosSetting(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("qos")
|
||||
Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false)
|
||||
Service.QoS.MaxInFlightRequests = sec.Key("MAX_INFLIGHT").MustInt(4 * runtime.NumCPU())
|
||||
Service.QoS.MaxWaitingRequests = sec.Key("MAX_WAITING").MustInt(100)
|
||||
Service.QoS.TargetWaitTime = sec.Key("TARGET_WAIT_TIME").MustDuration(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ type Package struct {
|
||||
|
||||
// PackageFile represents a package file
|
||||
type PackageFile struct {
|
||||
ID int64 `json:"id"`
|
||||
Size int64
|
||||
ID int64 `json:"id"`
|
||||
Size int64 `json:"size"`
|
||||
Name string `json:"name"`
|
||||
HashMD5 string `json:"md5"`
|
||||
HashSHA1 string `json:"sha1"`
|
||||
|
||||
@@ -117,6 +117,7 @@ files = Files
|
||||
|
||||
error = Error
|
||||
error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
|
||||
error503 = The server was unable to complete your request. Please try again later.
|
||||
go_back = Go Back
|
||||
invalid_data = Invalid data: %v
|
||||
|
||||
|
||||
@@ -1544,14 +1544,19 @@ func Routes() *web.Router {
|
||||
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
||||
m.Group("/packages/{username}", func() {
|
||||
m.Group("/{type}/{name}", func() {
|
||||
m.Get("/", packages.ListPackageVersions)
|
||||
|
||||
m.Group("/{version}", func() {
|
||||
m.Get("", packages.GetPackage)
|
||||
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
|
||||
m.Get("/files", packages.ListPackageFiles)
|
||||
})
|
||||
|
||||
m.Post("/-/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
|
||||
m.Post("/-/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
|
||||
m.Group("/-", func() {
|
||||
m.Get("/latest", packages.GetLatestPackageVersion)
|
||||
m.Post("/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
|
||||
m.Post("/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
|
||||
})
|
||||
})
|
||||
|
||||
m.Get("/", packages.ListPackages)
|
||||
|
||||
@@ -56,13 +56,10 @@ func ListPackages(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
packageType := ctx.FormTrim("type")
|
||||
query := ctx.FormTrim("q")
|
||||
|
||||
pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{
|
||||
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(packageType),
|
||||
Name: packages.SearchValue{Value: query},
|
||||
Type: packages.Type(ctx.FormTrim("type")),
|
||||
Name: packages.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &listOptions,
|
||||
})
|
||||
@@ -71,22 +68,6 @@ func ListPackages(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
pds, err := packages.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiPackages := make([]*api.Package, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiPackages = append(apiPackages, apiPackage)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(count), listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiPackages)
|
||||
@@ -217,6 +198,121 @@ func ListPackageFiles(ctx *context.APIContext) {
|
||||
ctx.JSON(http.StatusOK, apiPackageFiles)
|
||||
}
|
||||
|
||||
// ListPackageVersions gets all versions of a package
|
||||
func ListPackageVersions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /packages/{owner}/{type}/{name} package listPackageVersions
|
||||
// ---
|
||||
// summary: Gets all versions of a package
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: type
|
||||
// in: path
|
||||
// description: type of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: name of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PackageList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(ctx.PathParam("type")),
|
||||
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &listOptions,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(count), listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiPackages)
|
||||
}
|
||||
|
||||
// GetLatestPackageVersion gets the latest version of a package
|
||||
func GetLatestPackageVersion(ctx *context.APIContext) {
|
||||
// swagger:operation GET /packages/{owner}/{type}/{name}/-/latest package getLatestPackageVersion
|
||||
// ---
|
||||
// summary: Gets the latest version of a package
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: type
|
||||
// in: path
|
||||
// description: type of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: name of the package
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Package"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
pvs, _, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(ctx.PathParam("type")),
|
||||
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
pd, err := packages.GetPackageDescriptor(ctx, pvs[0])
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiPackage)
|
||||
}
|
||||
|
||||
// LinkPackage sets a repository link for a package
|
||||
func LinkPackage(ctx *context.APIContext) {
|
||||
// swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage
|
||||
@@ -335,3 +431,26 @@ func UnlinkPackage(ctx *context.APIContext) {
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func searchPackages(ctx *context.APIContext, opts *packages.PackageSearchOptions) ([]*api.Package, int64, error) {
|
||||
pvs, count, err := packages.SearchVersions(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
pds, err := packages.GetPackageDescriptors(ctx, pvs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
apiPackages := make([]*api.Package, 0, len(pds))
|
||||
for _, pd := range pds {
|
||||
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
apiPackages = append(apiPackages, apiPackage)
|
||||
}
|
||||
|
||||
return apiPackages, count, nil
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func Migrate(ctx *context.APIContext) {
|
||||
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
|
||||
IsMirror: opts.Mirror,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
handleMigrateError(ctx, repoOwner, err)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
giteacontext "code.gitea.io/gitea/services/context"
|
||||
|
||||
"github.com/bohde/codel"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
const tplStatus503 templates.TplName = "status/503"
|
||||
|
||||
type Priority int
|
||||
|
||||
func (p Priority) String() string {
|
||||
switch p {
|
||||
case HighPriority:
|
||||
return "high"
|
||||
case DefaultPriority:
|
||||
return "default"
|
||||
case LowPriority:
|
||||
return "low"
|
||||
default:
|
||||
return fmt.Sprintf("%d", p)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
LowPriority = Priority(-10)
|
||||
DefaultPriority = Priority(0)
|
||||
HighPriority = Priority(10)
|
||||
)
|
||||
|
||||
// QoS implements quality of service for requests, based upon whether
|
||||
// or not the user is logged in. All traffic may get dropped, and
|
||||
// anonymous users are deprioritized.
|
||||
func QoS() func(next http.Handler) http.Handler {
|
||||
if !setting.Service.QoS.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxOutstanding := setting.Service.QoS.MaxInFlightRequests
|
||||
if maxOutstanding <= 0 {
|
||||
maxOutstanding = 10
|
||||
}
|
||||
|
||||
c := codel.NewPriority(codel.Options{
|
||||
// The maximum number of waiting requests.
|
||||
MaxPending: setting.Service.QoS.MaxWaitingRequests,
|
||||
// The maximum number of in-flight requests.
|
||||
MaxOutstanding: maxOutstanding,
|
||||
// The target latency that a blocked request should wait
|
||||
// for. After this, it might be dropped.
|
||||
TargetLatency: setting.Service.QoS.TargetWaitTime,
|
||||
})
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
priority := requestPriority(ctx)
|
||||
|
||||
// Check if the request can begin processing.
|
||||
err := c.Acquire(ctx, int(priority))
|
||||
if err != nil {
|
||||
log.Error("QoS error, dropping request of priority %s: %v", priority, err)
|
||||
renderServiceUnavailable(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Release long-polling immediately, so they don't always
|
||||
// take up an in-flight request
|
||||
if strings.Contains(req.URL.Path, "/user/events") {
|
||||
c.Release()
|
||||
} else {
|
||||
defer c.Release()
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// requestPriority assigns a priority value for a request based upon
|
||||
// whether the user is logged in and how expensive the endpoint is
|
||||
func requestPriority(ctx context.Context) Priority {
|
||||
// If the user is logged in, assign high priority.
|
||||
data := middleware.GetContextData(ctx)
|
||||
if _, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
||||
return HighPriority
|
||||
}
|
||||
|
||||
rctx := chi.RouteContext(ctx)
|
||||
if rctx == nil {
|
||||
return DefaultPriority
|
||||
}
|
||||
|
||||
// If we're operating in the context of a repo, assign low priority
|
||||
routePattern := rctx.RoutePattern()
|
||||
if strings.HasPrefix(routePattern, "/{username}/{reponame}/") {
|
||||
return LowPriority
|
||||
}
|
||||
|
||||
return DefaultPriority
|
||||
}
|
||||
|
||||
// renderServiceUnavailable will render an HTTP 503 Service
|
||||
// Unavailable page, providing HTML if the client accepts it.
|
||||
func renderServiceUnavailable(w http.ResponseWriter, req *http.Request) {
|
||||
acceptsHTML := false
|
||||
for _, part := range req.Header["Accept"] {
|
||||
if strings.Contains(part, "text/html") {
|
||||
acceptsHTML = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the client doesn't accept HTML, then render a plain text response
|
||||
if !acceptsHTML {
|
||||
http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
tmplCtx := giteacontext.TemplateContext{}
|
||||
tmplCtx["Locale"] = middleware.Locale(w, req)
|
||||
ctxData := middleware.GetContextData(req.Context())
|
||||
err := templates.HTMLRenderer().HTML(w, http.StatusServiceUnavailable, tplStatus503, ctxData, tmplCtx)
|
||||
if err != nil {
|
||||
log.Error("Error occurs again when rendering service unavailable page: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Internal server error, please collect error logs and report to Gitea issue tracker"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRequestPriority(t *testing.T) {
|
||||
type test struct {
|
||||
Name string
|
||||
User *user_model.User
|
||||
RoutePattern string
|
||||
Expected Priority
|
||||
}
|
||||
|
||||
cases := []test{
|
||||
{
|
||||
Name: "Logged In",
|
||||
User: &user_model.User{},
|
||||
Expected: HighPriority,
|
||||
},
|
||||
{
|
||||
Name: "Sign In",
|
||||
RoutePattern: "/user/login",
|
||||
Expected: DefaultPriority,
|
||||
},
|
||||
{
|
||||
Name: "Repo Home",
|
||||
RoutePattern: "/{username}/{reponame}",
|
||||
Expected: DefaultPriority,
|
||||
},
|
||||
{
|
||||
Name: "User Repo",
|
||||
RoutePattern: "/{username}/{reponame}/src/branch/main",
|
||||
Expected: LowPriority,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "")
|
||||
|
||||
if tc.User != nil {
|
||||
data := middleware.GetContextData(ctx)
|
||||
data[middleware.ContextDataKeySignedUser] = tc.User
|
||||
}
|
||||
|
||||
rctx := chi.RouteContext(ctx)
|
||||
rctx.RoutePatterns = []string{tc.RoutePattern}
|
||||
|
||||
assert.Exactly(t, tc.Expected, requestPriority(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderServiceUnavailable(t *testing.T) {
|
||||
t.Run("HTML", func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "")
|
||||
ctx.Req.Header.Set("Accept", "text/html")
|
||||
|
||||
renderServiceUnavailable(resp, ctx.Req)
|
||||
assert.Equal(t, http.StatusServiceUnavailable, resp.Code)
|
||||
assert.Contains(t, resp.Header().Get("Content-Type"), "text/html")
|
||||
|
||||
body := resp.Body.String()
|
||||
assert.Contains(t, body, `lang="en-US"`)
|
||||
assert.Contains(t, body, "503 Service Unavailable")
|
||||
})
|
||||
|
||||
t.Run("plain", func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "")
|
||||
ctx.Req.Header.Set("Accept", "text/plain")
|
||||
|
||||
renderServiceUnavailable(resp, ctx.Req)
|
||||
assert.Equal(t, http.StatusServiceUnavailable, resp.Code)
|
||||
assert.Contains(t, resp.Header().Get("Content-Type"), "text/plain")
|
||||
|
||||
body := resp.Body.String()
|
||||
assert.Contains(t, body, "503 Service Unavailable")
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
gotemplate "html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -69,7 +70,7 @@ func RefBlame(ctx *context.Context) {
|
||||
blob := entry.Blob()
|
||||
fileSize := blob.Size()
|
||||
ctx.Data["FileSize"] = fileSize
|
||||
ctx.Data["FileName"] = blob.Name()
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
|
||||
tplName := tplRepoViewContent
|
||||
if !ctx.FormBool("only_content") {
|
||||
@@ -285,8 +286,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
|
||||
if i != len(lines)-1 {
|
||||
line += "\n"
|
||||
}
|
||||
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
|
||||
line, lexerNameForLine := highlight.Code(fileName, language, line)
|
||||
line, lexerNameForLine := highlight.Code(path.Base(ctx.Repo.TreePath), language, line)
|
||||
|
||||
// set lexer name to the first detected lexer. this is certainly suboptimal and
|
||||
// we should instead highlight the whole file at once
|
||||
|
||||
@@ -215,13 +215,12 @@ func SearchCommits(ctx *context.Context) {
|
||||
|
||||
// FileHistory show a file's reversions
|
||||
func FileHistory(ctx *context.Context) {
|
||||
fileName := ctx.Repo.TreePath
|
||||
if len(fileName) == 0 {
|
||||
if ctx.Repo.TreePath == "" {
|
||||
Commits(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), fileName) // FIXME: legacy code used ShortName
|
||||
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("FileCommitsCount", err)
|
||||
return
|
||||
@@ -238,7 +237,7 @@ func FileHistory(ctx *context.Context) {
|
||||
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
|
||||
File: fileName,
|
||||
File: ctx.Repo.TreePath,
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -253,7 +252,7 @@ func FileHistory(ctx *context.Context) {
|
||||
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["FileName"] = fileName
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
|
||||
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
||||
|
||||
@@ -160,7 +160,6 @@ func editFile(ctx *context.Context, isNewFile bool) {
|
||||
defer dataRc.Close()
|
||||
|
||||
ctx.Data["FileSize"] = blob.Size()
|
||||
ctx.Data["FileName"] = blob.Name()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, _ := util.ReadAtMost(dataRc, buf)
|
||||
|
||||
@@ -52,7 +52,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
|
||||
ctx.Data["FileIsSymlink"] = entry.IsLink()
|
||||
ctx.Data["FileName"] = blob.Name()
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||
|
||||
if ctx.Repo.TreePath == ".editorconfig" {
|
||||
|
||||
@@ -162,7 +162,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
defer dataRc.Close()
|
||||
|
||||
ctx.Data["FileIsText"] = fInfo.isTextFile
|
||||
ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
|
||||
ctx.Data["FileTreePath"] = path.Join(subfolder, readmeFile.Name())
|
||||
ctx.Data["FileSize"] = fInfo.fileSize
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
||||
|
||||
|
||||
+1
-1
@@ -285,7 +285,7 @@ func Routes() *web.Router {
|
||||
|
||||
webRoutes := web.NewRouter()
|
||||
webRoutes.Use(mid...)
|
||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
|
||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
|
||||
routes.Mount("", webRoutes)
|
||||
return routes
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito
|
||||
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
|
||||
IsMirror: opts.Mirror,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
})
|
||||
}, false)
|
||||
} else {
|
||||
r, err = repo_model.GetRepositoryByID(ctx, opts.MigrateToRepoID)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func (g *GithubDownloaderV3) LogString() string {
|
||||
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
|
||||
githubClient := github.NewClient(client)
|
||||
if baseURL != "https://github.com" {
|
||||
githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
|
||||
githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
|
||||
}
|
||||
g.clients = append(g.clients, githubClient)
|
||||
g.rates = append(g.rates, nil)
|
||||
@@ -872,3 +872,18 @@ func (g *GithubDownloaderV3) GetReviews(ctx context.Context, reviewable base.Rev
|
||||
}
|
||||
return allReviews, nil
|
||||
}
|
||||
|
||||
// FormatCloneURL add authentication into remote URLs
|
||||
func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
|
||||
u, err := url.Parse(remoteAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(opts.AuthToken) > 0 {
|
||||
// "multiple tokens" are used to benefit more "API rate limit quota"
|
||||
// git clone doesn't count for rate limits, so only use the first token.
|
||||
// source: https://github.com/orgs/community/discussions/44515
|
||||
u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGitHubDownloadRepo(t *testing.T) {
|
||||
@@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) {
|
||||
},
|
||||
}, reviews)
|
||||
}
|
||||
|
||||
func TestGithubMultiToken(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
token string
|
||||
expectedCloneURL string
|
||||
}{
|
||||
{
|
||||
desc: "Single Token",
|
||||
token: "single_token",
|
||||
expectedCloneURL: "https://oauth2:single_token@github.com",
|
||||
},
|
||||
{
|
||||
desc: "Multi Token",
|
||||
token: "token1,token2",
|
||||
expectedCloneURL: "https://oauth2:token1@github.com",
|
||||
},
|
||||
}
|
||||
factory := GithubDownloaderV3Factory{}
|
||||
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.desc, func(t *testing.T) {
|
||||
opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
|
||||
client, err := factory.New(t.Context(), opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tC.expectedCloneURL, cloneURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,8 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
// Create repos.
|
||||
repoIDs := make([]int64, 0)
|
||||
for i := 0; i < 3; i++ {
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(),
|
||||
repo_service.CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}, true)
|
||||
assert.NoError(t, err, "CreateRepository %d", i)
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
@@ -267,7 +268,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create repo and check teams repositories.
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: "repo-last"})
|
||||
r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: "repo-last"}, true)
|
||||
assert.NoError(t, err, "CreateRepository last")
|
||||
if r != nil {
|
||||
repoIDs = append(repoIDs, r.ID)
|
||||
|
||||
@@ -213,7 +213,7 @@ func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.Use
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{
|
||||
Name: IndexRepositoryName,
|
||||
})
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateRepository: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,8 +30,12 @@ type BlobUploader struct {
|
||||
reading bool
|
||||
}
|
||||
|
||||
func buildFilePath(id string) string {
|
||||
return util.FilePathJoinAbs(setting.Packages.ChunkedUploadPath, id)
|
||||
func uploadPathTempDir() *tempdir.TempDir {
|
||||
return setting.AppDataTempDir("package-upload")
|
||||
}
|
||||
|
||||
func buildFilePath(uploadPath *tempdir.TempDir, id string) string {
|
||||
return uploadPath.JoinPath(id)
|
||||
}
|
||||
|
||||
// NewBlobUploader creates a new blob uploader for the given id
|
||||
@@ -48,7 +52,12 @@ func NewBlobUploader(ctx context.Context, id string) (*BlobUploader, error) {
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(buildFilePath(model.ID), os.O_RDWR|os.O_CREATE, 0o666)
|
||||
uploadPath := uploadPathTempDir()
|
||||
_, err = uploadPath.MkdirAllSub("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := os.OpenFile(buildFilePath(uploadPath, model.ID), os.O_RDWR|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -118,13 +127,13 @@ func (u *BlobUploader) Read(p []byte) (int, error) {
|
||||
return u.file.Read(p)
|
||||
}
|
||||
|
||||
// Remove deletes the data and the model of a blob upload
|
||||
// RemoveBlobUploadByID Remove deletes the data and the model of a blob upload
|
||||
func RemoveBlobUploadByID(ctx context.Context, id string) error {
|
||||
if err := packages_model.DeleteBlobUploadByID(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := os.Remove(buildFilePath(id))
|
||||
err := os.Remove(buildFilePath(uploadPathTempDir(), id))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -199,7 +199,10 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re
|
||||
}
|
||||
|
||||
// CreateRepositoryDirectly creates a repository for the user/organization.
|
||||
func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
|
||||
// if needsUpdateToReady is true, it will update the repository status to ready when success
|
||||
func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
|
||||
opts CreateRepoOptions, needsUpdateToReady bool,
|
||||
) (*repo_model.Repository, error) {
|
||||
if !doer.CanCreateRepoIn(owner) {
|
||||
return nil, repo_model.ErrReachLimitOfRepo{
|
||||
Limit: owner.MaxRepoCreation,
|
||||
@@ -243,8 +246,6 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
|
||||
ObjectFormatName: opts.ObjectFormatName,
|
||||
}
|
||||
|
||||
needsUpdateStatus := opts.Status != repo_model.RepositoryReady
|
||||
|
||||
// 1 - create the repository database operations first
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return createRepositoryInDB(ctx, doer, owner, repo, false)
|
||||
@@ -318,7 +319,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User,
|
||||
}
|
||||
|
||||
// 7 - update repository status to be ready
|
||||
if needsUpdateStatus {
|
||||
if needsUpdateToReady {
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestCreateRepositoryDirectly(t *testing.T) {
|
||||
|
||||
createdRepo, err := CreateRepositoryDirectly(git.DefaultContext, user2, user2, CreateRepoOptions{
|
||||
Name: "created-repo",
|
||||
})
|
||||
}, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, createdRepo)
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestCreateRepositoryDirectly(t *testing.T) {
|
||||
|
||||
createdRepo2, err := CreateRepositoryDirectly(db.DefaultContext, user2, user2, CreateRepoOptions{
|
||||
Name: "created-repo",
|
||||
})
|
||||
}, true)
|
||||
assert.Nil(t, createdRepo2)
|
||||
assert.Error(t, err)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ type WebSearchResults struct {
|
||||
|
||||
// CreateRepository creates a repository for the user/organization.
|
||||
func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
|
||||
repo, err := CreateRepositoryDirectly(ctx, doer, owner, opts)
|
||||
repo, err := CreateRepositoryDirectly(ctx, doer, owner, opts, true)
|
||||
if err != nil {
|
||||
// No need to rollback here we should do this in CreateRepository...
|
||||
return nil, err
|
||||
|
||||
@@ -111,7 +111,7 @@ func CreateMigrateTask(ctx context.Context, doer, u *user_model.User, opts base.
|
||||
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
|
||||
IsMirror: opts.Mirror,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
task.EndTime = timeutil.TimeStampNow()
|
||||
task.Status = structs.TaskStatusFailed
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
|
||||
<div class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content">
|
||||
<h4 class="file-header ui top attached header tw-flex tw-items-center tw-justify-between tw-flex-wrap">
|
||||
<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
|
||||
{{template "repo/file_info" .}}
|
||||
|
||||
@@ -70,15 +70,15 @@
|
||||
{{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}}
|
||||
{{if not $.PageIsWiki}}
|
||||
{{/* view single file diff */}}
|
||||
{{if $.FileName}}
|
||||
{{if $.FileTreePath}}
|
||||
<a class="btn interact-bg tw-p-2 view-single-diff" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_file_diff"}}"
|
||||
href="{{$commitRepoLink}}/commit/{{.ID.String}}?files={{$.FileName}}"
|
||||
href="{{$commitRepoLink}}/commit/{{.ID.String}}?files={{$.FileTreePath}}"
|
||||
>{{svg "octicon-file-diff"}}</a>
|
||||
{{end}}
|
||||
|
||||
{{/* view at history point */}}
|
||||
{{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
|
||||
{{if $.FileName}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileName)}}{{end}}
|
||||
{{if $.FileTreePath}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileTreePath)}}{{end}}
|
||||
<a class="btn interact-bg tw-p-2 view-commit-path" data-tooltip-content="{{ctx.Locale.Tr "repo.commits.view_path"}}" href="{{$viewCommitLink}}">{{svg "octicon-file-code"}}</a>
|
||||
{{end}}
|
||||
</td>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
{{end}}
|
||||
|
||||
{{if .ReadmeExist}}
|
||||
<a class="flex-text-block muted" href="{{.TreeLink}}/{{.FileName}}">
|
||||
<a class="flex-text-block muted" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}/{{PathEscapeSegments .FileTreePath}}">
|
||||
{{svg "octicon-book"}} {{ctx.Locale.Tr "readme"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
<div class="repo-setting-content">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "repo.settings.branch_protection" .Rule.RuleName}}
|
||||
{{if .Rule.RuleName}}
|
||||
{{ctx.Locale.Tr "repo.settings.branch_protection" .Rule.RuleName}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.settings.branches.add_new_rule"}}
|
||||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment branch-protection">
|
||||
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.protect_patterns"}}</h5>
|
||||
<div class="field">
|
||||
<div class="field required">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern"}}</label>
|
||||
<input name="rule_name" type="text" value="{{.Rule.RuleName}}">
|
||||
<input name="rule_name" type="text" value="{{.Rule.RuleName}}" required>
|
||||
<input name="rule_id" type="hidden" value="{{.Rule.ID}}">
|
||||
<p class="help tw-ml-0">{{ctx.Locale.Tr "repo.settings.protect_branch_name_pattern_desc" "https://github.com/gobwas/glob"}}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileName}} non-diff-file-content">
|
||||
<div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content">
|
||||
{{- if .FileError}}
|
||||
<div class="ui error message">
|
||||
<div class="text left tw-whitespace-pre">{{.FileError}}</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4">
|
||||
{{if .ReadmeInList}}
|
||||
{{svg "octicon-book" 16 "tw-mr-2"}}
|
||||
<strong><a class="muted" href="#readme">{{.FileName}}</a></strong>
|
||||
<strong><a class="muted" href="#readme">{{.FileTreePath}}</a></strong>
|
||||
{{else}}
|
||||
{{template "repo/file_info" .}}
|
||||
{{end}}
|
||||
@@ -78,7 +78,7 @@
|
||||
<button class="ui mini basic button escape-button tw-mr-1">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
|
||||
{{end}}
|
||||
{{if and .ReadmeInList .CanEditReadmeFile}}
|
||||
<a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}/{{PathEscapeSegments .FileName}}">{{svg "octicon-pencil"}}</a>
|
||||
<a class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.editor.edit_this_file"}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .FileTreePath}}">{{svg "octicon-pencil"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="503 Service Unavailable" class="page-content">
|
||||
<div class="ui container">
|
||||
<div class="status-page-error">
|
||||
<div class="status-page-error-title">503 Service Unavailable</div>
|
||||
<div class="tw-text-center">
|
||||
<div class="tw-my-4">{{ctx.Locale.Tr "error503"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
Generated
+103
-4
@@ -3339,6 +3339,104 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/packages/{owner}/{type}/{name}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"package"
|
||||
],
|
||||
"summary": "Gets all versions of a package",
|
||||
"operationId": "listPackageVersions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the package",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "type of the package",
|
||||
"name": "type",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the package",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page number of results to return (1-based)",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "page size of results",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/PackageList"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/packages/{owner}/{type}/{name}/-/latest": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"package"
|
||||
],
|
||||
"summary": "Gets the latest version of a package",
|
||||
"operationId": "getLatestPackageVersion",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the package",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "type of the package",
|
||||
"name": "type",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the package",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/Package"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/packages/{owner}/{type}/{name}/-/link/{repo_name}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -24386,10 +24484,6 @@
|
||||
"description": "PackageFile represents a package file",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Size": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
@@ -24414,6 +24508,11 @@
|
||||
"sha512": {
|
||||
"type": "string",
|
||||
"x-go-name": "HashSHA512"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "Size"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
@@ -83,70 +83,101 @@ func TestPackageAPI(t *testing.T) {
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.NotNil(t, p.Creator)
|
||||
assert.Equal(t, user.Name, p.Creator.UserName)
|
||||
})
|
||||
|
||||
t.Run("RepositoryLink", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
t.Run("ListPackageVersions", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
_, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
|
||||
assert.NoError(t, err)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// no repository link
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var apiPackages []*api.Package
|
||||
DecodeJSON(t, resp, &apiPackages)
|
||||
|
||||
var ap1 *api.Package
|
||||
DecodeJSON(t, resp, &ap1)
|
||||
assert.Nil(t, ap1.Repository)
|
||||
assert.Len(t, apiPackages, 1)
|
||||
assert.Equal(t, string(packages_model.TypeGeneric), apiPackages[0].Type)
|
||||
assert.Equal(t, packageName, apiPackages[0].Name)
|
||||
assert.Equal(t, packageVersion, apiPackages[0].Version)
|
||||
})
|
||||
|
||||
// create a repository
|
||||
newRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
|
||||
Name: "repo4",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
t.Run("LatestPackageVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// link to public repository
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, newRepo.Name)).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/latest", user.Name, packageName)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var apiPackage *api.Package
|
||||
DecodeJSON(t, resp, &apiPackage)
|
||||
|
||||
var ap2 *api.Package
|
||||
DecodeJSON(t, resp, &ap2)
|
||||
assert.NotNil(t, ap2.Repository)
|
||||
assert.Equal(t, newRepo.ID, ap2.Repository.ID)
|
||||
assert.Equal(t, string(packages_model.TypeGeneric), apiPackage.Type)
|
||||
assert.Equal(t, packageName, apiPackage.Name)
|
||||
assert.Equal(t, packageVersion, apiPackage.Version)
|
||||
})
|
||||
|
||||
// link to repository without write access, should fail
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, "repo3")).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
t.Run("RepositoryLink", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// remove link
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/unlink", user.Name, packageName)).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
_, err := packages_model.GetPackageByName(db.DefaultContext, user.ID, packages_model.TypeGeneric, packageName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
// no repository link
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var ap3 *api.Package
|
||||
DecodeJSON(t, resp, &ap3)
|
||||
assert.Nil(t, ap3.Repository)
|
||||
var ap1 *api.Package
|
||||
DecodeJSON(t, resp, &ap1)
|
||||
assert.Nil(t, ap1.Repository)
|
||||
|
||||
// force link to a repository the currently logged-in user doesn't have access to
|
||||
privateRepoID := int64(6)
|
||||
assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, privateRepoID))
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var ap4 *api.Package
|
||||
DecodeJSON(t, resp, &ap4)
|
||||
assert.Nil(t, ap4.Repository)
|
||||
|
||||
assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, privateRepoID))
|
||||
// create a repository
|
||||
newRepo, err := repo_service.CreateRepository(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
|
||||
Name: "repo4",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// link to public repository
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, newRepo.Name)).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var ap2 *api.Package
|
||||
DecodeJSON(t, resp, &ap2)
|
||||
assert.NotNil(t, ap2.Repository)
|
||||
assert.Equal(t, newRepo.ID, ap2.Repository.ID)
|
||||
|
||||
// link to repository without write access, should fail
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/link/%s", user.Name, packageName, "repo3")).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// remove link
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/-/unlink", user.Name, packageName)).AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var ap3 *api.Package
|
||||
DecodeJSON(t, resp, &ap3)
|
||||
assert.Nil(t, ap3.Repository)
|
||||
|
||||
// force link to a repository the currently logged-in user doesn't have access to
|
||||
privateRepoID := int64(6)
|
||||
assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, ap1.ID, privateRepoID))
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).AddTokenAuth(tokenReadPackage)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var ap4 *api.Package
|
||||
DecodeJSON(t, resp, &ap4)
|
||||
assert.Nil(t, ap4.Repository)
|
||||
|
||||
assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, privateRepoID))
|
||||
})
|
||||
|
||||
t.Run("ListPackageFiles", func(t *testing.T) {
|
||||
|
||||
@@ -133,7 +133,7 @@ func TestCompareCodeExpand(t *testing.T) {
|
||||
Readme: "Default",
|
||||
AutoInit: true,
|
||||
DefaultBranch: "main",
|
||||
})
|
||||
}, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session := loginUser(t, user1.Name)
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestMirrorPull(t *testing.T) {
|
||||
IsPrivate: opts.Private,
|
||||
IsMirror: opts.Mirror,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
})
|
||||
}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ func testMirrorPush(t *testing.T, u *url.URL) {
|
||||
|
||||
mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
|
||||
Name: "test-push-mirror",
|
||||
})
|
||||
}, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
AutoInit: true,
|
||||
ObjectFormatName: git.Sha1ObjectFormat.Name(),
|
||||
DefaultBranch: "master",
|
||||
})
|
||||
}, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// add CODEOWNERS to default branch
|
||||
|
||||
Reference in New Issue
Block a user