diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index a713fe0d8b85..6bcf740c238b 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 ffef1e4948ee..1295692cec85 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 503b8c9ddfa4..ef0b7c1a941e 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 dd8facb7a313..32ba8744d452 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 c6081f07d06b..ce54defacdc6 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 25480f3f9654..178fa72f09bf 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 f741a8744538..62bbf5dcba84 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 77e3ecad2ca7..5315547865e1 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 3f938948aed6..f7a338469124 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 0c8d7411ed5d..50c34edae2e5 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 f0caa30eb82b..46e0152ef455 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 768c72a61b01..55bf570de6b9 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 039e934b3a4d..e83e8df5d094 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 a20253676e67..8c954492ae51 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 1480f7808176..000000000000 --- 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 ff3c86d3f84f..549f4e25b11a 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 42026f9b1054..e1d60d89eb9f 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 49c8d2a904d1..f9a33cd4f68c 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 a6a9d54a8ca3..05bf1fb28a56 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 c427d8d7e312..24da14b63463 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 5b215496b80c..7887fd42b72e 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 2bafdffd141c..6c6da34aa006 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 74acb7a85fd8..5aa9e15f5c3e 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 93f4f67cf648..2f020f924dd2 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 edf7376e7cc4..4f2e0ebb13c3 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 2d1f3291f8b9..74969f2cad7c 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 a8efc92fe17c..82d9c34b41f1 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 b0d8a7c3f1e6..2eb0b6f39118 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 e5ba4ad2c139..4f208098e476 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 869a69c37721..43f5527986b9 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 2dba74822e53..7ad65cd51e39 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 8b259a362b1e..ca961524d12c 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 ba92b0507343..860672587d2b 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 2bac4372d378..7939491aec62 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 355c2878113f..cdf030396fcf 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 36913011b0b3..c46071e839a4 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"}} - {{.ctxData.locale.Tr "repo.issues.context.quote_reply"}} +
{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}
+
{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}
{{if not .ctxData.UnitIssuesGlobalDisabled}} - {{.ctxData.locale.Tr "repo.issues.context.reference_issue"}} +
{{.ctxData.locale.Tr "repo.issues.context.reference_issue"}}
{{end}} {{if or .ctxData.Permission.IsAdmin .IsCommentPoster .ctxData.HasIssuesOrPullsWritePermission}}
- {{.ctxData.locale.Tr "repo.issues.context.edit"}} +
{{.ctxData.locale.Tr "repo.issues.context.edit"}}
{{if .delete}} + - + {{end}} {{end}} diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl index 719d5b06b9f5..ab6f1bc5841b 100644 --- a/templates/user/dashboard/navbar.tmpl +++ b/templates/user/dashboard/navbar.tmpl @@ -5,12 +5,10 @@ {{avatar $.Context .ContextUser}} {{.ContextUser.ShortName 40}} - {{if .ContextUser.IsOrganization}} - - {{if .ContextUser.Visibility.IsLimited}}
{{.locale.Tr "org.settings.visibility.limited_shortname"}}
{{end}} - {{if .ContextUser.Visibility.IsPrivate}}
{{.locale.Tr "org.settings.visibility.private_shortname"}}
{{end}} -
- {{end}} + + {{if .ContextUser.Visibility.IsLimited}}
{{.locale.Tr "org.settings.visibility.limited_shortname"}}
{{end}} + {{if .ContextUser.Visibility.IsPrivate}}
{{.locale.Tr "org.settings.visibility.private_shortname"}}
{{end}} +
{{svg "octicon-triangle-down" 14 "dropdown icon"}}