diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go
index a713fe0d8b85c..6bcf740c238b0 100644
--- a/models/db/iterate_test.go
+++ b/models/db/iterate_test.go
@@ -25,7 +25,7 @@ func TestIterate(t *testing.T) {
return nil
})
assert.NoError(t, err)
- assert.EqualValues(t, 83, repoCnt)
+ assert.EqualValues(t, 84, repoCnt)
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID}
diff --git a/models/db/list_test.go b/models/db/list_test.go
index ffef1e4948eea..1295692cec856 100644
--- a/models/db/list_test.go
+++ b/models/db/list_test.go
@@ -35,11 +35,11 @@ func TestFind(t *testing.T) {
var repoUnits []repo_model.RepoUnit
err := db.Find(db.DefaultContext, &opts, &repoUnits)
assert.NoError(t, err)
- assert.EqualValues(t, 83, len(repoUnits))
+ assert.EqualValues(t, 84, len(repoUnits))
cnt, err := db.Count(db.DefaultContext, &opts, new(repo_model.RepoUnit))
assert.NoError(t, err)
- assert.EqualValues(t, 83, cnt)
+ assert.EqualValues(t, 84, cnt)
repoUnits = make([]repo_model.RepoUnit, 0, 10)
newCnt, err := db.FindAndCount(db.DefaultContext, &opts, &repoUnits)
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 503b8c9ddfa46..ef0b7c1a941e2 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -569,3 +569,9 @@
type: 3
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
created_unix: 946684810
+
+-
+ id: 84
+ repo_id: 56
+ type: 1
+ created_unix: 946684810
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index dd8facb7a3134..32ba8744d4522 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -1634,3 +1634,16 @@
is_private: true
num_issues: 1
status: 0
+
+-
+ id: 56
+ owner_id: 2
+ owner_name: user2
+ lower_name: readme-test
+ name: readme-test
+ default_branch: master
+ is_empty: false
+ is_archived: false
+ is_private: true
+ status: 0
+ num_issues: 0
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index c6081f07d06b7..ce54defacdc67 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -66,7 +66,7 @@
num_followers: 2
num_following: 1
num_stars: 2
- num_repos: 11
+ num_repos: 12
num_teams: 0
num_members: 0
visibility: 0
diff --git a/models/git/lfs_lock.go b/models/git/lfs_lock.go
index 25480f3f96541..178fa72f09bf6 100644
--- a/models/git/lfs_lock.go
+++ b/models/git/lfs_lock.go
@@ -6,7 +6,6 @@ package git
import (
"context"
"fmt"
- "path"
"strings"
"time"
@@ -17,6 +16,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
// LFSLock represents a git lfs lock of repository.
@@ -34,11 +34,7 @@ func init() {
// BeforeInsert is invoked from XORM before inserting an object of this type.
func (l *LFSLock) BeforeInsert() {
- l.Path = cleanPath(l.Path)
-}
-
-func cleanPath(p string) string {
- return path.Clean("/" + p)[1:]
+ l.Path = util.CleanPath(l.Path)
}
// CreateLFSLock creates a new lock.
@@ -53,7 +49,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
return nil, err
}
- lock.Path = cleanPath(lock.Path)
+ lock.Path = util.CleanPath(lock.Path)
lock.RepoID = repo.ID
l, err := GetLFSLock(dbCtx, repo, lock.Path)
@@ -73,7 +69,7 @@ func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLo
// GetLFSLock returns release by given path.
func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (*LFSLock, error) {
- path = cleanPath(path)
+ path = util.CleanPath(path)
rel := &LFSLock{RepoID: repo.ID}
has, err := db.GetEngine(ctx).Where("lower(path) = ?", strings.ToLower(path)).Get(rel)
if err != nil {
diff --git a/modules/cache/context.go b/modules/cache/context.go
index f741a87445383..62bbf5dcba84a 100644
--- a/modules/cache/context.go
+++ b/modules/cache/context.go
@@ -6,6 +6,7 @@ package cache
import (
"context"
"sync"
+ "time"
"code.gitea.io/gitea/modules/log"
)
@@ -14,65 +15,151 @@ import (
// 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 {
- ctx context.Context
- data map[any]map[any]any
- lock sync.RWMutex
+ data map[any]map[any]any
+ lock sync.RWMutex
+ created time.Time
+ discard bool
}
func (cc *cacheContext) Get(tp, key any) any {
cc.lock.RLock()
defer cc.lock.RUnlock()
- if cc.data[tp] == nil {
- return nil
- }
return cc.data[tp][key]
}
func (cc *cacheContext) Put(tp, key, value any) {
cc.lock.Lock()
defer cc.lock.Unlock()
- if cc.data[tp] == nil {
- cc.data[tp] = make(map[any]any)
+
+ if cc.discard {
+ return
+ }
+
+ d := cc.data[tp]
+ if d == nil {
+ d = make(map[any]any)
+ cc.data[tp] = d
}
- cc.data[tp][key] = value
+ d[key] = value
}
func (cc *cacheContext) Delete(tp, key any) {
cc.lock.Lock()
defer cc.lock.Unlock()
- if cc.data[tp] == nil {
- return
- }
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, 10s is enough.
+// If a cacheContext is used more than 10s, it's probably misuse.
+const cacheContextLifetime = 10 * time.Second
+
+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.
+*/
+
func WithCacheContext(ctx context.Context) context.Context {
+ if c, ok := ctx.Value(cacheContextKey).(*cacheContext); ok {
+ if !c.isDiscard() {
+ // reuse parent context
+ return ctx
+ }
+ }
return context.WithValue(ctx, cacheContextKey, &cacheContext{
- ctx: ctx,
- data: make(map[any]map[any]any),
+ 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()
+ return ctx
+ }
+
+ return ctx
+}
+
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, may be misused for long-life tasks: %v", c)
+ return nil
+ }
return c.Get(tp, key)
}
- log.Warn("cannot get cache context when getting data: %v", ctx)
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, may be misused for long-life tasks: %v", c)
+ return
+ }
c.Put(tp, key, value)
return
}
- log.Warn("cannot get cache context when setting data: %v", ctx)
}
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, may be misused for long-life tasks: %v", c)
+ return
+ }
c.Delete(tp, key)
}
}
diff --git a/modules/cache/context_test.go b/modules/cache/context_test.go
index 77e3ecad2ca70..5315547865e19 100644
--- a/modules/cache/context_test.go
+++ b/modules/cache/context_test.go
@@ -6,6 +6,7 @@ package cache
import (
"context"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
)
@@ -25,7 +26,7 @@ func TestWithCacheContext(t *testing.T) {
assert.EqualValues(t, 1, v.(int))
RemoveContextData(ctx, field, "my_config1")
- RemoveContextData(ctx, field, "my_config2") // remove an non-exist key
+ RemoveContextData(ctx, field, "my_config2") // remove a non-exist key
v = GetContextData(ctx, field, "my_config1")
assert.Nil(t, v)
@@ -38,4 +39,40 @@ func TestWithCacheContext(t *testing.T) {
v = GetContextData(ctx, field, "my_config1")
assert.EqualValues(t, 1, v)
+
+ now := timeNow
+ defer func() {
+ timeNow = now
+ }()
+ timeNow = func() time.Time {
+ return now().Add(10 * time.Second)
+ }
+ v = GetContextData(ctx, field, "my_config1")
+ assert.Nil(t, v)
+}
+
+func TestWithNoCacheContext(t *testing.T) {
+ ctx := context.Background()
+
+ 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
}
diff --git a/modules/context/api.go b/modules/context/api.go
index 3f938948aed66..f7a3384691246 100644
--- a/modules/context/api.go
+++ b/modules/context/api.go
@@ -244,7 +244,7 @@ func APIContexter() func(http.Handler) http.Handler {
}
}
- httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
+ httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
ctx.Data["Context"] = &ctx
diff --git a/modules/context/context.go b/modules/context/context.go
index 0c8d7411ed5db..50c34edae2e53 100644
--- a/modules/context/context.go
+++ b/modules/context/context.go
@@ -388,7 +388,7 @@ func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
if duration == 0 {
duration = 5 * time.Minute
}
- httpcache.AddCacheControlToHeader(header, duration)
+ httpcache.SetCacheControlInHeader(header, duration)
if !opts.LastModified.IsZero() {
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
@@ -753,7 +753,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
}
}
- httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform")
+ httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
ctx.Data["CsrfToken"] = ctx.csrf.GetToken()
diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go
index f0caa30eb82b3..46e0152ef4553 100644
--- a/modules/httpcache/httpcache.go
+++ b/modules/httpcache/httpcache.go
@@ -15,8 +15,8 @@ import (
"code.gitea.io/gitea/modules/setting"
)
-// AddCacheControlToHeader adds suitable cache-control headers to response
-func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
+// SetCacheControlInHeader sets suitable cache-control headers in the response
+func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
directives := make([]string, 0, 2+len(additionalDirectives))
// "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store"
@@ -31,7 +31,7 @@ func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDire
directives = append(directives, "max-age=0", "private", "must-revalidate")
// to remind users they are using non-prod setting.
- h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
+ h.Set("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
}
h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", "))
@@ -50,7 +50,7 @@ func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (
// HandleGenericTimeCache handles time-based caching for a HTTP request
func HandleGenericTimeCache(req *http.Request, w http.ResponseWriter, lastModified time.Time) (handled bool) {
- AddCacheControlToHeader(w.Header(), setting.StaticCacheTime)
+ SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
ifModifiedSince := req.Header.Get("If-Modified-Since")
if ifModifiedSince != "" {
@@ -81,7 +81,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin
return true
}
}
- AddCacheControlToHeader(w.Header(), setting.StaticCacheTime)
+ SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
return false
}
@@ -125,6 +125,6 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s
}
}
}
- AddCacheControlToHeader(w.Header(), setting.StaticCacheTime)
+ SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
return false
}
diff --git a/modules/label/parser.go b/modules/label/parser.go
index 768c72a61b014..55bf570de6b95 100644
--- a/modules/label/parser.go
+++ b/modules/label/parser.go
@@ -36,17 +36,17 @@ func (err ErrTemplateLoad) Error() string {
// GetTemplateFile loads the label template file by given name,
// then parses and returns a list of name-color pairs and optionally description.
func GetTemplateFile(name string) ([]*Label, error) {
- data, err := options.GetRepoInitFile("label", name+".yaml")
+ data, err := options.Labels(name + ".yaml")
if err == nil && len(data) > 0 {
return parseYamlFormat(name+".yaml", data)
}
- data, err = options.GetRepoInitFile("label", name+".yml")
+ data, err = options.Labels(name + ".yml")
if err == nil && len(data) > 0 {
return parseYamlFormat(name+".yml", data)
}
- data, err = options.GetRepoInitFile("label", name)
+ data, err = options.Labels(name)
if err != nil {
return nil, ErrTemplateLoad{name, fmt.Errorf("GetRepoInitFile: %w", err)}
}
diff --git a/modules/options/base.go b/modules/options/base.go
index 039e934b3a4d9..e83e8df5d0945 100644
--- a/modules/options/base.go
+++ b/modules/options/base.go
@@ -7,11 +7,52 @@ import (
"fmt"
"io/fs"
"os"
+ "path"
"path/filepath"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
+// Locale reads the content of a specific locale from static/bindata or custom path.
+func Locale(name string) ([]byte, error) {
+ return fileFromDir(path.Join("locale", util.CleanPath(name)))
+}
+
+// Readme reads the content of a specific readme from static/bindata or custom path.
+func Readme(name string) ([]byte, error) {
+ return fileFromDir(path.Join("readme", util.CleanPath(name)))
+}
+
+// Gitignore reads the content of a gitignore locale from static/bindata or custom path.
+func Gitignore(name string) ([]byte, error) {
+ return fileFromDir(path.Join("gitignore", util.CleanPath(name)))
+}
+
+// License reads the content of a specific license from static/bindata or custom path.
+func License(name string) ([]byte, error) {
+ return fileFromDir(path.Join("license", util.CleanPath(name)))
+}
+
+// Labels reads the content of a specific labels from static/bindata or custom path.
+func Labels(name string) ([]byte, error) {
+ return fileFromDir(path.Join("label", util.CleanPath(name)))
+}
+
+// WalkLocales reads the content of a specific locale
+func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
+ if IsDynamic() {
+ if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to walk locales. Error: %w", err)
+ }
+ }
+
+ if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("failed to walk locales. Error: %w", err)
+ }
+ return nil
+}
+
func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, err error) error) error {
if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
// name is the path relative to the root
@@ -37,3 +78,18 @@ func walkAssetDir(root string, callback func(path, name string, d fs.DirEntry, e
}
return nil
}
+
+func statDirIfExist(dir string) ([]string, error) {
+ isDir, err := util.IsDir(dir)
+ if err != nil {
+ return nil, fmt.Errorf("unable to check if static directory %s is a directory. %w", dir, err)
+ }
+ if !isDir {
+ return nil, nil
+ }
+ files, err := util.StatDir(dir, true)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read directory %q. %w", dir, err)
+ }
+ return files, nil
+}
diff --git a/modules/options/dynamic.go b/modules/options/dynamic.go
index a20253676e671..8c954492ae51f 100644
--- a/modules/options/dynamic.go
+++ b/modules/options/dynamic.go
@@ -7,10 +7,8 @@ package options
import (
"fmt"
- "io/fs"
"os"
"path"
- "path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -27,76 +25,20 @@ func Dir(name string) ([]string, error) {
var result []string
- customDir := path.Join(setting.CustomPath, "options", name)
-
- isDir, err := util.IsDir(customDir)
- if err != nil {
- return []string{}, fmt.Errorf("Unabe to check if custom directory %s is a directory. %w", customDir, err)
- }
- if isDir {
- files, err := util.StatDir(customDir, true)
- if err != nil {
- return []string{}, fmt.Errorf("Failed to read custom directory. %w", err)
- }
-
- result = append(result, files...)
- }
-
- staticDir := path.Join(setting.StaticRootPath, "options", name)
-
- isDir, err = util.IsDir(staticDir)
- if err != nil {
- return []string{}, fmt.Errorf("unable to check if static directory %s is a directory. %w", staticDir, err)
- }
- if isDir {
- files, err := util.StatDir(staticDir, true)
+ for _, dir := range []string{
+ path.Join(setting.CustomPath, "options", name), // custom dir
+ path.Join(setting.StaticRootPath, "options", name), // static dir
+ } {
+ files, err := statDirIfExist(dir)
if err != nil {
- return []string{}, fmt.Errorf("Failed to read static directory. %w", err)
+ return nil, err
}
-
result = append(result, files...)
}
return directories.AddAndGet(name, result), nil
}
-// Locale reads the content of a specific locale from static or custom path.
-func Locale(name string) ([]byte, error) {
- return fileFromDir(path.Join("locale", name))
-}
-
-// WalkLocales reads the content of a specific locale from static or custom path.
-func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
- if err := walkAssetDir(filepath.Join(setting.StaticRootPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
- return fmt.Errorf("failed to walk locales. Error: %w", err)
- }
-
- if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
- return fmt.Errorf("failed to walk locales. Error: %w", err)
- }
- return nil
-}
-
-// Readme reads the content of a specific readme from static or custom path.
-func Readme(name string) ([]byte, error) {
- return fileFromDir(path.Join("readme", name))
-}
-
-// Gitignore reads the content of a specific gitignore from static or custom path.
-func Gitignore(name string) ([]byte, error) {
- return fileFromDir(path.Join("gitignore", name))
-}
-
-// License reads the content of a specific license from static or custom path.
-func License(name string) ([]byte, error) {
- return fileFromDir(path.Join("license", name))
-}
-
-// Labels reads the content of a specific labels from static or custom path.
-func Labels(name string) ([]byte, error) {
- return fileFromDir(path.Join("label", name))
-}
-
// fileFromDir is a helper to read files from static or custom path.
func fileFromDir(name string) ([]byte, error) {
customPath := path.Join(setting.CustomPath, "options", name)
diff --git a/modules/options/repo.go b/modules/options/repo.go
deleted file mode 100644
index 1480f7808176c..0000000000000
--- a/modules/options/repo.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package options
-
-import (
- "fmt"
- "os"
- "path"
- "strings"
-
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
-)
-
-// GetRepoInitFile returns repository init files
-func GetRepoInitFile(tp, name string) ([]byte, error) {
- cleanedName := strings.TrimLeft(path.Clean("/"+name), "/")
- relPath := path.Join("options", tp, cleanedName)
-
- // Use custom file when available.
- customPath := path.Join(setting.CustomPath, relPath)
- isFile, err := util.IsFile(customPath)
- if err != nil {
- log.Error("Unable to check if %s is a file. Error: %v", customPath, err)
- }
- if isFile {
- return os.ReadFile(customPath)
- }
-
- switch tp {
- case "readme":
- return Readme(cleanedName)
- case "gitignore":
- return Gitignore(cleanedName)
- case "license":
- return License(cleanedName)
- case "label":
- return Labels(cleanedName)
- default:
- return []byte{}, fmt.Errorf("Invalid init file type")
- }
-}
diff --git a/modules/options/static.go b/modules/options/static.go
index ff3c86d3f84f6..549f4e25b11a3 100644
--- a/modules/options/static.go
+++ b/modules/options/static.go
@@ -8,10 +8,8 @@ package options
import (
"fmt"
"io"
- "io/fs"
"os"
"path"
- "path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -28,17 +26,14 @@ func Dir(name string) ([]string, error) {
var result []string
- customDir := path.Join(setting.CustomPath, "options", name)
- isDir, err := util.IsDir(customDir)
- if err != nil {
- return []string{}, fmt.Errorf("unable to check if custom directory %q is a directory. %w", customDir, err)
- }
- if isDir {
- files, err := util.StatDir(customDir, true)
+ for _, dir := range []string{
+ path.Join(setting.CustomPath, "options", name), // custom dir
+ // no static dir
+ } {
+ files, err := statDirIfExist(dir)
if err != nil {
- return []string{}, fmt.Errorf("unable to read custom directory %q. %w", customDir, err)
+ return nil, err
}
-
result = append(result, files...)
}
@@ -69,39 +64,6 @@ func AssetDir(dirName string) ([]string, error) {
return results, nil
}
-// Locale reads the content of a specific locale from bindata or custom path.
-func Locale(name string) ([]byte, error) {
- return fileFromDir(path.Join("locale", name))
-}
-
-// WalkLocales reads the content of a specific locale from static or custom path.
-func WalkLocales(callback func(path, name string, d fs.DirEntry, err error) error) error {
- if err := walkAssetDir(filepath.Join(setting.CustomPath, "options", "locale"), callback); err != nil && !os.IsNotExist(err) {
- return fmt.Errorf("failed to walk locales. Error: %w", err)
- }
- return nil
-}
-
-// Readme reads the content of a specific readme from bindata or custom path.
-func Readme(name string) ([]byte, error) {
- return fileFromDir(path.Join("readme", name))
-}
-
-// Gitignore reads the content of a gitignore locale from bindata or custom path.
-func Gitignore(name string) ([]byte, error) {
- return fileFromDir(path.Join("gitignore", name))
-}
-
-// License reads the content of a specific license from bindata or custom path.
-func License(name string) ([]byte, error) {
- return fileFromDir(path.Join("license", name))
-}
-
-// Labels reads the content of a specific labels from static or custom path.
-func Labels(name string) ([]byte, error) {
- return fileFromDir(path.Join("label", name))
-}
-
// fileFromDir is a helper to read files from bindata or custom path.
func fileFromDir(name string) ([]byte, error) {
customPath := path.Join(setting.CustomPath, "options", name)
diff --git a/modules/public/public.go b/modules/public/public.go
index 42026f9b10549..e1d60d89eb9f9 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -6,7 +6,6 @@ package public
import (
"net/http"
"os"
- "path"
"path/filepath"
"strings"
@@ -14,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
// Options represents the available options to configure the handler.
@@ -103,7 +103,7 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
func (opts *Options) handle(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) bool {
// use clean to keep the file is a valid path with no . or ..
- f, err := fs.Open(path.Clean(file))
+ f, err := fs.Open(util.CleanPath(file))
if err != nil {
if os.IsNotExist(err) {
return false
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 49c8d2a904d1a..f9a33cd4f68c9 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -136,7 +136,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir,
}
// README
- data, err := options.GetRepoInitFile("readme", opts.Readme)
+ data, err := options.Readme(opts.Readme)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err)
}
@@ -164,7 +164,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir,
var buf bytes.Buffer
names := strings.Split(opts.Gitignores, ",")
for _, name := range names {
- data, err = options.GetRepoInitFile("gitignore", name)
+ data, err = options.Gitignore(name)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err)
}
@@ -182,7 +182,7 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir,
// LICENSE
if len(opts.License) > 0 {
- data, err = options.GetRepoInitFile("license", opts.License)
+ data, err = options.License(opts.License)
if err != nil {
return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.License, err)
}
diff --git a/modules/storage/local.go b/modules/storage/local.go
index a6a9d54a8ca30..05bf1fb28a56c 100644
--- a/modules/storage/local.go
+++ b/modules/storage/local.go
@@ -8,7 +8,6 @@ import (
"io"
"net/url"
"os"
- "path"
"path/filepath"
"strings"
@@ -59,7 +58,7 @@ func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
}
func (l *LocalStorage) buildLocalPath(p string) string {
- return filepath.Join(l.dir, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:])
+ return filepath.Join(l.dir, util.CleanPath(strings.ReplaceAll(p, "\\", "/")))
}
// Open a file
diff --git a/modules/storage/minio.go b/modules/storage/minio.go
index c427d8d7e3122..24da14b634631 100644
--- a/modules/storage/minio.go
+++ b/modules/storage/minio.go
@@ -15,6 +15,7 @@ import (
"time"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/util"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
@@ -120,7 +121,7 @@ func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error
}
func (m *MinioStorage) buildMinioPath(p string) string {
- return strings.TrimPrefix(path.Join(m.basePath, path.Clean("/" + strings.ReplaceAll(p, "\\", "/"))[1:]), "/")
+ return strings.TrimPrefix(path.Join(m.basePath, util.CleanPath(strings.ReplaceAll(p, "\\", "/"))), "/")
}
// Open open a file
diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go
index 5b215496b80c8..7887fd42b72ef 100644
--- a/modules/typesniffer/typesniffer.go
+++ b/modules/typesniffer/typesniffer.go
@@ -106,6 +106,16 @@ func DetectContentType(data []byte) SniffedType {
}
}
+ if strings.HasPrefix(ct, "audio/") && bytes.HasPrefix(data, []byte("ID3")) {
+ // The MP3 detection is quite inaccurate, any content with "ID3" prefix will result in "audio/mpeg".
+ // So remove the "ID3" prefix and detect again, if result is text, then it must be text content.
+ // This works especially because audio files contain many unprintable/invalid characters like `0x00`
+ ct2 := http.DetectContentType(data[3:])
+ if strings.HasPrefix(ct2, "text/") {
+ ct = ct2
+ }
+ }
+
return SniffedType{ct}
}
diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go
index 2bafdffd141c3..6c6da34aa006a 100644
--- a/modules/typesniffer/typesniffer_test.go
+++ b/modules/typesniffer/typesniffer_test.go
@@ -109,6 +109,10 @@ func TestIsAudio(t *testing.T) {
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
assert.True(t, DetectContentType(mp3).IsAudio())
assert.False(t, DetectContentType([]byte("plain text")).IsAudio())
+
+ assert.True(t, DetectContentType([]byte("ID3Toy\000")).IsAudio())
+ assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ...")).IsText()) // test ID3 tag for plain text
+ assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
}
func TestDetectContentTypeFromReader(t *testing.T) {
diff --git a/modules/util/path.go b/modules/util/path.go
index 74acb7a85fd87..5aa9e15f5c3e9 100644
--- a/modules/util/path.go
+++ b/modules/util/path.go
@@ -14,6 +14,14 @@ import (
"strings"
)
+// CleanPath ensure to clean the path
+func CleanPath(p string) string {
+ if strings.HasPrefix(p, "/") {
+ return path.Clean(p)
+ }
+ return path.Clean("/" + p)[1:]
+}
+
// EnsureAbsolutePath ensure that a path is absolute, making it
// relative to absoluteBase if necessary
func EnsureAbsolutePath(path, absoluteBase string) string {
diff --git a/modules/util/path_test.go b/modules/util/path_test.go
index 93f4f67cf6486..2f020f924dd2a 100644
--- a/modules/util/path_test.go
+++ b/modules/util/path_test.go
@@ -136,3 +136,15 @@ func TestMisc_IsReadmeFileName(t *testing.T) {
assert.Equal(t, testCase.idx, idx)
}
}
+
+func TestCleanPath(t *testing.T) {
+ cases := map[string]string{
+ "../../test": "test",
+ "/test": "/test",
+ "/../test": "/test",
+ }
+
+ for k, v := range cases {
+ assert.Equal(t, v, CleanPath(k))
+ }
+}
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index edf7376e7cc45..4f2e0ebb13c3e 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -57,6 +57,7 @@ new_mirror=新鏡像
new_fork=新增儲存庫 fork
new_org=新增組織
new_project=新增專案
+new_project_column=新增欄位
manage_org=管理組織
admin_panel=網站管理
account_settings=帳戶設定
@@ -90,9 +91,11 @@ disabled=已停用
copy=複製
copy_url=複製 URL
+copy_content=複製內容
copy_branch=複製分支名稱
copy_success=複製成功!
copy_error=複製失敗
+copy_type_unsupported=無法複製此類型的檔案
write=撰寫
preview=預覽
@@ -109,8 +112,14 @@ never=從來沒有
rss_feed=RSS 摘要
[aria]
+navbar=導航列
+footer=頁尾
+footer.software=關於軟體
+footer.links=連結
[filter]
+string.asc=A - Z
+string.desc=Z - A
[error]
occurred=發生錯誤
@@ -238,7 +247,10 @@ default_enable_timetracking_popup=預設情況下啟用新存儲庫的時間跟
no_reply_address=隱藏電子信箱域名
no_reply_address_helper=作為隱藏電子信箱使用者的域名。例如,如果隱藏的電子信箱域名設定為「noreply.example.org」,帳號「joe」將以「joe@noreply.example.org」的身分登錄到 Git 中。
password_algorithm=密碼雜湊演算法
+invalid_password_algorithm=無效的密碼雜湊演算法
password_algorithm_helper=設定密碼雜湊演算法。演算法有不同的需求和強度。「argon2」雖然有優秀的特性但會占用大量記憶體,所以可能不適用於小型系統。
+enable_update_checker=啟用更新檢查器
+enable_update_checker_helper=定期連線到 gitea.io 檢查更新。
[home]
uname_holder=帳號或電子信箱
@@ -285,6 +297,8 @@ org_no_results=沒有找到符合的組織。
code_no_results=找不到符合您關鍵字的原始碼。
code_search_results=「%s」的搜尋結果
code_last_indexed_at=最後索引 %s
+relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。
+relevant_repositories=只顯示相關的儲存庫,顯示未篩選的結果。
[auth]
@@ -357,6 +371,7 @@ password_pwned_err=無法完成對 HaveIBeenPwned 的請求。
[mail]
view_it_on=在 %s 上查看
+reply=或是直接回覆此電子郵件
link_not_working_do_paste=無法開啟?請複製超連結到瀏覽器貼上。
hi_user_x=%s 您好,
@@ -491,6 +506,7 @@ user_not_exist=該用戶名不存在
team_not_exist=團隊不存在
last_org_owner=你不能從「Owners」團隊中刪除最後一個使用者。每個組織中至少要有一個擁有者。
cannot_add_org_to_team=組織不能被新增為團隊成員。
+organization_leave_success=您已成功離開組織 %s。
invalid_ssh_key=無法驗證您的 SSH 密鑰:%s
invalid_gpg_key=無法驗證您的 GPG 密鑰:%s
@@ -1002,10 +1018,12 @@ unstar=移除星號
star=加上星號
fork=Fork
download_archive=下載此儲存庫
+more_operations=更多操作
no_desc=暫無描述
quick_guide=快速幫助
clone_this_repo=Clone 此儲存庫
+cite_this_repo=引用此儲存庫
create_new_repo_command=從命令列建立新儲存庫。
push_exist_repo=從命令列推送已存在的儲存庫
empty_message=此儲存庫未包含任何內容。
@@ -1155,6 +1173,7 @@ commits.signed_by_untrusted_user_unmatched=由不受信任且與提交者不相
commits.gpg_key_id=GPG 金鑰 ID
commits.ssh_key_fingerprint=SSH 金鑰指紋
+commit.operations=操作
commit.revert=還原
commit.revert-header=還原: %s
commit.revert-content=選擇還原的目標分支:
@@ -1187,11 +1206,22 @@ projects.type.bug_triage=Bug 檢傷分類
projects.template.desc=專案範本
projects.template.desc_helper=選擇專案範本以開始
projects.type.uncategorized=未分類
+projects.column.edit=編輯欄位
projects.column.edit_title=組織名稱
projects.column.new_title=組織名稱
+projects.column.new_submit=建立欄位
+projects.column.new=新增欄位
+projects.column.set_default=設為預設
+projects.column.set_default_desc=將此欄位設定為未分類問題及合併請求的預設預設值
+projects.column.delete=刪除欄位
+projects.column.deletion_desc=刪除專案欄位會將所有相關的問題移動到「未分類」,是否繼續?
projects.column.color=彩色
projects.open=開啟
projects.close=關閉
+projects.column.assigned_to=已指派給
+projects.card_type.desc=卡片預覽
+projects.card_type.images_and_text=圖片和文字
+projects.card_type.text_only=純文字
issues.desc=管理錯誤報告、任務和里程碑。
issues.filter_assignees=篩選負責人
@@ -1268,6 +1298,7 @@ issues.filter_label_no_select=所有標籤
issues.filter_milestone=里程碑
issues.filter_milestone_no_select=所有里程碑
issues.filter_project=專案
+issues.filter_project_all=所有專案
issues.filter_project_none=未選擇專案
issues.filter_assignee=負責人
issues.filter_assginee_no_select=所有負責人
@@ -1300,6 +1331,8 @@ issues.action_milestone=里程碑
issues.action_milestone_no_select=無里程碑
issues.action_assignee=負責人
issues.action_assignee_no_select=沒有負責人
+issues.action_check=選取/取消選取
+issues.action_check_all=全選/取消全選
issues.opened_by=建立於 %[1]s 由 %[3]s
pulls.merged_by=由 %[3]s 建立,合併於 %[1]s
pulls.merged_by_fake=由 %[2]s 建立,合併於 %[1]s
@@ -1798,6 +1831,7 @@ settings.mirror_sync_in_progress=鏡像同步正在進行中。 請稍後再回
settings.site=網站
settings.update_settings=更新設定
settings.branches.update_default_branch=更新預設分支
+settings.branches.add_new_rule=加入新規則
settings.advanced_settings=進階設定
settings.wiki_desc=啟用儲存庫 Wiki
settings.use_internal_wiki=使用內建 Wiki
@@ -1827,8 +1861,11 @@ settings.pulls.ignore_whitespace=衝突時忽略空白
settings.pulls.enable_autodetect_manual_merge=啟用自動偵測手動合併 (注意: 在某些特殊情況下可能發生誤判)
settings.pulls.allow_rebase_update=啟用透過 Rebase 更新合併請求分支
settings.pulls.default_delete_branch_after_merge=預設在合併後刪除合併請求分支
+settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行編輯
+settings.releases_desc=啟用儲存庫版本發佈
settings.packages_desc=啟用儲存庫套件註冊中心
settings.projects_desc=啟用儲存庫專案
+settings.actions_desc=啟用儲存庫 Actions
settings.admin_settings=管理員設定
settings.admin_enable_health_check=啟用儲存庫的健康檢查 (git fsck)
settings.admin_code_indexer=程式碼索引器
@@ -2038,6 +2075,8 @@ settings.deploy_key_deletion_desc=移除部署金鑰將拒絕它存取此儲存
settings.deploy_key_deletion_success=部署金鑰已移除。
settings.branches=分支
settings.protected_branch=分支保護
+settings.protected_branch.save_rule=儲存規則
+settings.protected_branch.delete_rule=刪除規則
settings.protected_branch_can_push=允許推送?
settings.protected_branch_can_push_yes=你可以推送
settings.protected_branch_can_push_no=你不能推送
@@ -2072,6 +2111,7 @@ settings.dismiss_stale_approvals=捨棄過時的核可
settings.dismiss_stale_approvals_desc=當新的提交有修改到合併請求的內容,並被推送到此分支時,將捨棄舊的核可。
settings.require_signed_commits=僅接受經簽署的提交
settings.require_signed_commits_desc=拒絕未經簽署或未經驗證的提交推送到此分支。
+settings.protect_branch_name_pattern=受保護的分支名稱模式
settings.protect_protected_file_patterns=受保護的檔案模式 (以分號區隔「\;」):
settings.protect_protected_file_patterns_desc=即便使用者有權限新增、修改、刪除此分支的檔案,仍不允許直接修改受保護的檔案。可以用半形分號「\;」分隔多個模式。請於github.com/gobwas/glob 文件查看模式格式。範例:.drone.yml
, /docs/**/*.txt
。
settings.protect_unprotected_file_patterns=未受保護的檔案模式 (以分號區隔「\;」):
@@ -2080,6 +2120,7 @@ settings.add_protected_branch=啟用保護
settings.delete_protected_branch=停用保護
settings.update_protect_branch_success=已更新「%s」的分支保護。
settings.remove_protected_branch_success=已停用「%s」的分支保護。
+settings.remove_protected_branch_failed=刪除分支保護規則「%s」失敗。
settings.protected_branch_deletion=停用分支保護
settings.protected_branch_deletion_desc=停用分支保護將允許有寫入權限的使用者推送至該分支,是否繼續?
settings.block_rejected_reviews=有退回的審核時阻擋合併
@@ -2089,9 +2130,13 @@ settings.block_on_official_review_requests_desc=如果有官方的審核請求
settings.block_outdated_branch=如果合併請求已經過時則阻擋合併
settings.block_outdated_branch_desc=當 head 分支落後於基礎分支時不得合併。
settings.default_branch_desc=請選擇用來提交程式碼和合併請求的預設分支。
+settings.merge_style_desc=合併方式
+settings.default_merge_style_desc=預設合併方式
settings.choose_branch=選擇一個分支...
settings.no_protected_branch=沒有受保護的分支。
settings.edit_protected_branch=編輯
+settings.protected_branch_required_rule_name=必須填寫規則名稱
+settings.protected_branch_duplicate_rule_name=規則名稱已存在
settings.protected_branch_required_approvals_min=需要的核可數量不能為負數。
settings.tags=標籤
settings.tags.protection=標籤保護
@@ -2247,6 +2292,8 @@ release.downloads=下載附件
release.download_count=下載次數:%s
release.add_tag_msg=使用此版本的標題和內容作為標籤訊息。
release.add_tag=只建立標籤
+release.releases_for=%s 的版本發佈
+release.tags_for=%s 的標籤
branch.name=分支名稱
branch.search=搜尋分支
@@ -2305,7 +2352,7 @@ org_full_name_holder=組織全名
org_name_helper=組織名稱應該要簡短且方便記憶
create_org=建立組織
repo_updated=更新於
-members=成員數
+members=成員
teams=團隊
code=程式碼
lower_members=名成員
@@ -2957,6 +3004,7 @@ monitor.queue.pool.cancel_desc=讓佇列沒有任何工作者群組可能造成
notices.system_notice_list=系統提示
notices.view_detail_header=查看提示細節
+notices.operations=操作
notices.select_all=選取全部
notices.deselect_all=取消所有選取
notices.inverse_selection=反向選取
@@ -3081,6 +3129,8 @@ keywords=關鍵字
details=詳情
details.author=作者
details.project_site=專案網站
+details.repository_site=儲存庫網站
+details.documentation_site=文件網站
details.license=授權條款
assets=檔案
versions=版本
@@ -3088,7 +3138,14 @@ versions.on=於
versions.view_all=檢視全部
dependency.id=ID
dependency.version=版本
+cargo.registry=在 Cargo 組態檔設定此註冊中心 (例如: ~/.cargo/config.toml
):
+cargo.install=執行下列命令以使用 Cargo 安裝此套件:
+cargo.documentation=關於 Cargo registry 的詳情請參閱說明文件。
+cargo.details.repository_site=儲存庫網站
+cargo.details.documentation_site=文件網站
+chef.registry=在您的 ~/.chef/config.rb
檔設定此註冊中心:
chef.install=執行下列命令安裝此套件:
+chef.documentation=關於 Chef registry 的詳情請參閱說明文件。
composer.registry=在您的 ~/.composer/config.json
檔設定此註冊中心:
composer.install=執行下列命令以使用 Composer 安裝此套件:
composer.documentation=關於 Composer registry 的詳情請參閱說明文件。
@@ -3098,6 +3155,11 @@ conan.details.repository=儲存庫
conan.registry=透過下列命令設定此註冊中心:
conan.install=執行下列命令以使用 Conan 安裝此套件:
conan.documentation=關於 Conan registry 的詳情請參閱說明文件。
+conda.registry=在您的 .condarc
檔設定此註冊中心為 Conda 存儲庫:
+conda.install=執行下列命令以使用 Conda 安裝此套件:
+conda.documentation=關於 Conda registry 的詳情請參閱說明文件。
+conda.details.repository_site=儲存庫網站
+conda.details.documentation_site=文件網站
container.details.type=映像檔類型
container.details.platform=平台
container.pull=透過下列命令拉取映像檔:
@@ -3156,7 +3218,27 @@ settings.delete.description=刪除套件是永久且不可還原的。
settings.delete.notice=您正要刪除 %s (%s),此動作是無法還原的,您確定嗎?
settings.delete.success=已刪除該套件。
settings.delete.error=刪除套件失敗。
+owner.settings.cargo.initialize=初始化索引
+owner.settings.cargo.rebuild=重建索引
+owner.settings.cleanuprules.title=管理清理規則
+owner.settings.cleanuprules.add=加入清理規則
+owner.settings.cleanuprules.edit=編輯清理規則
+owner.settings.cleanuprules.none=沒有可用的清理規則。閱讀文件以了解更多。
+owner.settings.cleanuprules.preview=清理規則預覽
+owner.settings.cleanuprules.preview.none=清理規則不符合任何套件。
owner.settings.cleanuprules.enabled=已啟用
+owner.settings.cleanuprules.pattern_full_match=將比對規則套用到完整的套件名稱
+owner.settings.cleanuprules.keep.title=符合這些規則的版本即使符合下面的刪除規則也會被保留。
+owner.settings.cleanuprules.keep.count=保留最新的
+owner.settings.cleanuprules.keep.count.1=每個套件 1 個版本
+owner.settings.cleanuprules.keep.count.n=每個套件 %d 個版本
+owner.settings.cleanuprules.keep.pattern=保留版本的比對規則
+owner.settings.cleanuprules.keep.pattern.container=Container 套件的最新
版本總是會保留。
+owner.settings.cleanuprules.remove.title=符合這些規則的版本將被移除,除非前述的規則要求保留它們。
+owner.settings.cleanuprules.remove.days=移除早於天數的版本
+owner.settings.cleanuprules.remove.pattern=移除版本的比對規則
+owner.settings.cleanuprules.success.update=已更新清理規則。
+owner.settings.cleanuprules.success.delete=已刪除清理規則。
[secrets]
value=值
@@ -3166,16 +3248,23 @@ name=組織名稱
+runners.status=狀態
runners.id=ID
runners.name=組織名稱
runners.owner_type=認證類型
runners.description=組織描述
runners.labels=標籤
+runners.agent_labels=代理程式標籤
+runners.custom_labels=自訂標籤
runners.task_list.run=執行
+runners.task_list.status=狀態
runners.task_list.repository=儲存庫
runners.task_list.commit=提交
+runners.task_list.done_at=完成於
runners.status.active=啟用
+runs.open_tab=%d 開放中
+runs.closed_tab=%d 已關閉
runs.commit=提交
diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go
index 2d1f3291f8b9c..74969f2cad7c8 100644
--- a/routers/api/v1/repo/notes.go
+++ b/routers/api/v1/repo/notes.go
@@ -58,8 +58,18 @@ func getNote(ctx *context.APIContext, identifier string) {
return
}
+ commitSHA, err := ctx.Repo.GitRepo.ConvertToSHA1(identifier)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound(err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "ConvertToSHA1", err)
+ }
+ return
+ }
+
var note git.Note
- if err := git.GetNote(ctx, ctx.Repo.GitRepo, identifier, ¬e); err != nil {
+ if err := git.GetNote(ctx, ctx.Repo.GitRepo, commitSHA.String(), ¬e); err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(identifier)
return
diff --git a/routers/install/routes.go b/routers/install/routes.go
index a8efc92fe17ca..82d9c34b41f18 100644
--- a/routers/install/routes.go
+++ b/routers/install/routes.go
@@ -64,7 +64,7 @@ func installRecovery(ctx goctx.Context) func(next http.Handler) http.Handler {
"SignedUserName": "",
}
- httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
+ httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {
diff --git a/routers/web/base.go b/routers/web/base.go
index b0d8a7c3f1e6d..2eb0b6f39118a 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/services/auth"
@@ -44,7 +45,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
routing.UpdateFuncInfo(req.Context(), funcInfo)
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
- rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:]
+ rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/"))
u, err := objStore.URL(rPath, path.Base(rPath))
if err != nil {
@@ -80,7 +81,7 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
routing.UpdateFuncInfo(req.Context(), funcInfo)
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
- rPath = path.Clean("/" + strings.ReplaceAll(rPath, "\\", "/"))[1:]
+ rPath = util.CleanPath(strings.ReplaceAll(rPath, "\\", "/"))
if rPath == "" {
http.Error(w, "file not found", http.StatusNotFound)
return
@@ -158,7 +159,7 @@ func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler {
store["SignedUserName"] = ""
}
- httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform")
+ httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform")
w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if !setting.IsProd {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index e5ba4ad2c1398..4f208098e4766 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -726,7 +726,7 @@ func UploadFilePost(ctx *context.Context) {
func cleanUploadFileName(name string) string {
// Rebase the filename
- name = strings.Trim(path.Clean("/"+name), "/")
+ name = strings.Trim(util.CleanPath(name), "/")
// Git disallows any filenames to have a .git directory in them.
for _, part := range strings.Split(name, "/") {
if strings.ToLower(part) == ".git" {
diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go
index 869a69c377219..43f5527986b9b 100644
--- a/routers/web/repo/lfs.go
+++ b/routers/web/repo/lfs.go
@@ -207,7 +207,7 @@ func LFSLockFile(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
return
}
- lockPath = path.Clean("/" + lockPath)[1:]
+ lockPath = util.CleanPath(lockPath)
if len(lockPath) == 0 {
ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 2dba74822e531..7ad65cd51e39c 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -17,7 +17,7 @@ func cacheableRedirect(ctx *context.Context, location string) {
// here we should not use `setting.StaticCacheTime`, it is pretty long (default: 6 hours)
// we must make sure the redirection cache time is short enough, otherwise a user won't see the updated avatar in 6 hours
// it's OK to make the cache time short, it is only a redirection, and doesn't cost much to make a new request
- httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 5*time.Minute)
+ httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 5*time.Minute)
ctx.Redirect(location)
}
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 8b259a362b1eb..ca961524d12c2 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -9,7 +9,6 @@ import (
"fmt"
"io"
"os"
- "path"
"path/filepath"
"strconv"
"strings"
@@ -30,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/uri"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/pull"
"github.com/google/uuid"
@@ -866,7 +866,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
}
// SECURITY: The TreePath must be cleaned!
- comment.TreePath = path.Clean("/" + comment.TreePath)[1:]
+ comment.TreePath = util.CleanPath(comment.TreePath)
var patch string
reader, writer := io.Pipe()
diff --git a/services/packages/container/blob_uploader.go b/services/packages/container/blob_uploader.go
index ba92b0507343a..860672587d2b4 100644
--- a/services/packages/container/blob_uploader.go
+++ b/services/packages/container/blob_uploader.go
@@ -8,13 +8,13 @@ import (
"errors"
"io"
"os"
- "path"
"path/filepath"
"strings"
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"
)
var (
@@ -33,7 +33,7 @@ type BlobUploader struct {
}
func buildFilePath(id string) string {
- return filepath.Join(setting.Packages.ChunkedUploadPath, path.Clean("/" + strings.ReplaceAll(id, "\\", "/"))[1:])
+ return filepath.Join(setting.Packages.ChunkedUploadPath, util.CleanPath(strings.ReplaceAll(id, "\\", "/")))
}
// NewBlobUploader creates a new blob uploader for the given id
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index 2bac4372d378c..7939491aec624 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -7,7 +7,6 @@ import (
"context"
"fmt"
"net/url"
- "path"
"strings"
"time"
@@ -15,6 +14,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
)
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
@@ -129,7 +129,7 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
func CleanUploadFileName(name string) string {
// Rebase the filename
- name = strings.Trim(path.Clean("/"+name), "/")
+ name = strings.Trim(util.CleanPath(name), "/")
// Git disallows any filenames to have a .git directory in them.
for _, part := range strings.Split(name, "/") {
if strings.ToLower(part) == ".git" {
diff --git a/services/repository/push.go b/services/repository/push.go
index 355c2878113fd..cdf030396fcf6 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -80,7 +80,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PushUpdates: %s/%s", optsList[0].RepoUserName, optsList[0].RepoName))
defer finished()
- ctx = cache.WithCacheContext(ctx)
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, optsList[0].RepoUserName, optsList[0].RepoName)
if err != nil {
diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl
index 36913011b0b3d..c46071e839a4a 100644
--- a/templates/repo/issue/view_content/context_menu.tmpl
+++ b/templates/repo/issue/view_content/context_menu.tmpl
@@ -10,17 +10,18 @@
{{else}}
{{$referenceUrl = Printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
{{end}}
- {{.ctxData.locale.Tr "repo.issues.context.copy_link"}}
-
+