Some text with 😄😄 2 emoji next to each other
`) test( "😎🤪🔐🤑❓", - `😎🤪🔐🤑❓
`) + `😎🤪🔐🤑❓
`) // should match nothing test( diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 24f1ab7a01..8417019ddb 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -10,6 +10,7 @@ import ( "regexp" "strings" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/setting" @@ -198,7 +199,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } type prefixedIDs struct { - values map[string]bool + values container.Set[string] } // Generate generates a new element id. @@ -219,14 +220,12 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { if !bytes.HasPrefix(result, []byte("user-content-")) { result = append([]byte("user-content-"), result...) } - if _, ok := p.values[util.BytesToReadOnlyString(result)]; !ok { - p.values[util.BytesToReadOnlyString(result)] = true + if p.values.Add(util.BytesToReadOnlyString(result)) { return result } for i := 1; ; i++ { newResult := fmt.Sprintf("%s-%d", result, i) - if _, ok := p.values[newResult]; !ok { - p.values[newResult] = true + if p.values.Add(newResult) { return []byte(newResult) } } @@ -234,12 +233,12 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { // Put puts a given element id to the used ids table. func (p *prefixedIDs) Put(value []byte) { - p.values[util.BytesToReadOnlyString(value)] = true + p.values.Add(util.BytesToReadOnlyString(value)) } func newPrefixedIDs() *prefixedIDs { return &prefixedIDs{ - values: map[string]bool{}, + values: make(container.Set[string]), } } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index fdbc291c94..49ed3d75d6 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -6,6 +6,7 @@ package markdown_test import ( "context" + "os" "strings" "testing" @@ -37,6 +38,7 @@ func TestMain(m *testing.M) { if err := git.InitSimple(context.Background()); err != nil { log.Fatal("git init failed, err: %v", err) } + os.Exit(m.Run()) } func TestRender_StandardLinks(t *testing.T) { @@ -426,3 +428,51 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, res) } + +func TestMathBlock(t *testing.T) { + const nl = "\n" + testcases := []struct { + testcase string + expected string + }{ + { + "$a$", + `a
a
a
b
a
b
a a$b b
a a$b b
` + nl, + }, + { + `a$b $a a$b b$`, + `a$b a a$b b
a
` + nl,
+ },
+ }
+
+ for _, test := range testcases {
+ res, err := RenderString(&markup.RenderContext{}, test.testcase)
+ assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
+ assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
+
+ }
+}
diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go
index 0339674b6c..8dc88eb858 100644
--- a/modules/markup/markdown/math/inline_parser.go
+++ b/modules/markup/markdown/math/inline_parser.go
@@ -37,7 +37,7 @@ func NewInlineBracketParser() parser.InlineParser {
return defaultInlineBracketParser
}
-// Trigger triggers this parser on $
+// Trigger triggers this parser on $ or \
func (parser *inlineParser) Trigger() []byte {
return parser.start[0:1]
}
@@ -50,29 +50,50 @@ func isAlphanumeric(b byte) bool {
// Parse parses the current line and returns a result of parsing.
func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
- opener := bytes.Index(line, parser.start)
- if opener < 0 {
- return nil
- }
- if opener != 0 && isAlphanumeric(line[opener-1]) {
+
+ if !bytes.HasPrefix(line, parser.start) {
+ // We'll catch this one on the next time round
return nil
}
- opener += len(parser.start)
- ender := bytes.Index(line[opener:], parser.end)
- if ender < 0 {
+ precedingCharacter := block.PrecendingCharacter()
+ if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) {
+ // need to exclude things like `a$` from being considered a start
return nil
}
- if len(line) > opener+ender+len(parser.end) && isAlphanumeric(line[opener+ender+len(parser.end)]) {
- return nil
+
+ // move the opener marker point at the start of the text
+ opener := len(parser.start)
+
+ // Now look for an ending line
+ ender := opener
+ for {
+ pos := bytes.Index(line[ender:], parser.end)
+ if pos < 0 {
+ return nil
+ }
+
+ ender += pos
+
+ // Now we want to check the character at the end of our parser section
+ // that is ender + len(parser.end)
+ pos = ender + len(parser.end)
+ if len(line) <= pos {
+ break
+ }
+ if !isAlphanumeric(line[pos]) {
+ break
+ }
+ // move the pointer onwards
+ ender += len(parser.end)
}
block.Advance(opener)
_, pos := block.Position()
node := NewInline()
- segment := pos.WithStop(pos.Start + ender)
+ segment := pos.WithStop(pos.Start + ender - opener)
node.AppendChild(node, ast.NewRawTextSegment(segment))
- block.Advance(ender + len(parser.end))
+ block.Advance(ender - opener + len(parser.end))
trimBlock(node, block)
return node
diff --git a/modules/markup/markdown/meta.go b/modules/markup/markdown/meta.go
index b08121e868..45d79d537a 100644
--- a/modules/markup/markdown/meta.go
+++ b/modules/markup/markdown/meta.go
@@ -88,7 +88,9 @@ func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) {
line := contents[start:end]
if isYAMLSeparator(line) {
front = contents[frontMatterStart:start]
- body = contents[end+1:]
+ if end+1 < len(contents) {
+ body = contents[end+1:]
+ }
break
}
}
diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go
index 9332b35b42..720d0066f4 100644
--- a/modules/markup/markdown/meta_test.go
+++ b/modules/markup/markdown/meta_test.go
@@ -61,7 +61,7 @@ func TestExtractMetadataBytes(t *testing.T) {
var meta structs.IssueTemplate
body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta)
assert.NoError(t, err)
- assert.Equal(t, bodyTest, body)
+ assert.Equal(t, bodyTest, string(body))
assert.Equal(t, metaTest, meta)
assert.True(t, validateMetadata(meta))
})
@@ -82,7 +82,7 @@ func TestExtractMetadataBytes(t *testing.T) {
var meta structs.IssueTemplate
body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta)
assert.NoError(t, err)
- assert.Equal(t, "", body)
+ assert.Equal(t, "", string(body))
assert.Equal(t, metaTest, meta)
assert.True(t, validateMetadata(meta))
})
diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go
index 003579115f..1ba75dbb68 100644
--- a/modules/markup/markdown/renderconfig.go
+++ b/modules/markup/markdown/renderconfig.go
@@ -5,10 +5,9 @@
package markdown
import (
+ "fmt"
"strings"
- "code.gitea.io/gitea/modules/log"
-
"github.com/yuin/goldmark/ast"
"gopkg.in/yaml.v3"
)
@@ -33,17 +32,13 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
}
rc.yamlNode = value
- type basicRenderConfig struct {
- Gitea *yaml.Node `yaml:"gitea"`
- TOC bool `yaml:"include_toc"`
- Lang string `yaml:"lang"`
+ type commonRenderConfig struct {
+ TOC bool `yaml:"include_toc"`
+ Lang string `yaml:"lang"`
}
-
- var basic basicRenderConfig
-
- err := value.Decode(&basic)
- if err != nil {
- return err
+ var basic commonRenderConfig
+ if err := value.Decode(&basic); err != nil {
+ return fmt.Errorf("unable to decode into commonRenderConfig %w", err)
}
if basic.Lang != "" {
@@ -51,14 +46,48 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
}
rc.TOC = basic.TOC
- if basic.Gitea == nil {
+
+ type controlStringRenderConfig struct {
+ Gitea string `yaml:"gitea"`
+ }
+
+ var stringBasic controlStringRenderConfig
+
+ if err := value.Decode(&stringBasic); err == nil {
+ if stringBasic.Gitea != "" {
+ switch strings.TrimSpace(strings.ToLower(stringBasic.Gitea)) {
+ case "none":
+ rc.Meta = "none"
+ case "table":
+ rc.Meta = "table"
+ default: // "details"
+ rc.Meta = "details"
+ }
+ }
return nil
}
- var control *string
- if err := basic.Gitea.Decode(&control); err == nil && control != nil {
- log.Info("control %v", control)
- switch strings.TrimSpace(strings.ToLower(*control)) {
+ type giteaControl struct {
+ Meta *string `yaml:"meta"`
+ Icon *string `yaml:"details_icon"`
+ TOC *bool `yaml:"include_toc"`
+ Lang *string `yaml:"lang"`
+ }
+
+ type complexGiteaConfig struct {
+ Gitea *giteaControl `yaml:"gitea"`
+ }
+ var complex complexGiteaConfig
+ if err := value.Decode(&complex); err != nil {
+ return fmt.Errorf("unable to decode into complexRenderConfig %w", err)
+ }
+
+ if complex.Gitea == nil {
+ return nil
+ }
+
+ if complex.Gitea.Meta != nil {
+ switch strings.TrimSpace(strings.ToLower(*complex.Gitea.Meta)) {
case "none":
rc.Meta = "none"
case "table":
@@ -66,39 +95,18 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
default: // "details"
rc.Meta = "details"
}
- return nil
}
- type giteaControl struct {
- Meta string `yaml:"meta"`
- Icon string `yaml:"details_icon"`
- TOC *yaml.Node `yaml:"include_toc"`
- Lang string `yaml:"lang"`
+ if complex.Gitea.Icon != nil {
+ rc.Icon = strings.TrimSpace(strings.ToLower(*complex.Gitea.Icon))
}
- var controlStruct *giteaControl
- if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil {
- return err
+ if complex.Gitea.Lang != nil && *complex.Gitea.Lang != "" {
+ rc.Lang = *complex.Gitea.Lang
}
- switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) {
- case "none":
- rc.Meta = "none"
- case "table":
- rc.Meta = "table"
- default: // "details"
- rc.Meta = "details"
- }
-
- rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon))
-
- if controlStruct.Lang != "" {
- rc.Lang = controlStruct.Lang
- }
-
- var toc bool
- if err := controlStruct.TOC.Decode(&toc); err == nil {
- rc.TOC = toc
+ if complex.Gitea.TOC != nil {
+ rc.TOC = *complex.Gitea.TOC
}
return nil
diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go
index 1027035cda..672edbf46d 100644
--- a/modules/markup/markdown/renderconfig_test.go
+++ b/modules/markup/markdown/renderconfig_test.go
@@ -5,6 +5,7 @@
package markdown
import (
+ "strings"
"testing"
"gopkg.in/yaml.v3"
@@ -81,9 +82,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
TOC: true,
Lang: "testlang",
}, `
- include_toc: true
- lang: testlang
-`,
+ include_toc: true
+ lang: testlang
+ `,
},
{
"complexlang", &RenderConfig{
@@ -91,9 +92,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
Icon: "table",
Lang: "testlang",
}, `
- gitea:
- lang: testlang
-`,
+ gitea:
+ lang: testlang
+ `,
},
{
"complexlang2", &RenderConfig{
@@ -140,8 +141,8 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
Icon: "table",
Lang: "",
}
- if err := yaml.Unmarshal([]byte(tt.args), got); err != nil {
- t.Errorf("RenderConfig.UnmarshalYAML() error = %v", err)
+ if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", " ")), got); err != nil {
+ t.Errorf("RenderConfig.UnmarshalYAML() error = %v\n%q", err, tt.args)
return
}
diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go
index 5e5196a70a..4d96a6b0ed 100644
--- a/modules/notification/ui/ui.go
+++ b/modules/notification/ui/ui.go
@@ -10,6 +10,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification/base"
@@ -123,14 +124,14 @@ func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest
log.Error("Unable to load issue: %d for pr: %d: Error: %v", pr.IssueID, pr.ID, err)
return
}
- toNotify := make(map[int64]struct{}, 32)
+ toNotify := make(container.Set[int64], 32)
repoWatchers, err := repo_model.GetRepoWatchersIDs(db.DefaultContext, pr.Issue.RepoID)
if err != nil {
log.Error("GetRepoWatchersIDs: %v", err)
return
}
for _, id := range repoWatchers {
- toNotify[id] = struct{}{}
+ toNotify.Add(id)
}
issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(pr.IssueID)
if err != nil {
@@ -138,11 +139,11 @@ func (ns *notificationService) NotifyNewPullRequest(pr *issues_model.PullRequest
return
}
for _, id := range issueParticipants {
- toNotify[id] = struct{}{}
+ toNotify.Add(id)
}
delete(toNotify, pr.Issue.PosterID)
for _, mention := range mentions {
- toNotify[mention.ID] = struct{}{}
+ toNotify.Add(mention.ID)
}
for receiverID := range toNotify {
_ = ns.issueQueue.Push(issueNotificationOpts{
diff --git a/modules/options/static.go b/modules/options/static.go
index d9a6c83664..4d60879be3 100644
--- a/modules/options/static.go
+++ b/modules/options/static.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The Gitea Authors. All rights reserved.
+// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
@@ -32,12 +32,12 @@ func Dir(name string) ([]string, error) {
customDir := path.Join(setting.CustomPath, "options", name)
isDir, err := util.IsDir(customDir)
if err != nil {
- return []string{}, fmt.Errorf("Failed to check if custom directory %s is a directory. %v", err)
+ 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)
if err != nil {
- return []string{}, fmt.Errorf("Failed to read custom directory. %v", err)
+ return []string{}, fmt.Errorf("unable to read custom directory %q. %w", customDir, err)
}
result = append(result, files...)
@@ -45,11 +45,10 @@ func Dir(name string) ([]string, error) {
files, err := AssetDir(name)
if err != nil {
- return []string{}, fmt.Errorf("Failed to read embedded directory. %v", err)
+ return []string{}, fmt.Errorf("unable to read embedded directory %q. %w", name, err)
}
result = append(result, files...)
-
return directories.AddAndGet(name, result), nil
}
diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go
index 2ed4d26248..16cadefb89 100644
--- a/modules/packages/npm/creator.go
+++ b/modules/packages/npm/creator.go
@@ -66,7 +66,8 @@ type PackageMetadata struct {
License string `json:"license,omitempty"`
}
-// PackageMetadataVersion https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
+// PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
+// PackageMetadataVersion response: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
type PackageMetadataVersion struct {
ID string `json:"_id"`
Name string `json:"name"`
@@ -80,6 +81,7 @@ type PackageMetadataVersion struct {
Dependencies map[string]string `json:"dependencies,omitempty"`
DevDependencies map[string]string `json:"devDependencies,omitempty"`
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
+ Bin map[string]string `json:"bin,omitempty"`
OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"`
Readme string `json:"readme,omitempty"`
Dist PackageDistribution `json:"dist"`
@@ -220,6 +222,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
DevelopmentDependencies: meta.DevDependencies,
PeerDependencies: meta.PeerDependencies,
OptionalDependencies: meta.OptionalDependencies,
+ Bin: meta.Bin,
Readme: meta.Readme,
},
}
diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go
index 64ae6238f3..2b844f4b0e 100644
--- a/modules/packages/npm/creator_test.go
+++ b/modules/packages/npm/creator_test.go
@@ -23,6 +23,7 @@ func TestParsePackage(t *testing.T) {
packageVersion := "1.0.1-pre"
packageTag := "latest"
packageAuthor := "KN4CK3R"
+ packageBin := "gitea"
packageDescription := "Test Description"
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg=="
@@ -236,6 +237,9 @@ func TestParsePackage(t *testing.T) {
Dependencies: map[string]string{
"package": "1.2.0",
},
+ Bin: map[string]string{
+ "bin": packageBin,
+ },
Dist: PackageDistribution{
Integrity: integrity,
},
@@ -264,6 +268,7 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.Equal(t, packageDescription, p.Metadata.Readme)
assert.Equal(t, packageAuthor, p.Metadata.Author)
+ assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
assert.Equal(t, "MIT", p.Metadata.License)
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
assert.Contains(t, p.Metadata.Dependencies, "package")
diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go
index 643a4d344b..44714cd6ea 100644
--- a/modules/packages/npm/metadata.go
+++ b/modules/packages/npm/metadata.go
@@ -20,5 +20,6 @@ type Metadata struct {
DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"`
PeerDependencies map[string]string `json:"peer_dependencies,omitempty"`
OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"`
+ Bin map[string]string `json:"bin,omitempty"`
Readme string `json:"readme,omitempty"`
}
diff --git a/modules/private/internal.go b/modules/private/internal.go
index 2ea516ba80..21e5c9a279 100644
--- a/modules/private/internal.go
+++ b/modules/private/internal.go
@@ -10,6 +10,8 @@ import (
"fmt"
"net"
"net/http"
+ "os"
+ "strings"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
@@ -18,13 +20,14 @@ import (
"code.gitea.io/gitea/modules/setting"
)
-func newRequest(ctx context.Context, url, method string) *httplib.Request {
+func newRequest(ctx context.Context, url, method, sourceIP string) *httplib.Request {
if setting.InternalToken == "" {
log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
}
return httplib.NewRequest(url, method).
SetContext(ctx).
+ Header("X-Real-IP", sourceIP).
Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken))
}
@@ -42,8 +45,16 @@ func decodeJSONError(resp *http.Response) *Response {
return &res
}
+func getClientIP() string {
+ sshConnEnv := strings.TrimSpace(os.Getenv("SSH_CONNECTION"))
+ if len(sshConnEnv) == 0 {
+ return "127.0.0.1"
+ }
+ return strings.Fields(sshConnEnv)[0]
+}
+
func newInternalRequest(ctx context.Context, url, method string) *httplib.Request {
- req := newRequest(ctx, url, method).SetTLSClientConfig(&tls.Config{
+ req := newRequest(ctx, url, method, getClientIP()).SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: true,
ServerName: setting.Domain,
})
diff --git a/modules/public/public.go b/modules/public/public.go
index 7804e945e7..ac1d80c860 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -11,6 +11,7 @@ import (
"path/filepath"
"strings"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -83,11 +84,11 @@ func AssetsHandlerFunc(opts *Options) http.HandlerFunc {
}
// parseAcceptEncoding parse Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5 as compress methods
-func parseAcceptEncoding(val string) map[string]bool {
+func parseAcceptEncoding(val string) container.Set[string] {
parts := strings.Split(val, ";")
- types := make(map[string]bool)
+ types := make(container.Set[string])
for _, v := range strings.Split(parts[0], ",") {
- types[strings.TrimSpace(v)] = true
+ types.Add(strings.TrimSpace(v))
}
return types
}
diff --git a/modules/public/public_test.go b/modules/public/public_test.go
index 430e734564..8b58d6af33 100644
--- a/modules/public/public_test.go
+++ b/modules/public/public_test.go
@@ -7,28 +7,23 @@ package public
import (
"testing"
+ "code.gitea.io/gitea/modules/container"
+
"github.com/stretchr/testify/assert"
)
func TestParseAcceptEncoding(t *testing.T) {
kases := []struct {
Header string
- Expected map[string]bool
+ Expected container.Set[string]
}{
{
- Header: "deflate, gzip;q=1.0, *;q=0.5",
- Expected: map[string]bool{
- "deflate": true,
- "gzip": true,
- },
+ Header: "deflate, gzip;q=1.0, *;q=0.5",
+ Expected: container.SetOf("deflate", "gzip"),
},
{
- Header: " gzip, deflate, br",
- Expected: map[string]bool{
- "deflate": true,
- "gzip": true,
- "br": true,
- },
+ Header: " gzip, deflate, br",
+ Expected: container.SetOf("deflate", "gzip", "br"),
},
}
diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go
index 9666880adf..10120bf85d 100644
--- a/modules/public/serve_static.go
+++ b/modules/public/serve_static.go
@@ -60,7 +60,7 @@ func AssetIsDir(name string) (bool, error) {
// serveContent serve http content
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
- if encodings["gzip"] {
+ if encodings.Contains("gzip") {
if cf, ok := fi.(*vfsgen۰CompressedFileInfo); ok {
rdGzip := bytes.NewReader(cf.GzipBytes())
// all static files are managed by Gitea, so we can make sure every file has the correct ext name
diff --git a/modules/queue/unique_queue_channel.go b/modules/queue/unique_queue_channel.go
index 6e8d37a20c..d1bf7239eb 100644
--- a/modules/queue/unique_queue_channel.go
+++ b/modules/queue/unique_queue_channel.go
@@ -12,6 +12,7 @@ import (
"sync/atomic"
"time"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
)
@@ -33,7 +34,7 @@ type ChannelUniqueQueueConfiguration ChannelQueueConfiguration
type ChannelUniqueQueue struct {
*WorkerPool
lock sync.Mutex
- table map[string]bool
+ table container.Set[string]
shutdownCtx context.Context
shutdownCtxCancel context.CancelFunc
terminateCtx context.Context
@@ -58,7 +59,7 @@ func NewChannelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue
shutdownCtx, shutdownCtxCancel := context.WithCancel(terminateCtx)
queue := &ChannelUniqueQueue{
- table: map[string]bool{},
+ table: make(container.Set[string]),
shutdownCtx: shutdownCtx,
shutdownCtxCancel: shutdownCtxCancel,
terminateCtx: terminateCtx,
@@ -73,7 +74,7 @@ func NewChannelUniqueQueue(handle HandlerFunc, cfg, exemplar interface{}) (Queue
bs, _ := json.Marshal(datum)
queue.lock.Lock()
- delete(queue.table, string(bs))
+ queue.table.Remove(string(bs))
queue.lock.Unlock()
if u := handle(datum); u != nil {
@@ -127,16 +128,15 @@ func (q *ChannelUniqueQueue) PushFunc(data Data, fn func() error) error {
q.lock.Unlock()
}
}()
- if _, ok := q.table[string(bs)]; ok {
+ if !q.table.Add(string(bs)) {
return ErrAlreadyInQueue
}
// FIXME: We probably need to implement some sort of limit here
// If the downstream queue blocks this table will grow without limit
- q.table[string(bs)] = true
if fn != nil {
err := fn()
if err != nil {
- delete(q.table, string(bs))
+ q.table.Remove(string(bs))
return err
}
}
@@ -155,8 +155,7 @@ func (q *ChannelUniqueQueue) Has(data Data) (bool, error) {
q.lock.Lock()
defer q.lock.Unlock()
- _, has := q.table[string(bs)]
- return has, nil
+ return q.table.Contains(string(bs)), nil
}
// Flush flushes the channel with a timeout - the Flush worker will be registered as a flush worker with the manager
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index 48c3edf60f..b01be322d2 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -275,7 +276,7 @@ func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository)
return pullMirrorReleaseSync(repo, gitRepo)
}
- existingRelTags := make(map[string]struct{})
+ existingRelTags := make(container.Set[string])
opts := repo_model.FindReleasesOptions{
IncludeDrafts: true,
IncludeTags: true,
@@ -303,14 +304,14 @@ func SyncReleasesWithTags(repo *repo_model.Repository, gitRepo *git.Repository)
return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
}
} else {
- existingRelTags[strings.ToLower(rel.TagName)] = struct{}{}
+ existingRelTags.Add(strings.ToLower(rel.TagName))
}
}
}
_, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error {
tagName := strings.TrimPrefix(refname, git.TagPrefix)
- if _, ok := existingRelTags[strings.ToLower(tagName)]; ok {
+ if existingRelTags.Contains(strings.ToLower(tagName)) {
return nil
}
diff --git a/modules/setting/queue.go b/modules/setting/queue.go
index cb86cbdfe0..d3bb33b248 100644
--- a/modules/setting/queue.go
+++ b/modules/setting/queue.go
@@ -9,6 +9,7 @@ import (
"strconv"
"time"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
ini "gopkg.in/ini.v1"
@@ -109,8 +110,8 @@ func NewQueueService() {
// Now handle the old issue_indexer configuration
// FIXME: DEPRECATED to be removed in v1.18.0
section := Cfg.Section("queue.issue_indexer")
- directlySet := toDirectlySetKeysMap(section)
- if !directlySet["TYPE"] && defaultType == "" {
+ directlySet := toDirectlySetKeysSet(section)
+ if !directlySet.Contains("TYPE") && defaultType == "" {
switch typ := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_TYPE").MustString(""); typ {
case "levelqueue":
_, _ = section.NewKey("TYPE", "level")
@@ -124,25 +125,25 @@ func NewQueueService() {
log.Fatal("Unsupported indexer queue type: %v", typ)
}
}
- if !directlySet["LENGTH"] {
+ if !directlySet.Contains("LENGTH") {
length := Cfg.Section("indexer").Key("UPDATE_BUFFER_LEN").MustInt(0)
if length != 0 {
_, _ = section.NewKey("LENGTH", strconv.Itoa(length))
}
}
- if !directlySet["BATCH_LENGTH"] {
+ if !directlySet.Contains("BATCH_LENGTH") {
fallback := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(0)
if fallback != 0 {
_, _ = section.NewKey("BATCH_LENGTH", strconv.Itoa(fallback))
}
}
- if !directlySet["DATADIR"] {
+ if !directlySet.Contains("DATADIR") {
queueDir := filepath.ToSlash(Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_DIR").MustString(""))
if queueDir != "" {
_, _ = section.NewKey("DATADIR", queueDir)
}
}
- if !directlySet["CONN_STR"] {
+ if !directlySet.Contains("CONN_STR") {
connStr := Cfg.Section("indexer").Key("ISSUE_INDEXER_QUEUE_CONN_STR").MustString("")
if connStr != "" {
_, _ = section.NewKey("CONN_STR", connStr)
@@ -178,19 +179,19 @@ func handleOldLengthConfiguration(queueName, oldSection, oldKey string, defaultV
}
section := Cfg.Section("queue." + queueName)
- directlySet := toDirectlySetKeysMap(section)
- if !directlySet["LENGTH"] {
+ directlySet := toDirectlySetKeysSet(section)
+ if !directlySet.Contains("LENGTH") {
_, _ = section.NewKey("LENGTH", strconv.Itoa(value))
}
}
-// toDirectlySetKeysMap returns a bool map of keys directly set by this section
+// toDirectlySetKeysSet returns a set of keys directly set by this section
// Note: we cannot use section.HasKey(...) as that will immediately set the Key if a parent section has the Key
// but this section does not.
-func toDirectlySetKeysMap(section *ini.Section) map[string]bool {
- sectionMap := map[string]bool{}
+func toDirectlySetKeysSet(section *ini.Section) container.Set[string] {
+ sections := make(container.Set[string])
for _, key := range section.Keys() {
- sectionMap[key.Name()] = true
+ sections.Add(key.Name())
}
- return sectionMap
+ return sections
}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 6233437bf5..007e3ef61f 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -21,6 +21,7 @@ import (
"text/template"
"time"
+ "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/user"
@@ -234,7 +235,7 @@ var (
DefaultTheme string
Themes []string
Reactions []string
- ReactionsMap map[string]bool `ini:"-"`
+ ReactionsLookup container.Set[string] `ini:"-"`
CustomEmojis []string
CustomEmojisMap map[string]string `ini:"-"`
SearchRepoDescription bool
@@ -1100,9 +1101,9 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
newMarkup()
- UI.ReactionsMap = make(map[string]bool)
+ UI.ReactionsLookup = make(container.Set[string])
for _, reaction := range UI.Reactions {
- UI.ReactionsMap[reaction] = true
+ UI.ReactionsLookup.Add(reaction)
}
UI.CustomEmojisMap = make(map[string]string)
for _, emoji := range UI.CustomEmojis {
diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go
index 53e3fcf62d..10bd9e62ce 100644
--- a/modules/structs/org_team.go
+++ b/modules/structs/org_team.go
@@ -17,7 +17,7 @@ type Team struct {
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
- // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"]
+ // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"}
UnitsMap map[string]string `json:"units_map"`
CanCreateOrgRepo bool `json:"can_create_org_repo"`
}
@@ -33,7 +33,7 @@ type CreateTeamOption struct {
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
- // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"]
+ // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"}
UnitsMap map[string]string `json:"units_map"`
CanCreateOrgRepo bool `json:"can_create_org_repo"`
}
@@ -49,7 +49,7 @@ type EditTeamOption struct {
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
- // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"]
+ // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"}
UnitsMap map[string]string `json:"units_map"`
CanCreateOrgRepo *bool `json:"can_create_org_repo"`
}
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 403e8e6f4c..6832c76e1a 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -34,8 +34,10 @@ type ExternalTracker struct {
ExternalTrackerURL string `json:"external_tracker_url"`
// External Issue Tracker URL Format. Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index.
ExternalTrackerFormat string `json:"external_tracker_format"`
- // External Issue Tracker Number Format, either `numeric` or `alphanumeric`
+ // External Issue Tracker Number Format, either `numeric`, `alphanumeric`, or `regexp`
ExternalTrackerStyle string `json:"external_tracker_style"`
+ // External Issue Tracker issue regular expression
+ ExternalTrackerRegexpPattern string `json:"external_tracker_regexp_pattern"`
}
// ExternalWiki represents setting for external wiki
diff --git a/modules/sync/status_pool.go b/modules/sync/status_pool.go
index acbd93ab17..99e5ce9cb3 100644
--- a/modules/sync/status_pool.go
+++ b/modules/sync/status_pool.go
@@ -6,6 +6,8 @@ package sync
import (
"sync"
+
+ "code.gitea.io/gitea/modules/container"
)
// StatusTable is a table maintains true/false values.
@@ -14,13 +16,13 @@ import (
// in different goroutines.
type StatusTable struct {
lock sync.RWMutex
- pool map[string]struct{}
+ pool container.Set[string]
}
// NewStatusTable initializes and returns a new StatusTable object.
func NewStatusTable() *StatusTable {
return &StatusTable{
- pool: make(map[string]struct{}),
+ pool: make(container.Set[string]),
}
}
@@ -28,32 +30,29 @@ func NewStatusTable() *StatusTable {
// Returns whether set value was set to true
func (p *StatusTable) StartIfNotRunning(name string) bool {
p.lock.Lock()
- _, ok := p.pool[name]
- if !ok {
- p.pool[name] = struct{}{}
- }
+ added := p.pool.Add(name)
p.lock.Unlock()
- return !ok
+ return added
}
// Start sets value of given name to true in the pool.
func (p *StatusTable) Start(name string) {
p.lock.Lock()
- p.pool[name] = struct{}{}
+ p.pool.Add(name)
p.lock.Unlock()
}
// Stop sets value of given name to false in the pool.
func (p *StatusTable) Stop(name string) {
p.lock.Lock()
- delete(p.pool, name)
+ p.pool.Remove(name)
p.lock.Unlock()
}
// IsRunning checks if value of given name is set to true in the pool.
func (p *StatusTable) IsRunning(name string) bool {
p.lock.RLock()
- _, ok := p.pool[name]
+ exists := p.pool.Contains(name)
p.lock.RUnlock()
- return ok
+ return exists
}
diff --git a/modules/templates/dynamic.go b/modules/templates/dynamic.go
index 4896580f62..a86e71a8c8 100644
--- a/modules/templates/dynamic.go
+++ b/modules/templates/dynamic.go
@@ -33,6 +33,21 @@ func GetAsset(name string) ([]byte, error) {
return os.ReadFile(filepath.Join(setting.StaticRootPath, name))
}
+// GetAssetFilename returns the filename of the provided asset
+func GetAssetFilename(name string) (string, error) {
+ filename := filepath.Join(setting.CustomPath, name)
+ _, err := os.Stat(filename)
+ if err != nil && !os.IsNotExist(err) {
+ return filename, err
+ } else if err == nil {
+ return filename, nil
+ }
+
+ filename = filepath.Join(setting.StaticRootPath, name)
+ _, err = os.Stat(filename)
+ return filename, err
+}
+
// walkTemplateFiles calls a callback for each template asset
func walkTemplateFiles(callback func(path, name string, d fs.DirEntry, err error) error) error {
if err := walkAssetDir(filepath.Join(setting.CustomPath, "templates"), true, callback); err != nil && !os.IsNotExist(err) {
diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go
index 210bb5e73c..81ea660161 100644
--- a/modules/templates/htmlrenderer.go
+++ b/modules/templates/htmlrenderer.go
@@ -5,7 +5,12 @@
package templates
import (
+ "bytes"
"context"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -14,7 +19,14 @@ import (
"github.com/unrolled/render"
)
-var rendererKey interface{} = "templatesHtmlRendereer"
+var (
+ rendererKey interface{} = "templatesHtmlRenderer"
+
+ templateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
+ notDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
+ unexpectedError = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
+ expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
+)
// HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
@@ -32,6 +44,25 @@ func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
}
log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
+ compilingTemplates := true
+ defer func() {
+ if !compilingTemplates {
+ return
+ }
+
+ panicked := recover()
+ if panicked == nil {
+ return
+ }
+
+ // OK try to handle the panic...
+ err, ok := panicked.(error)
+ if ok {
+ handlePanicError(err)
+ }
+ log.Fatal("PANIC: Unable to compile templates!\n%v\n\nStacktrace:\n%s", panicked, log.Stack(2))
+ }()
+
renderer := render.New(render.Options{
Extensions: []string{".tmpl"},
Directory: "templates",
@@ -42,6 +73,7 @@ func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
IsDevelopment: false,
DisableHTTPErrorRendering: true,
})
+ compilingTemplates = false
if !setting.IsProd {
watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
PathsCallback: walkTemplateFiles,
@@ -50,3 +82,168 @@ func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) {
}
return context.WithValue(ctx, rendererKey, renderer), renderer
}
+
+func handlePanicError(err error) {
+ wrapFatal(handleNotDefinedPanicError(err))
+ wrapFatal(handleUnexpected(err))
+ wrapFatal(handleExpectedEnd(err))
+ wrapFatal(handleGenericTemplateError(err))
+}
+
+func wrapFatal(format string, args []interface{}) {
+ if format == "" {
+ return
+ }
+ log.FatalWithSkip(1, format, args...)
+}
+
+func handleGenericTemplateError(err error) (string, []interface{}) {
+ groups := templateError.FindStringSubmatch(err.Error())
+ if len(groups) != 4 {
+ return "", nil
+ }
+
+ templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
+
+ filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
+ if assetErr != nil {
+ return "", nil
+ }
+
+ lineNumber, _ := strconv.Atoi(lineNumberStr)
+
+ line := getLineFromAsset(templateName, lineNumber, "")
+
+ return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
+}
+
+func handleNotDefinedPanicError(err error) (string, []interface{}) {
+ groups := notDefinedError.FindStringSubmatch(err.Error())
+ if len(groups) != 4 {
+ return "", nil
+ }
+
+ templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
+
+ functionName, _ = strconv.Unquote(`"` + functionName + `"`)
+
+ filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
+ if assetErr != nil {
+ return "", nil
+ }
+
+ lineNumber, _ := strconv.Atoi(lineNumberStr)
+
+ line := getLineFromAsset(templateName, lineNumber, functionName)
+
+ return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
+}
+
+func handleUnexpected(err error) (string, []interface{}) {
+ groups := unexpectedError.FindStringSubmatch(err.Error())
+ if len(groups) != 4 {
+ return "", nil
+ }
+
+ templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
+ unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
+
+ filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
+ if assetErr != nil {
+ return "", nil
+ }
+
+ lineNumber, _ := strconv.Atoi(lineNumberStr)
+
+ line := getLineFromAsset(templateName, lineNumber, unexpected)
+
+ return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
+}
+
+func handleExpectedEnd(err error) (string, []interface{}) {
+ groups := expectedEndError.FindStringSubmatch(err.Error())
+ if len(groups) != 4 {
+ return "", nil
+ }
+
+ templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
+
+ filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
+ if assetErr != nil {
+ return "", nil
+ }
+
+ lineNumber, _ := strconv.Atoi(lineNumberStr)
+
+ line := getLineFromAsset(templateName, lineNumber, unexpected)
+
+ return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
+}
+
+const dashSeparator = "----------------------------------------------------------------------\n"
+
+func getLineFromAsset(templateName string, targetLineNum int, target string) string {
+ bs, err := GetAsset("templates/" + templateName + ".tmpl")
+ if err != nil {
+ return fmt.Sprintf("(unable to read template file: %v)", err)
+ }
+
+ sb := &strings.Builder{}
+
+ // Write the header
+ sb.WriteString(dashSeparator)
+
+ var lineBs []byte
+
+ // Iterate through the lines from the asset file to find the target line
+ for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
+ // Find the next new line
+ end := bytes.IndexByte(bs[start:], '\n')
+
+ // adjust the end to be a direct pointer in to []byte
+ if end < 0 {
+ end = len(bs)
+ } else {
+ end += start
+ }
+
+ // set lineBs to the current line []byte
+ lineBs = bs[start:end]
+
+ // move start to after the current new line position
+ start = end + 1
+
+ // Write 2 preceding lines + the target line
+ if targetLineNum-currentLineNum < 3 {
+ _, _ = sb.Write(lineBs)
+ _ = sb.WriteByte('\n')
+ }
+ }
+
+ // If there is a provided target to look for in the line add a pointer to it
+ // e.g. ^^^^^^^
+ if target != "" {
+ idx := bytes.Index(lineBs, []byte(target))
+
+ if idx >= 0 {
+ // take the current line and replace preceding text with whitespace (except for tab)
+ for i := range lineBs[:idx] {
+ if lineBs[i] != '\t' {
+ lineBs[i] = ' '
+ }
+ }
+
+ // write the preceding "space"
+ _, _ = sb.Write(lineBs[:idx])
+
+ // Now write the ^^ pointer
+ _, _ = sb.WriteString(strings.Repeat("^", len(target)))
+ _ = sb.WriteByte('\n')
+ }
+ }
+
+ // Finally write the footer
+ sb.WriteString(dashSeparator)
+
+ return sb.String()
+}
diff --git a/modules/templates/static.go b/modules/templates/static.go
index 3265bd9cfc..7f7cbe702f 100644
--- a/modules/templates/static.go
+++ b/modules/templates/static.go
@@ -31,6 +31,18 @@ func GlobalModTime(filename string) time.Time {
return timeutil.GetExecutableModTime()
}
+// GetAssetFilename returns the filename of the provided asset
+func GetAssetFilename(name string) (string, error) {
+ filename := filepath.Join(setting.CustomPath, name)
+ _, err := os.Stat(filename)
+ if err != nil && !os.IsNotExist(err) {
+ return name, err
+ } else if err == nil {
+ return filename, nil
+ }
+ return "(builtin) " + name, nil
+}
+
// GetAsset get a special asset, only for chi
func GetAsset(name string) ([]byte, error) {
bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
diff --git a/options/locale/TRANSLATORS b/options/locale/TRANSLATORS
index 3884207f0a..e67255f2fb 100644
--- a/options/locale/TRANSLATORS
+++ b/options/locale/TRANSLATORS
@@ -72,7 +72,7 @@ Thomas Fanninger + {{range .LineNumbers}} + {{.}} + {{end}} + | +{{.FormattedLines | Safe}} |
+
- {{range .LineNumbers}} - {{.}} - {{end}} - | -{{.FormattedLines | Safe}} |
-