From 1d3240887c519a04c13bcd7e852c6d6ad1cb00b5 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Fri, 15 Mar 2024 13:44:42 +0100 Subject: [PATCH 001/565] Render inline file permalinks --- modules/markup/html.go | 267 +++++++++++++++++++++ modules/markup/html_test.go | 57 +++++ modules/markup/renderer.go | 4 + modules/markup/sanitizer.go | 17 ++ services/markup/processorhelper.go | 81 +++++++ web_src/css/index.css | 1 + web_src/css/markup/content.css | 3 +- web_src/css/markup/filepreview.css | 35 +++ web_src/css/repo/linebutton.css | 3 +- web_src/js/features/repo-unicode-escape.js | 4 +- 10 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 web_src/css/markup/filepreview.css diff --git a/modules/markup/html.go b/modules/markup/html.go index b7291823b5..2501f8062d 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -10,10 +10,12 @@ import ( "path" "path/filepath" "regexp" + "strconv" "strings" "sync" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -61,6 +63,9 @@ var ( validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) + // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) + // While this email regex is definitely not perfect and I'm sure you can come up // with edge cases, it is still accepted by the CommonMark specification, as // well as the HTML5 spec: @@ -171,6 +176,7 @@ type processor func(ctx *RenderContext, node *html.Node) var defaultProcessors = []processor{ fullIssuePatternProcessor, comparePatternProcessor, + filePreviewPatternProcessor, fullHashPatternProcessor, shortLinkProcessor, linkProcessor, @@ -1054,6 +1060,267 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } } +func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { + if ctx.Metas == nil { + return + } + if DefaultProcessorHelper.GetRepoFileContent == nil || DefaultProcessorHelper.GetLocale == nil { + return + } + + next := node.NextSibling + for node != nil && node != next { + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) + if m == nil { + return + } + + // Ensure that every group (m[0]...m[9]) has a match + for i := 0; i < 10; i++ { + if m[i] == -1 { + return + } + } + + urlFull := node.Data[m[0]:m[1]] + + // Ensure that we only use links to local repositories + if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) { + return + } + + projPath := node.Data[m[2]:m[3]] + projPath = strings.TrimSuffix(projPath, "/") + + commitSha := node.Data[m[4]:m[5]] + filePath := node.Data[m[6]:m[7]] + hash := node.Data[m[8]:m[9]] + + start := m[0] + end := m[1] + + // If url ends in '.', it's very likely that it is not part of the + // actual url but used to finish a sentence. + if strings.HasSuffix(urlFull, ".") { + end-- + urlFull = urlFull[:len(urlFull)-1] + hash = hash[:len(hash)-1] + } + + projPathSegments := strings.Split(projPath, "/") + fileContent, err := DefaultProcessorHelper.GetRepoFileContent( + ctx.Ctx, + projPathSegments[len(projPathSegments)-2], + projPathSegments[len(projPathSegments)-1], + commitSha, filePath, + ) + if err != nil { + return + } + + lineSpecs := strings.Split(hash, "-") + lineCount := len(fileContent) + + var subTitle string + var lineOffset int + + if len(lineSpecs) == 1 { + line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + if line < 1 || line > lineCount { + return + } + + fileContent = fileContent[line-1 : line] + subTitle = "Line " + strconv.Itoa(line) + + lineOffset = line - 1 + } else { + startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) + + if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { + return + } + + fileContent = fileContent[startLine-1 : endLine] + subTitle = "Lines " + strconv.Itoa(startLine) + " to " + strconv.Itoa(endLine) + + lineOffset = startLine - 1 + } + + table := &html.Node{ + Type: html.ElementNode, + Data: atom.Table.String(), + Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, + } + tbody := &html.Node{ + Type: html.ElementNode, + Data: atom.Tbody.String(), + } + + locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx) + if err != nil { + log.Error("Unable to get locale. Error: %v", err) + return + } + + status := &charset.EscapeStatus{} + statuses := make([]*charset.EscapeStatus, len(fileContent)) + for i, line := range fileContent { + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) + status = status.Or(statuses[i]) + } + + for idx, code := range fileContent { + tr := &html.Node{ + Type: html.ElementNode, + Data: atom.Tr.String(), + } + + lineNum := strconv.Itoa(lineOffset + idx + 1) + + tdLinesnum := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "id", Val: "L" + lineNum}, + {Key: "class", Val: "lines-num"}, + }, + } + spanLinesNum := &html.Node{ + Type: html.ElementNode, + Data: atom.Span.String(), + Attr: []html.Attribute{ + {Key: "id", Val: "L" + lineNum}, + {Key: "data-line-number", Val: lineNum}, + }, + } + tdLinesnum.AppendChild(spanLinesNum) + tr.AppendChild(tdLinesnum) + + if status.Escaped { + tdLinesEscape := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "class", Val: "lines-escape"}, + }, + } + + if statuses[idx].Escaped { + btnTitle := "" + if statuses[idx].HasInvisible { + btnTitle += locale.TrString("repo.invisible_runes_line") + " " + } + if statuses[idx].HasAmbiguous { + btnTitle += locale.TrString("repo.ambiguous_runes_line") + } + + escapeBtn := &html.Node{ + Type: html.ElementNode, + Data: atom.Button.String(), + Attr: []html.Attribute{ + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, + {Key: "title", Val: btnTitle}, + }, + } + tdLinesEscape.AppendChild(escapeBtn) + } + + tr.AppendChild(tdLinesEscape) + } + + tdCode := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "rel", Val: "L" + lineNum}, + {Key: "class", Val: "lines-code chroma"}, + }, + } + codeInner := &html.Node{ + Type: html.ElementNode, + Data: atom.Code.String(), + Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, + } + codeText := &html.Node{ + Type: html.RawNode, + Data: string(code), + } + codeInner.AppendChild(codeText) + tdCode.AppendChild(codeInner) + tr.AppendChild(tdCode) + + tbody.AppendChild(tr) + } + + table.AppendChild(tbody) + + twrapper := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, + } + twrapper.AppendChild(table) + + header := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "header"}}, + } + afilepath := &html.Node{ + Type: html.ElementNode, + Data: atom.A.String(), + Attr: []html.Attribute{ + {Key: "href", Val: urlFull}, + {Key: "class", Val: "muted"}, + }, + } + afilepath.AppendChild(&html.Node{ + Type: html.TextNode, + Data: filePath, + }) + header.AppendChild(afilepath) + + psubtitle := &html.Node{ + Type: html.ElementNode, + Data: atom.Span.String(), + Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, + } + psubtitle.AppendChild(&html.Node{ + Type: html.TextNode, + Data: subTitle + " in ", + }) + psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "text black")) + header.AppendChild(psubtitle) + + preview := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, + } + preview.AppendChild(header) + preview.AppendChild(twrapper) + + // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div + before := node.Data[:start] + after := node.Data[end:] + node.Data = before + nextSibling := node.NextSibling + node.Parent.InsertBefore(&html.Node{ + Type: html.RawNode, + Data: "

", + }, nextSibling) + node.Parent.InsertBefore(preview, nextSibling) + node.Parent.InsertBefore(&html.Node{ + Type: html.RawNode, + Data: "

" + after, + }, nextSibling) + + node = node.NextSibling + } +} + // emojiShortCodeProcessor for rendering text like :smile: into emoji func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { start := 0 diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 132955c019..652db13e5e 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -5,6 +5,7 @@ package markup_test import ( "context" + "html/template" "io" "os" "strings" @@ -13,10 +14,12 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -673,3 +676,57 @@ func TestIssue18471(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "783b039...da951ce", res.String()) } + +func TestRender_FilePreview(t *testing.T) { + setting.AppURL = markup.TestAppURL + markup.Init(&markup.ProcessorHelper{ + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { + buf := []byte("A\nB\nC\nD\n") + return highlight.PlainText(buf), nil + }, + GetLocale: func(ctx context.Context) (translation.Locale, error) { + return translation.NewLocale("en-US"), nil + }, + }) + + sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" + commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L1-L2" + + test := func(input, expected string) { + buffer, err := markup.RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + RelativePath: ".md", + Metas: localMetas, + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + test( + commitFilePreview, + `

`+ + `
`+ + `
`+ + `path/to/file.go`+ + ``+ + `Lines 1 to 2 in b6dd621`+ + ``+ + `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
A`+"\n"+`
B`+"\n"+`
`+ + `
`+ + `
`+ + `

`, + ) +} diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 5a7adcc553..37d3fde58c 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "html/template" "io" "net/url" "path/filepath" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" @@ -31,6 +33,8 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool + GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) + GetLocale func(ctx context.Context) (translation.Locale, error) ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index ffc33c3b8e..73e17060a7 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -120,6 +120,23 @@ func createDefaultPolicy() *bluemonday.Policy { // Allow 'color' and 'background-color' properties for the style attribute on text elements. policy.AllowStyles("color", "background-color").OnElements("span", "p") + // Allow classes for file preview links... + policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") + policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") + policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") + policy.AllowAttrs("title").OnElements("button") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") + policy.AllowAttrs("data-tooltip-content").OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") + // Allow generally safe attributes generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index a4378678a0..134b1b5152 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -5,10 +5,21 @@ package markup import ( "context" + "fmt" + "html/template" + "io" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/translation" gitea_context "code.gitea.io/gitea/services/context" + file_service "code.gitea.io/gitea/services/repository/files" ) func ProcessorHelper() *markup.ProcessorHelper { @@ -29,5 +40,75 @@ func ProcessorHelper() *markup.ProcessorHelper { // when using gitea context (web context), use user's visibility and user's permission to check return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) }, + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { + repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) + if err != nil { + return nil, err + } + + var user *user.User + + giteaCtx, ok := ctx.(*gitea_context.Context) + if ok { + user = giteaCtx.Doer + } + + perms, err := access.GetUserRepoPermission(ctx, repo, user) + if err != nil { + return nil, err + } + if !perms.CanRead(unit.TypeCode) { + return nil, fmt.Errorf("cannot access repository code") + } + + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + return nil, err + } + + commit, err := gitRepo.GetCommit(commitSha) + if err != nil { + return nil, err + } + + language, err := file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) + } + + blob, err := commit.GetBlobByPath(filePath) + if err != nil { + return nil, err + } + + dataRc, err := blob.DataAsync() + if err != nil { + return nil, err + } + defer dataRc.Close() + + buf, _ := io.ReadAll(dataRc) + + fileContent, _, err := highlight.File(blob.Name(), language, buf) + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(buf) + } + + return fileContent, nil + }, + GetLocale: func(ctx context.Context) (translation.Locale, error) { + giteaCtx, ok := ctx.(*gitea_context.Context) + if ok { + return giteaCtx.Locale, nil + } + + giteaBaseCtx, ok := ctx.(*gitea_context.Base) + if ok { + return giteaBaseCtx.Locale, nil + } + + return nil, fmt.Errorf("could not retrieve locale from context") + }, } } diff --git a/web_src/css/index.css b/web_src/css/index.css index ab925a4aa0..8d2780ba42 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -30,6 +30,7 @@ @import "./markup/content.css"; @import "./markup/codecopy.css"; @import "./markup/asciicast.css"; +@import "./markup/filepreview.css"; @import "./chroma/base.css"; @import "./codemirror/base.css"; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 5eeef078a5..430b4802d6 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -451,7 +451,8 @@ text-decoration: inherit; } -.markup pre > code { +.markup pre > code, +.markup .file-preview code { padding: 0; margin: 0; font-size: 100%; diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css new file mode 100644 index 0000000000..69360e2a70 --- /dev/null +++ b/web_src/css/markup/filepreview.css @@ -0,0 +1,35 @@ +.markup table.file-preview { + margin-bottom: 0; +} + +.markup table.file-preview td { + padding: 0 10px !important; + border: none !important; +} + +.markup table.file-preview tr { + border-top: none; + background-color: inherit !important; +} + +.markup .file-preview-box { + margin-bottom: 16px; +} + +.markup .file-preview-box .header { + padding: .5rem; + padding-left: 1rem; + border: 1px solid var(--color-secondary); + border-bottom: none; + border-radius: 0.28571429rem 0.28571429rem 0 0; + background: var(--color-box-header); +} + +.markup .file-preview-box .header > a { + display: block; +} + +.markup .file-preview-box .table { + margin-top: 0; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css index 1e5e51eac5..7780d6a263 100644 --- a/web_src/css/repo/linebutton.css +++ b/web_src/css/repo/linebutton.css @@ -1,4 +1,5 @@ -.code-view .lines-num:hover { +.code-view .lines-num:hover, +.file-preview .lines-num:hover { color: var(--color-text-dark) !important; } diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js index d878532001..9f0c745223 100644 --- a/web_src/js/features/repo-unicode-escape.js +++ b/web_src/js/features/repo-unicode-escape.js @@ -7,8 +7,8 @@ export function initUnicodeEscapeButton() { e.preventDefault(); - const fileContent = btn.closest('.file-content, .non-diff-file-content'); - const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); + const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box'); + const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview'); if (btn.matches('.escape-button')) { for (const el of fileView) el.classList.add('unicode-escaped'); hideElem(btn); From 781a37fbe18c223763f51968862f1c8f61e7e260 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Fri, 15 Mar 2024 23:49:13 +0100 Subject: [PATCH 002/565] Check error in GetRepoFileContent for io.ReadAll --- services/markup/processorhelper.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index 134b1b5152..ab6a66b367 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -87,7 +87,10 @@ func ProcessorHelper() *markup.ProcessorHelper { } defer dataRc.Close() - buf, _ := io.ReadAll(dataRc) + buf, err := io.ReadAll(dataRc) + if err != nil { + log.Error("failed to completly read blob for %-v:%s. Error: %v", repo, filePath, err) + } fileContent, _, err := highlight.File(blob.Name(), language, buf) if err != nil { From 8309f008c2721e313e1949ce42ed410e844c16e7 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Fri, 15 Mar 2024 23:52:38 +0100 Subject: [PATCH 003/565] Fix some code issues --- modules/markup/html.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 2501f8062d..631c93fc36 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -10,6 +10,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strconv" "strings" "sync" @@ -64,7 +65,7 @@ var ( validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){4,5})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) // While this email regex is definitely not perfect and I'm sure you can come up // with edge cases, it is still accepted by the CommonMark specification, as @@ -1075,11 +1076,9 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { return } - // Ensure that every group (m[0]...m[9]) has a match - for i := 0; i < 10; i++ { - if m[i] == -1 { - return - } + // Ensure that every group has a match + if slices.Contains(m, -1) { + return } urlFull := node.Data[m[0]:m[1]] @@ -1089,8 +1088,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { return } - projPath := node.Data[m[2]:m[3]] - projPath = strings.TrimSuffix(projPath, "/") + projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") commitSha := node.Data[m[4]:m[5]] filePath := node.Data[m[6]:m[7]] From fae8d9f70d31704af91cbf37bcefcc4772830695 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Fri, 15 Mar 2024 23:54:07 +0100 Subject: [PATCH 004/565] Accept at minimum 4 chars for the commit sha --- modules/markup/html.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 631c93fc36..7fe7e5fc4a 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -65,7 +65,7 @@ var ( validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){4,5})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){4,5})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) // While this email regex is definitely not perfect and I'm sure you can come up // with edge cases, it is still accepted by the CommonMark specification, as From 6721cba75b4997448b618a4b00ef25f142924de0 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 16 Mar 2024 00:35:56 +0100 Subject: [PATCH 005/565] Fix filePreviewPattern --- modules/markup/html.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 7fe7e5fc4a..d0d2530735 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -65,7 +65,7 @@ var ( validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){4,5})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) // While this email regex is definitely not perfect and I'm sure you can come up // with edge cases, it is still accepted by the CommonMark specification, as From 562e5cdf324597882b7e6971be1b9a148bbc7839 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 16 Mar 2024 01:17:04 +0100 Subject: [PATCH 006/565] Get locales directly from context like the other code; add translations for subtitle --- modules/markup/html.go | 34 ++++++++++++++++++------------ modules/markup/html_test.go | 4 ---- modules/markup/renderer.go | 2 -- options/locale/locale_en-US.ini | 4 ++++ services/markup/processorhelper.go | 14 ------------ 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index d0d2530735..1e83dad701 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -5,6 +5,7 @@ package markup import ( "bytes" + "html/template" "io" "net/url" "path" @@ -1065,7 +1066,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.Metas == nil { return } - if DefaultProcessorHelper.GetRepoFileContent == nil || DefaultProcessorHelper.GetLocale == nil { + if DefaultProcessorHelper.GetRepoFileContent == nil { return } @@ -1119,9 +1120,17 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { lineSpecs := strings.Split(hash, "-") lineCount := len(fileContent) - var subTitle string + commitLinkBuffer := new(bytes.Buffer) + html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) + + var subTitle template.HTML var lineOffset int + locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale) + if !ok { + locale = translation.NewLocale("en-US") + } + if len(lineSpecs) == 1 { line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) if line < 1 || line > lineCount { @@ -1129,7 +1138,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { } fileContent = fileContent[line-1 : line] - subTitle = "Line " + strconv.Itoa(line) + subTitle = locale.Tr( + "markup.filepreview.line", line, + template.HTML(commitLinkBuffer.String()), + ) lineOffset = line - 1 } else { @@ -1141,7 +1153,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { } fileContent = fileContent[startLine-1 : endLine] - subTitle = "Lines " + strconv.Itoa(startLine) + " to " + strconv.Itoa(endLine) + subTitle = locale.Tr( + "markup.filepreview.lines", startLine, endLine, + template.HTML(commitLinkBuffer.String()), + ) lineOffset = startLine - 1 } @@ -1156,12 +1171,6 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { Data: atom.Tbody.String(), } - locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx) - if err != nil { - log.Error("Unable to get locale. Error: %v", err) - return - } - status := &charset.EscapeStatus{} statuses := make([]*charset.EscapeStatus, len(fileContent)) for i, line := range fileContent { @@ -1286,10 +1295,9 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, } psubtitle.AppendChild(&html.Node{ - Type: html.TextNode, - Data: subTitle + " in ", + Type: html.RawNode, + Data: string(subTitle), }) - psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "text black")) header.AppendChild(psubtitle) preview := &html.Node{ diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 652db13e5e..c43f006266 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -19,7 +19,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -684,9 +683,6 @@ func TestRender_FilePreview(t *testing.T) { buf := []byte("A\nB\nC\nD\n") return highlight.PlainText(buf), nil }, - GetLocale: func(ctx context.Context) (translation.Locale, error) { - return translation.NewLocale("en-US"), nil - }, }) sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 37d3fde58c..b6d742e5ce 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" @@ -34,7 +33,6 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) - GetLocale func(ctx context.Context) (translation.Locale, error) ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a7f4de48a8..ebc8db24ca 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3707,3 +3707,7 @@ normal_file = Normal file executable_file = Executable file symbolic_link = Symbolic link submodule = Submodule + +[markup] +filepreview.line = Line %[1]d in %[3]s +filepreview.lines = Lines %[1]d to %[2]d in %[3]s diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index ab6a66b367..df96f25ce9 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/translation" gitea_context "code.gitea.io/gitea/services/context" file_service "code.gitea.io/gitea/services/repository/files" ) @@ -100,18 +99,5 @@ func ProcessorHelper() *markup.ProcessorHelper { return fileContent, nil }, - GetLocale: func(ctx context.Context) (translation.Locale, error) { - giteaCtx, ok := ctx.(*gitea_context.Context) - if ok { - return giteaCtx.Locale, nil - } - - giteaBaseCtx, ok := ctx.(*gitea_context.Base) - if ok { - return giteaBaseCtx.Locale, nil - } - - return nil, fmt.Errorf("could not retrieve locale from context") - }, } } From d789d33229b3998bb33f1505d122504c8039f23d Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 16 Mar 2024 08:09:49 +0100 Subject: [PATCH 007/565] Split filePreviewPatternProcessor into a new type FilePreview and some functions to make code more maintainable --- modules/markup/file_preview.go | 269 +++++++++++++++++++++++++++++++++ modules/markup/html.go | 245 +----------------------------- 2 files changed, 276 insertions(+), 238 deletions(-) create mode 100644 modules/markup/file_preview.go diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go new file mode 100644 index 0000000000..646bf83630 --- /dev/null +++ b/modules/markup/file_preview.go @@ -0,0 +1,269 @@ +package markup + +import ( + "bytes" + "html/template" + "regexp" + "slices" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" + "golang.org/x/net/html" + "golang.org/x/net/html/atom" +) + +var ( + // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" + filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) +) + +type FilePreview struct { + fileContent []template.HTML + subTitle template.HTML + lineOffset int + urlFull string + filePath string + start int + end int +} + +func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { + preview := &FilePreview{} + + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) + if m == nil { + return nil + } + + // Ensure that every group has a match + if slices.Contains(m, -1) { + return nil + } + + preview.urlFull = node.Data[m[0]:m[1]] + + // Ensure that we only use links to local repositories + if !strings.HasPrefix(preview.urlFull, setting.AppURL+setting.AppSubURL) { + return nil + } + + projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") + + commitSha := node.Data[m[4]:m[5]] + preview.filePath = node.Data[m[6]:m[7]] + hash := node.Data[m[8]:m[9]] + + preview.start = m[0] + preview.end = m[1] + + // If url ends in '.', it's very likely that it is not part of the + // actual url but used to finish a sentence. + if strings.HasSuffix(preview.urlFull, ".") { + preview.end-- + preview.urlFull = preview.urlFull[:len(preview.urlFull)-1] + hash = hash[:len(hash)-1] + } + + projPathSegments := strings.Split(projPath, "/") + fileContent, err := DefaultProcessorHelper.GetRepoFileContent( + ctx.Ctx, + projPathSegments[len(projPathSegments)-2], + projPathSegments[len(projPathSegments)-1], + commitSha, preview.filePath, + ) + if err != nil { + return nil + } + + lineSpecs := strings.Split(hash, "-") + lineCount := len(fileContent) + + commitLinkBuffer := new(bytes.Buffer) + html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) + + if len(lineSpecs) == 1 { + line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + if line < 1 || line > lineCount { + return nil + } + + preview.fileContent = fileContent[line-1 : line] + preview.subTitle = locale.Tr( + "markup.filepreview.line", line, + template.HTML(commitLinkBuffer.String()), + ) + + preview.lineOffset = line - 1 + } else { + startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) + + if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { + return nil + } + + preview.fileContent = fileContent[startLine-1 : endLine] + preview.subTitle = locale.Tr( + "markup.filepreview.lines", startLine, endLine, + template.HTML(commitLinkBuffer.String()), + ) + + preview.lineOffset = startLine - 1 + } + + return preview +} + +func (p *FilePreview) CreateHtml(locale translation.Locale) *html.Node { + table := &html.Node{ + Type: html.ElementNode, + Data: atom.Table.String(), + Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, + } + tbody := &html.Node{ + Type: html.ElementNode, + Data: atom.Tbody.String(), + } + + status := &charset.EscapeStatus{} + statuses := make([]*charset.EscapeStatus, len(p.fileContent)) + for i, line := range p.fileContent { + statuses[i], p.fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) + status = status.Or(statuses[i]) + } + + for idx, code := range p.fileContent { + tr := &html.Node{ + Type: html.ElementNode, + Data: atom.Tr.String(), + } + + lineNum := strconv.Itoa(p.lineOffset + idx + 1) + + tdLinesnum := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "id", Val: "L" + lineNum}, + {Key: "class", Val: "lines-num"}, + }, + } + spanLinesNum := &html.Node{ + Type: html.ElementNode, + Data: atom.Span.String(), + Attr: []html.Attribute{ + {Key: "id", Val: "L" + lineNum}, + {Key: "data-line-number", Val: lineNum}, + }, + } + tdLinesnum.AppendChild(spanLinesNum) + tr.AppendChild(tdLinesnum) + + if status.Escaped { + tdLinesEscape := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "class", Val: "lines-escape"}, + }, + } + + if statuses[idx].Escaped { + btnTitle := "" + if statuses[idx].HasInvisible { + btnTitle += locale.TrString("repo.invisible_runes_line") + " " + } + if statuses[idx].HasAmbiguous { + btnTitle += locale.TrString("repo.ambiguous_runes_line") + } + + escapeBtn := &html.Node{ + Type: html.ElementNode, + Data: atom.Button.String(), + Attr: []html.Attribute{ + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, + {Key: "title", Val: btnTitle}, + }, + } + tdLinesEscape.AppendChild(escapeBtn) + } + + tr.AppendChild(tdLinesEscape) + } + + tdCode := &html.Node{ + Type: html.ElementNode, + Data: atom.Td.String(), + Attr: []html.Attribute{ + {Key: "rel", Val: "L" + lineNum}, + {Key: "class", Val: "lines-code chroma"}, + }, + } + codeInner := &html.Node{ + Type: html.ElementNode, + Data: atom.Code.String(), + Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, + } + codeText := &html.Node{ + Type: html.RawNode, + Data: string(code), + } + codeInner.AppendChild(codeText) + tdCode.AppendChild(codeInner) + tr.AppendChild(tdCode) + + tbody.AppendChild(tr) + } + + table.AppendChild(tbody) + + twrapper := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, + } + twrapper.AppendChild(table) + + header := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "header"}}, + } + afilepath := &html.Node{ + Type: html.ElementNode, + Data: atom.A.String(), + Attr: []html.Attribute{ + {Key: "href", Val: p.urlFull}, + {Key: "class", Val: "muted"}, + }, + } + afilepath.AppendChild(&html.Node{ + Type: html.TextNode, + Data: p.filePath, + }) + header.AppendChild(afilepath) + + psubtitle := &html.Node{ + Type: html.ElementNode, + Data: atom.Span.String(), + Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, + } + psubtitle.AppendChild(&html.Node{ + Type: html.RawNode, + Data: string(p.subTitle), + }) + header.AppendChild(psubtitle) + + preview_node := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, + } + preview_node.AppendChild(header) + preview_node.AppendChild(twrapper) + + return preview_node +} diff --git a/modules/markup/html.go b/modules/markup/html.go index 1e83dad701..2e38c05f58 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -5,19 +5,15 @@ package markup import ( "bytes" - "html/template" "io" "net/url" "path" "path/filepath" "regexp" - "slices" - "strconv" "strings" "sync" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -65,9 +61,6 @@ var ( validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`) - // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) - // While this email regex is definitely not perfect and I'm sure you can come up // with edge cases, it is still accepted by the CommonMark specification, as // well as the HTML5 spec: @@ -1072,252 +1065,28 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { next := node.NextSibling for node != nil && node != next { - m := filePreviewPattern.FindStringSubmatchIndex(node.Data) - if m == nil { - return - } - - // Ensure that every group has a match - if slices.Contains(m, -1) { - return - } - - urlFull := node.Data[m[0]:m[1]] - - // Ensure that we only use links to local repositories - if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) { - return - } - - projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") - - commitSha := node.Data[m[4]:m[5]] - filePath := node.Data[m[6]:m[7]] - hash := node.Data[m[8]:m[9]] - - start := m[0] - end := m[1] - - // If url ends in '.', it's very likely that it is not part of the - // actual url but used to finish a sentence. - if strings.HasSuffix(urlFull, ".") { - end-- - urlFull = urlFull[:len(urlFull)-1] - hash = hash[:len(hash)-1] - } - - projPathSegments := strings.Split(projPath, "/") - fileContent, err := DefaultProcessorHelper.GetRepoFileContent( - ctx.Ctx, - projPathSegments[len(projPathSegments)-2], - projPathSegments[len(projPathSegments)-1], - commitSha, filePath, - ) - if err != nil { - return - } - - lineSpecs := strings.Split(hash, "-") - lineCount := len(fileContent) - - commitLinkBuffer := new(bytes.Buffer) - html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) - - var subTitle template.HTML - var lineOffset int - locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale) if !ok { locale = translation.NewLocale("en-US") } - if len(lineSpecs) == 1 { - line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - if line < 1 || line > lineCount { - return - } - - fileContent = fileContent[line-1 : line] - subTitle = locale.Tr( - "markup.filepreview.line", line, - template.HTML(commitLinkBuffer.String()), - ) - - lineOffset = line - 1 - } else { - startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) - - if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { - return - } - - fileContent = fileContent[startLine-1 : endLine] - subTitle = locale.Tr( - "markup.filepreview.lines", startLine, endLine, - template.HTML(commitLinkBuffer.String()), - ) - - lineOffset = startLine - 1 + preview := NewFilePreview(ctx, node, locale) + if preview == nil { + return } - table := &html.Node{ - Type: html.ElementNode, - Data: atom.Table.String(), - Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, - } - tbody := &html.Node{ - Type: html.ElementNode, - Data: atom.Tbody.String(), - } - - status := &charset.EscapeStatus{} - statuses := make([]*charset.EscapeStatus, len(fileContent)) - for i, line := range fileContent { - statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) - status = status.Or(statuses[i]) - } - - for idx, code := range fileContent { - tr := &html.Node{ - Type: html.ElementNode, - Data: atom.Tr.String(), - } - - lineNum := strconv.Itoa(lineOffset + idx + 1) - - tdLinesnum := &html.Node{ - Type: html.ElementNode, - Data: atom.Td.String(), - Attr: []html.Attribute{ - {Key: "id", Val: "L" + lineNum}, - {Key: "class", Val: "lines-num"}, - }, - } - spanLinesNum := &html.Node{ - Type: html.ElementNode, - Data: atom.Span.String(), - Attr: []html.Attribute{ - {Key: "id", Val: "L" + lineNum}, - {Key: "data-line-number", Val: lineNum}, - }, - } - tdLinesnum.AppendChild(spanLinesNum) - tr.AppendChild(tdLinesnum) - - if status.Escaped { - tdLinesEscape := &html.Node{ - Type: html.ElementNode, - Data: atom.Td.String(), - Attr: []html.Attribute{ - {Key: "class", Val: "lines-escape"}, - }, - } - - if statuses[idx].Escaped { - btnTitle := "" - if statuses[idx].HasInvisible { - btnTitle += locale.TrString("repo.invisible_runes_line") + " " - } - if statuses[idx].HasAmbiguous { - btnTitle += locale.TrString("repo.ambiguous_runes_line") - } - - escapeBtn := &html.Node{ - Type: html.ElementNode, - Data: atom.Button.String(), - Attr: []html.Attribute{ - {Key: "class", Val: "toggle-escape-button btn interact-bg"}, - {Key: "title", Val: btnTitle}, - }, - } - tdLinesEscape.AppendChild(escapeBtn) - } - - tr.AppendChild(tdLinesEscape) - } - - tdCode := &html.Node{ - Type: html.ElementNode, - Data: atom.Td.String(), - Attr: []html.Attribute{ - {Key: "rel", Val: "L" + lineNum}, - {Key: "class", Val: "lines-code chroma"}, - }, - } - codeInner := &html.Node{ - Type: html.ElementNode, - Data: atom.Code.String(), - Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, - } - codeText := &html.Node{ - Type: html.RawNode, - Data: string(code), - } - codeInner.AppendChild(codeText) - tdCode.AppendChild(codeInner) - tr.AppendChild(tdCode) - - tbody.AppendChild(tr) - } - - table.AppendChild(tbody) - - twrapper := &html.Node{ - Type: html.ElementNode, - Data: atom.Div.String(), - Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, - } - twrapper.AppendChild(table) - - header := &html.Node{ - Type: html.ElementNode, - Data: atom.Div.String(), - Attr: []html.Attribute{{Key: "class", Val: "header"}}, - } - afilepath := &html.Node{ - Type: html.ElementNode, - Data: atom.A.String(), - Attr: []html.Attribute{ - {Key: "href", Val: urlFull}, - {Key: "class", Val: "muted"}, - }, - } - afilepath.AppendChild(&html.Node{ - Type: html.TextNode, - Data: filePath, - }) - header.AppendChild(afilepath) - - psubtitle := &html.Node{ - Type: html.ElementNode, - Data: atom.Span.String(), - Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, - } - psubtitle.AppendChild(&html.Node{ - Type: html.RawNode, - Data: string(subTitle), - }) - header.AppendChild(psubtitle) - - preview := &html.Node{ - Type: html.ElementNode, - Data: atom.Div.String(), - Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, - } - preview.AppendChild(header) - preview.AppendChild(twrapper) + preview_node := preview.CreateHtml(locale) // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div - before := node.Data[:start] - after := node.Data[end:] + before := node.Data[:preview.start] + after := node.Data[preview.end:] node.Data = before nextSibling := node.NextSibling node.Parent.InsertBefore(&html.Node{ Type: html.RawNode, Data: "

", }, nextSibling) - node.Parent.InsertBefore(preview, nextSibling) + node.Parent.InsertBefore(preview_node, nextSibling) node.Parent.InsertBefore(&html.Node{ Type: html.RawNode, Data: "

" + after, From 8218e80bfc3a1f9ba02ce60f1acafdc0e57c5ae0 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 16 Mar 2024 08:18:47 +0100 Subject: [PATCH 008/565] Fix linting issues --- modules/markup/file_preview.go | 22 ++++++++++++---------- modules/markup/html.go | 4 ++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 646bf83630..be788aae4b 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -9,16 +9,15 @@ import ( "strings" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) -var ( - // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" - filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) -) +// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" +var filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) type FilePreview struct { fileContent []template.HTML @@ -82,7 +81,10 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca lineCount := len(fileContent) commitLinkBuffer := new(bytes.Buffer) - html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) + err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) + if err != nil { + log.Error("failed to render commitLink: %v", err) + } if len(lineSpecs) == 1 { line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) @@ -117,7 +119,7 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca return preview } -func (p *FilePreview) CreateHtml(locale translation.Locale) *html.Node { +func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { table := &html.Node{ Type: html.ElementNode, Data: atom.Table.String(), @@ -257,13 +259,13 @@ func (p *FilePreview) CreateHtml(locale translation.Locale) *html.Node { }) header.AppendChild(psubtitle) - preview_node := &html.Node{ + node := &html.Node{ Type: html.ElementNode, Data: atom.Div.String(), Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, } - preview_node.AppendChild(header) - preview_node.AppendChild(twrapper) + node.AppendChild(header) + node.AppendChild(twrapper) - return preview_node + return node } diff --git a/modules/markup/html.go b/modules/markup/html.go index 2e38c05f58..9a04e02fb8 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1075,7 +1075,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { return } - preview_node := preview.CreateHtml(locale) + previewNode := preview.CreateHTML(locale) // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div before := node.Data[:preview.start] @@ -1086,7 +1086,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { Type: html.RawNode, Data: "

", }, nextSibling) - node.Parent.InsertBefore(preview_node, nextSibling) + node.Parent.InsertBefore(previewNode, nextSibling) node.Parent.InsertBefore(&html.Node{ Type: html.RawNode, Data: "

" + after, From 10bca456a9140519e95559aa7bac2221e1156c5b Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 18 Mar 2024 06:19:27 +0100 Subject: [PATCH 009/565] Remove `rel` and `id` attributes that only add the linenumber to elements --- modules/markup/file_preview.go | 3 --- modules/markup/sanitizer.go | 1 - 2 files changed, 4 deletions(-) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index be788aae4b..167bbd1997 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -149,7 +149,6 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { Type: html.ElementNode, Data: atom.Td.String(), Attr: []html.Attribute{ - {Key: "id", Val: "L" + lineNum}, {Key: "class", Val: "lines-num"}, }, } @@ -157,7 +156,6 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { Type: html.ElementNode, Data: atom.Span.String(), Attr: []html.Attribute{ - {Key: "id", Val: "L" + lineNum}, {Key: "data-line-number", Val: lineNum}, }, } @@ -200,7 +198,6 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { Type: html.ElementNode, Data: atom.Td.String(), Attr: []html.Attribute{ - {Key: "rel", Val: "L" + lineNum}, {Key: "class", Val: "lines-code chroma"}, }, } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 73e17060a7..c37027b843 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -128,7 +128,6 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") - policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") From db6f6281fcf568ae8e35330a4a93c9be1cb46efd Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 18 Mar 2024 06:21:35 +0100 Subject: [PATCH 010/565] Add copyright & license header to file_preview.go --- modules/markup/file_preview.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 167bbd1997..377809529d 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -1,3 +1,6 @@ +// Copyright The Forgejo Authors. +// SPDX-License-Identifier: MIT + package markup import ( From ed8e8a792e75b930074cd3cf1bab580a09ff8485 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 18 Mar 2024 06:23:12 +0100 Subject: [PATCH 011/565] Run make fmt --- modules/markup/file_preview.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 377809529d..2702cb7ce3 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" + "golang.org/x/net/html" "golang.org/x/net/html/atom" ) From d6428f92ce7ce67d127cbd5bb4977aa92abf071c Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 25 Mar 2024 14:33:30 +0100 Subject: [PATCH 012/565] Fix typo in language files --- options/locale/locale_en-US.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ebc8db24ca..efaf8b72c9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3709,5 +3709,5 @@ symbolic_link = Symbolic link submodule = Submodule [markup] -filepreview.line = Line %[1]d in %[3]s +filepreview.line = Line %[1]d in %[2]s filepreview.lines = Lines %[1]d to %[2]d in %[3]s From 069d87b80f909e91626249afbb240a1df339a8fd Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 25 Mar 2024 14:33:54 +0100 Subject: [PATCH 013/565] Remove unneeded case for a trailing dot --- modules/markup/file_preview.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 2702cb7ce3..3e76dcb8a4 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -62,14 +62,6 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.start = m[0] preview.end = m[1] - // If url ends in '.', it's very likely that it is not part of the - // actual url but used to finish a sentence. - if strings.HasSuffix(preview.urlFull, ".") { - preview.end-- - preview.urlFull = preview.urlFull[:len(preview.urlFull)-1] - hash = hash[:len(hash)-1] - } - projPathSegments := strings.Split(projPath, "/") fileContent, err := DefaultProcessorHelper.GetRepoFileContent( ctx.Ctx, From 2b6546adc954d450a9c6befccd407ce2ca1636a0 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 25 Mar 2024 16:05:01 +0100 Subject: [PATCH 014/565] Add setting to restrict count of lines being displayed & only highlight those lines --- custom/conf/app.example.ini | 2 + modules/markup/file_preview.go | 103 ++++++++++++++++++++++++----- modules/markup/html.go | 2 +- modules/markup/renderer.go | 4 +- modules/markup/sanitizer.go | 1 + modules/setting/markup.go | 2 + options/locale/locale_en-US.ini | 1 + services/markup/processorhelper.go | 24 ++++--- web_src/css/markup/filepreview.css | 6 ++ 9 files changed, 117 insertions(+), 28 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b3896bc31c..91f86da5f8 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2338,6 +2338,8 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits) ;MERMAID_MAX_SOURCE_CHARACTERS = 5000 +;; Set the maximum number of lines allowed for a filepreview. (Set to -1 to disable limits; set to 0 to disable the feature) +;FILEPREVIEW_MAX_LINES = 50 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 3e76dcb8a4..32683c317c 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -4,6 +4,7 @@ package markup import ( + "bufio" "bytes" "html/template" "regexp" @@ -12,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" @@ -31,9 +33,15 @@ type FilePreview struct { filePath string start int end int + isTruncated bool } func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { + if (setting.FilePreviewMaxLines == 0) { + // Feature is disabled + return nil + } + preview := &FilePreview{} m := filePreviewPattern.FindStringSubmatchIndex(node.Data) @@ -63,18 +71,20 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.end = m[1] projPathSegments := strings.Split(projPath, "/") - fileContent, err := DefaultProcessorHelper.GetRepoFileContent( + var language string + fileBlob, err := DefaultProcessorHelper.GetRepoFileBlob( ctx.Ctx, projPathSegments[len(projPathSegments)-2], projPathSegments[len(projPathSegments)-1], commitSha, preview.filePath, + &language, ) if err != nil { return nil } lineSpecs := strings.Split(hash, "-") - lineCount := len(fileContent) + // lineCount := len(fileContent) commitLinkBuffer := new(bytes.Buffer) err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) @@ -82,28 +92,31 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca log.Error("failed to render commitLink: %v", err) } - if len(lineSpecs) == 1 { - line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - if line < 1 || line > lineCount { - return nil - } + var startLine, endLine int - preview.fileContent = fileContent[line-1 : line] + if len(lineSpecs) == 1 { + startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + endLine = startLine + // if line < 1 || line > lineCount { + // return nil + // } + + // preview.fileContent = fileContent[line-1 : line] preview.subTitle = locale.Tr( - "markup.filepreview.line", line, + "markup.filepreview.line", startLine, template.HTML(commitLinkBuffer.String()), ) - preview.lineOffset = line - 1 + preview.lineOffset = startLine - 1 } else { - startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) + startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + endLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) - if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { - return nil - } + // if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { + // return nil + // } - preview.fileContent = fileContent[startLine-1 : endLine] + // preview.fileContent = fileContent[startLine-1 : endLine] preview.subTitle = locale.Tr( "markup.filepreview.lines", startLine, endLine, template.HTML(commitLinkBuffer.String()), @@ -112,6 +125,50 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.lineOffset = startLine - 1 } + lineCount := endLine - (startLine-1) + if startLine < 1 || endLine < 1 || lineCount < 1 { + return nil + } + + if setting.FilePreviewMaxLines > 0 && lineCount > setting.FilePreviewMaxLines { + preview.isTruncated = true + lineCount = setting.FilePreviewMaxLines + } + + dataRc, err := fileBlob.DataAsync() + if err != nil { + return nil + } + defer dataRc.Close() + + reader := bufio.NewReader(dataRc) + + // skip all lines until we find our startLine + for i := 1; i < startLine; i++ { + _, err := reader.ReadBytes('\n') + if err != nil { + return nil + } + } + + // capture the lines we're interested in + lineBuffer := new(bytes.Buffer) + for i := 0; i < lineCount; i++ { + buf, err := reader.ReadBytes('\n') + if err != nil { + break; + } + lineBuffer.Write(buf) + } + + // highlight the file... + fileContent, _, err := highlight.File(fileBlob.Name(), language, lineBuffer.Bytes()) + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(lineBuffer.Bytes()) + } + preview.fileContent = fileContent + return preview } @@ -258,6 +315,20 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, } node.AppendChild(header) + + if (p.isTruncated) { + warning := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "ui warning message tw-text-left"}}, + } + warning.AppendChild(&html.Node{ + Type: html.TextNode, + Data: locale.TrString("markup.filepreview.truncated"), + }) + node.AppendChild(warning) + } + node.AppendChild(twrapper) return node diff --git a/modules/markup/html.go b/modules/markup/html.go index 9a04e02fb8..4c74a81ba7 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1059,7 +1059,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.Metas == nil { return } - if DefaultProcessorHelper.GetRepoFileContent == nil { + if DefaultProcessorHelper.GetRepoFileBlob == nil { return } diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index b6d742e5ce..b08c9eb230 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -8,7 +8,7 @@ import ( "context" "errors" "fmt" - "html/template" + // "html/template" "io" "net/url" "path/filepath" @@ -32,7 +32,7 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool - GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) + GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index c37027b843..1048f0e374 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -135,6 +135,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") policy.AllowAttrs("data-tooltip-content").OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div") // Allow generally safe attributes generalSafeAttrs := []string{ diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 6c2246342b..e893c1c2f1 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -15,6 +15,7 @@ var ( ExternalMarkupRenderers []*MarkupRenderer ExternalSanitizerRules []MarkupSanitizerRule MermaidMaxSourceCharacters int + FilePreviewMaxLines int ) const ( @@ -62,6 +63,7 @@ func loadMarkupFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "markdown", &Markdown) MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000) + FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50) ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index efaf8b72c9..8533cf0650 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3711,3 +3711,4 @@ submodule = Submodule [markup] filepreview.line = Line %[1]d in %[2]s filepreview.lines = Lines %[1]d to %[2]d in %[3]s +filepreview.truncated = Preview has been truncated diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index df96f25ce9..98a7824a6e 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -6,15 +6,17 @@ package markup import ( "context" "fmt" - "html/template" - "io" + + // "html/template" + // "io" "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/highlight" + // "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" gitea_context "code.gitea.io/gitea/services/context" @@ -39,7 +41,7 @@ func ProcessorHelper() *markup.ProcessorHelper { // when using gitea context (web context), use user's visibility and user's permission to check return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) }, - GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { + GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) if err != nil { return nil, err @@ -70,9 +72,11 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } - language, err := file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) - if err != nil { - log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) + if language != nil { + *language, err = file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) + } } blob, err := commit.GetBlobByPath(filePath) @@ -80,7 +84,9 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } - dataRc, err := blob.DataAsync() + return blob, nil + + /*dataRc, err := blob.DataAsync() if err != nil { return nil, err } @@ -97,7 +103,7 @@ func ProcessorHelper() *markup.ProcessorHelper { fileContent = highlight.PlainText(buf) } - return fileContent, nil + return fileContent, nil*/ }, } } diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css index 69360e2a70..d2ec16ea8b 100644 --- a/web_src/css/markup/filepreview.css +++ b/web_src/css/markup/filepreview.css @@ -25,6 +25,12 @@ background: var(--color-box-header); } +.markup .file-preview-box .warning { + border-radius: 0; + margin: 0; + padding: .5rem .5rem .5rem 1rem; +} + .markup .file-preview-box .header > a { display: block; } From 4f43b7338b956e7850927ba452492fe1b4f33238 Mon Sep 17 00:00:00 2001 From: Leo Heitmann Ruiz Date: Tue, 26 Mar 2024 22:01:53 +0100 Subject: [PATCH 015/565] "Plaintext" => "Text" --- modules/highlight/highlight.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index d7ab3f7afd..8fa2de6daf 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -217,7 +217,7 @@ func PlainText(code []byte) []template.HTML { func formatLexerName(name string) string { if name == "fallback" { - return "Plaintext" + return "Text" } return util.ToTitleCaseNoLower(name) From c340e020786b1451c1693853fc3c42102729a10a Mon Sep 17 00:00:00 2001 From: Leo Heitmann Ruiz Date: Tue, 26 Mar 2024 22:25:34 +0100 Subject: [PATCH 016/565] "Plaintext" => "Text" --- modules/highlight/highlight_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index 659688bd0f..dd15b97847 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -58,7 +58,7 @@ func TestFile(t *testing.T) { name: "tags.txt", code: "<>", want: lines("<>"), - lexerName: "Plaintext", + lexerName: "Text", }, { name: "tags.py", From 4c7cb0a5d20e8973b03e35d91119cf917eed125e Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Wed, 27 Mar 2024 18:25:37 +0100 Subject: [PATCH 017/565] Close git.Repository when GetRepoFileBlob returns --- services/markup/processorhelper.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index 98a7824a6e..7466e962d2 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -66,6 +66,7 @@ func ProcessorHelper() *markup.ProcessorHelper { if err != nil { return nil, err } + defer gitRepo.Close() commit, err := gitRepo.GetCommit(commitSha) if err != nil { From 7e0014dd1391e123d95f2537c3b2165fef7122ef Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Wed, 27 Mar 2024 18:36:12 +0100 Subject: [PATCH 018/565] Fix formating & remove commented out code --- modules/markup/file_preview.go | 20 ++++---------------- modules/markup/renderer.go | 2 +- services/markup/processorhelper.go | 19 ------------------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 32683c317c..95c94e0c14 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -37,7 +37,7 @@ type FilePreview struct { } func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { - if (setting.FilePreviewMaxLines == 0) { + if setting.FilePreviewMaxLines == 0 { // Feature is disabled return nil } @@ -84,7 +84,6 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca } lineSpecs := strings.Split(hash, "-") - // lineCount := len(fileContent) commitLinkBuffer := new(bytes.Buffer) err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) @@ -97,11 +96,6 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca if len(lineSpecs) == 1 { startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) endLine = startLine - // if line < 1 || line > lineCount { - // return nil - // } - - // preview.fileContent = fileContent[line-1 : line] preview.subTitle = locale.Tr( "markup.filepreview.line", startLine, template.HTML(commitLinkBuffer.String()), @@ -111,12 +105,6 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca } else { startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) endLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) - - // if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { - // return nil - // } - - // preview.fileContent = fileContent[startLine-1 : endLine] preview.subTitle = locale.Tr( "markup.filepreview.lines", startLine, endLine, template.HTML(commitLinkBuffer.String()), @@ -125,7 +113,7 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.lineOffset = startLine - 1 } - lineCount := endLine - (startLine-1) + lineCount := endLine - (startLine - 1) if startLine < 1 || endLine < 1 || lineCount < 1 { return nil } @@ -156,7 +144,7 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca for i := 0; i < lineCount; i++ { buf, err := reader.ReadBytes('\n') if err != nil { - break; + break } lineBuffer.Write(buf) } @@ -316,7 +304,7 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { } node.AppendChild(header) - if (p.isTruncated) { + if p.isTruncated { warning := &html.Node{ Type: html.ElementNode, Data: atom.Div.String(), diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index b08c9eb230..163cd5d688 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -32,7 +32,7 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool - GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) + GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index 7466e962d2..ac751d0e62 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -86,25 +86,6 @@ func ProcessorHelper() *markup.ProcessorHelper { } return blob, nil - - /*dataRc, err := blob.DataAsync() - if err != nil { - return nil, err - } - defer dataRc.Close() - - buf, err := io.ReadAll(dataRc) - if err != nil { - log.Error("failed to completly read blob for %-v:%s. Error: %v", repo, filePath, err) - } - - fileContent, _, err := highlight.File(blob.Name(), language, buf) - if err != nil { - log.Error("highlight.File failed, fallback to plain text: %v", err) - fileContent = highlight.PlainText(buf) - } - - return fileContent, nil*/ }, } } From 5785ae72c75ea66cdadfc260d59084e0bb0bf0bb Mon Sep 17 00:00:00 2001 From: oliverpool Date: Wed, 27 Mar 2024 22:02:51 +0100 Subject: [PATCH 019/565] [TESTS] prevent overriding testlogger when calling mainApp --- modules/testlogger/testlogger.go | 5 +++ tests/integration/cmd_forgejo_actions_test.go | 32 +++++++++------- tests/integration/cmd_forgejo_test.go | 36 ------------------ tests/integration/cmd_keys_test.go | 27 +++++++------ tests/integration/integration_test.go | 38 +++++++++++++++++++ 5 files changed, 75 insertions(+), 63 deletions(-) delete mode 100644 tests/integration/cmd_forgejo_test.go diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index c5ca906220..43fbaf0afd 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -128,6 +128,11 @@ func (w *testLoggerWriterCloser) recordError(msg string) { err = w.errs[len(w.errs)-1] } + if len(w.t) > 0 { + // format error message to easily add it to the ignore list + msg = fmt.Sprintf("// %s\n\t`%s`,", w.t[len(w.t)-1].Name(), msg) + } + err = errors.Join(err, errors.New(msg)) if len(w.errs) > 0 { diff --git a/tests/integration/cmd_forgejo_actions_test.go b/tests/integration/cmd_forgejo_actions_test.go index 44211007f5..e45526ac7a 100644 --- a/tests/integration/cmd_forgejo_actions_test.go +++ b/tests/integration/cmd_forgejo_actions_test.go @@ -4,8 +4,11 @@ package integration import ( gocontext "context" + "errors" + "io" "net/url" "os" + "os/exec" "strings" "testing" @@ -19,16 +22,18 @@ import ( func Test_CmdForgejo_Actions(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { - token, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-runner-token"}) + token, err := runMainApp("forgejo-cli", "actions", "generate-runner-token") assert.NoError(t, err) assert.EqualValues(t, 40, len(token)) - secret, err := cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "generate-secret"}) + secret, err := runMainApp("forgejo-cli", "actions", "generate-secret") assert.NoError(t, err) assert.EqualValues(t, 40, len(secret)) - _, err = cmdForgejoCaptureOutput(t, []string{"forgejo", "forgejo-cli", "actions", "register"}) - assert.ErrorContains(t, err, "at least one of the --secret") + _, err = runMainApp("forgejo-cli", "actions", "register") + var exitErr *exec.ExitError + assert.True(t, errors.As(err, &exitErr)) + assert.Contains(t, string(exitErr.Stderr), "at least one of the --secret") for _, testCase := range []struct { testName string @@ -62,10 +67,12 @@ func Test_CmdForgejo_Actions(t *testing.T) { }, } { t.Run(testCase.testName, func(t *testing.T) { - cmd := []string{"forgejo", "forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope} - output, err := cmdForgejoCaptureOutput(t, cmd) - assert.ErrorContains(t, err, testCase.errorMessage) + output, err := runMainApp("forgejo-cli", "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope) assert.EqualValues(t, "", output) + + var exitErr *exec.ExitError + assert.True(t, errors.As(err, &exitErr)) + assert.Contains(t, string(exitErr.Stderr), testCase.errorMessage) }) } @@ -75,7 +82,7 @@ func Test_CmdForgejo_Actions(t *testing.T) { for _, testCase := range []struct { testName string secretOption func() string - stdin []string + stdin io.Reader }{ { testName: "secret from argument", @@ -88,7 +95,7 @@ func Test_CmdForgejo_Actions(t *testing.T) { secretOption: func() string { return "--secret-stdin" }, - stdin: []string{secret}, + stdin: strings.NewReader(secret), }, { testName: "secret from file", @@ -100,8 +107,7 @@ func Test_CmdForgejo_Actions(t *testing.T) { }, } { t.Run(testCase.testName, func(t *testing.T) { - cmd := []string{"forgejo", "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26"} - uuid, err := cmdForgejoCaptureOutput(t, cmd, testCase.stdin...) + uuid, err := runMainAppWithStdin(testCase.stdin, "forgejo-cli", "actions", "register", testCase.secretOption(), "--scope=org26") assert.NoError(t, err) assert.EqualValues(t, expecteduuid, uuid) }) @@ -161,7 +167,7 @@ func Test_CmdForgejo_Actions(t *testing.T) { } { t.Run(testCase.testName, func(t *testing.T) { cmd := []string{ - "forgejo", "forgejo-cli", "actions", "register", + "actions", "register", "--secret", testCase.secret, "--scope", testCase.scope, } if testCase.name != "" { @@ -177,7 +183,7 @@ func Test_CmdForgejo_Actions(t *testing.T) { // Run twice to verify it is idempotent // for i := 0; i < 2; i++ { - uuid, err := cmdForgejoCaptureOutput(t, cmd) + uuid, err := runMainApp("forgejo-cli", cmd...) assert.NoError(t, err) if assert.EqualValues(t, testCase.uuid, uuid) { ownerName, repoName, found := strings.Cut(testCase.scope, "/") diff --git a/tests/integration/cmd_forgejo_test.go b/tests/integration/cmd_forgejo_test.go deleted file mode 100644 index 76f5a6fc08..0000000000 --- a/tests/integration/cmd_forgejo_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT - -package integration - -import ( - "bytes" - "context" - "strings" - "testing" - - "code.gitea.io/gitea/cmd/forgejo" - - "github.com/urfave/cli/v2" -) - -func cmdForgejoCaptureOutput(t *testing.T, args []string, stdin ...string) (string, error) { - buf := new(bytes.Buffer) - - app := cli.NewApp() - app.Writer = buf - app.ErrWriter = buf - ctx := context.Background() - ctx = forgejo.ContextSetNoInit(ctx, true) - ctx = forgejo.ContextSetNoExit(ctx, true) - ctx = forgejo.ContextSetStdout(ctx, buf) - ctx = forgejo.ContextSetStderr(ctx, buf) - if len(stdin) > 0 { - ctx = forgejo.ContextSetStdin(ctx, strings.NewReader(strings.Join(stdin, ""))) - } - app.Commands = []*cli.Command{ - forgejo.CmdForgejo(ctx), - } - err := app.Run(args) - - return buf.String(), err -} diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index 61f11c58b0..a3220c13ce 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -4,16 +4,15 @@ package integration import ( - "bytes" + "errors" "net/url" + "os/exec" "testing" - "code.gitea.io/gitea/cmd" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" ) func Test_CmdKeys(t *testing.T) { @@ -24,30 +23,30 @@ func Test_CmdKeys(t *testing.T) { wantErr bool expectedOutput string }{ - {"test_empty_1", []string{"keys", "--username=git", "--type=test", "--content=test"}, true, ""}, - {"test_empty_2", []string{"keys", "-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""}, + {"test_empty_1", []string{"--username=git", "--type=test", "--content=test"}, true, ""}, + {"test_empty_2", []string{"-e", "git", "-u", "git", "-t", "test", "-k", "test"}, true, ""}, { "with_key", - []string{"keys", "-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="}, + []string{"-e", "git", "-u", "git", "-t", "ssh-rsa", "-k", "AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM="}, false, "# gitea public key\ncommand=\"" + setting.AppPath + " --config=" + util.ShellEscape(setting.CustomConf) + " serv key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM= user2@localhost\n", }, - {"invalid", []string{"keys", "--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"}, + {"invalid", []string{"--not-a-flag=git"}, true, "Incorrect Usage: flag provided but not defined: -not-a-flag\n\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out := new(bytes.Buffer) - app := cli.NewApp() - app.Writer = out - app.Commands = []*cli.Command{cmd.CmdKeys} - cmd.CmdKeys.HideHelp = true - err := app.Run(append([]string{"prog"}, tt.args...)) + out, err := runMainApp("keys", tt.args...) + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + t.Log(string(exitErr.Stderr)) + } if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } - assert.Equal(t, tt.expectedOutput, out.String()) + assert.Equal(t, tt.expectedOutput, out) }) } }) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index e8f28105c1..b087281ff4 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -17,6 +17,7 @@ import ( "net/http/httptest" "net/url" "os" + "os/exec" "path/filepath" "strconv" "strings" @@ -24,6 +25,7 @@ import ( "testing" "time" + "code.gitea.io/gitea/cmd" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -93,7 +95,43 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder { } } +// runMainApp runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr. +func runMainApp(subcommand string, args ...string) (string, error) { + return runMainAppWithStdin(nil, subcommand, args...) +} + +// runMainAppWithStdin runs the subcommand and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr. +func runMainAppWithStdin(stdin io.Reader, subcommand string, args ...string) (string, error) { + // running the main app directly will very likely mess with the testing setup (logger & co.) + // hence we run it as a subprocess and capture its output + args = append([]string{subcommand}, args...) + cmd := exec.Command(os.Args[0], args...) + cmd.Env = append(os.Environ(), + "GITEA_TEST_CLI=true", + "GITEA_CONF="+setting.CustomConf, + "GITEA_WORK_DIR="+setting.AppWorkPath) + cmd.Stdin = stdin + out, err := cmd.Output() + return string(out), err +} + func TestMain(m *testing.M) { + // GITEA_TEST_CLI is set by runMainAppWithStdin + // inspired by https://abhinavg.net/2022/05/15/hijack-testmain/ + if testCLI := os.Getenv("GITEA_TEST_CLI"); testCLI == "true" { + app := cmd.NewMainApp("test-version", "integration-test") + args := append([]string{ + "executable-name", // unused, but expected at position 1 + "--config", os.Getenv("GITEA_CONF"), + }, + os.Args[1:]..., // skip the executable name + ) + if err := cmd.RunMainApp(app, args...); err != nil { + panic(err) // should never happen since RunMainApp exits on error + } + return + } + defer log.GetManager().Close() managerCtx, cancel := context.WithCancel(context.Background()) From 16a8658878a2656cb131453b728b65a89271f11f Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 28 Mar 2024 04:20:13 +0100 Subject: [PATCH 020/565] Update test --- modules/markup/html_test.go | 40 ++++++++++++------ .../markup/tests/repo/repo1_filepreview/HEAD | 1 + .../tests/repo/repo1_filepreview/config | 6 +++ .../tests/repo/repo1_filepreview/description | 1 + .../tests/repo/repo1_filepreview/info/exclude | 6 +++ .../19/0d9492934af498c3f669d6a2431dc5459e5b20 | Bin 0 -> 120 bytes .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904 | Bin 0 -> 15 bytes .../83/57a737d04385bb7f2ab59ff184be94756e7972 | Bin 0 -> 44 bytes .../84/22d40f12717e1ebd5cef2449f6c09d1f775969 | Bin 0 -> 23 bytes .../d4/490327def9658be036d6a52c4417d84e74dd4c | Bin 0 -> 46 bytes .../ee/2b1253d9cf407796e2e724926cbe3a974b214d | 1 + .../repo/repo1_filepreview/refs/heads/master | 1 + 12 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 modules/markup/tests/repo/repo1_filepreview/HEAD create mode 100644 modules/markup/tests/repo/repo1_filepreview/config create mode 100644 modules/markup/tests/repo/repo1_filepreview/description create mode 100644 modules/markup/tests/repo/repo1_filepreview/info/exclude create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/19/0d9492934af498c3f669d6a2431dc5459e5b20 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/83/57a737d04385bb7f2ab59ff184be94756e7972 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/84/22d40f12717e1ebd5cef2449f6c09d1f775969 create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/d4/490327def9658be036d6a52c4417d84e74dd4c create mode 100644 modules/markup/tests/repo/repo1_filepreview/objects/ee/2b1253d9cf407796e2e724926cbe3a974b214d create mode 100644 modules/markup/tests/repo/repo1_filepreview/refs/heads/master diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index c43f006266..3583894bae 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -5,7 +5,6 @@ package markup_test import ( "context" - "html/template" "io" "os" "strings" @@ -14,14 +13,15 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var localMetas = map[string]string{ @@ -677,16 +677,30 @@ func TestIssue18471(t *testing.T) { } func TestRender_FilePreview(t *testing.T) { + setting.StaticRootPath = "../../" + setting.Names = []string{"english"} + setting.Langs = []string{"en-US"} + translation.InitLocales(context.Background()) + setting.AppURL = markup.TestAppURL markup.Init(&markup.ProcessorHelper{ - GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { - buf := []byte("A\nB\nC\nD\n") - return highlight.PlainText(buf), nil + GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { + gitRepo, err := git.OpenRepository(git.DefaultContext, "./tests/repo/repo1_filepreview") + require.NoError(t, err) + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit("HEAD") + require.NoError(t, err) + + blob, err := commit.GetBlobByPath("path/to/file.go") + require.NoError(t, err) + + return blob, nil }, }) - sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" - commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L1-L2" + sha := "190d9492934af498c3f669d6a2431dc5459e5b20" + commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3" test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ @@ -703,21 +717,21 @@ func TestRender_FilePreview(t *testing.T) { `

`+ `
`+ `
`+ - `path/to/file.go`+ + `path/to/file.go`+ ``+ - `Lines 1 to 2 in b6dd621`+ + `Lines 2 to 3 in 190d949`+ ``+ `
`+ `
`+ ``+ ``+ ``+ - ``+ - ``+ + ``+ + ``+ ``+ ``+ - ``+ - ``+ + ``+ + ``+ ``+ ``+ `
A`+"\n"+`B`+"\n"+`
B`+"\n"+`C`+"\n"+`
`+ diff --git a/modules/markup/tests/repo/repo1_filepreview/HEAD b/modules/markup/tests/repo/repo1_filepreview/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/modules/markup/tests/repo/repo1_filepreview/config b/modules/markup/tests/repo/repo1_filepreview/config new file mode 100644 index 0000000000..42cc799c8d --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true +[remote "origin"] + url = /home/mai/projects/codeark/forgejo/forgejo/modules/markup/tests/repo/repo1_filepreview/../../__test_repo diff --git a/modules/markup/tests/repo/repo1_filepreview/description b/modules/markup/tests/repo/repo1_filepreview/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/modules/markup/tests/repo/repo1_filepreview/info/exclude b/modules/markup/tests/repo/repo1_filepreview/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/19/0d9492934af498c3f669d6a2431dc5459e5b20 b/modules/markup/tests/repo/repo1_filepreview/objects/19/0d9492934af498c3f669d6a2431dc5459e5b20 new file mode 100644 index 0000000000000000000000000000000000000000..161d0bafc6731f5fe0b3b3c29ffe5463b056e840 GIT binary patch literal 120 zcmV-;0Ehp00hNtO4#F@D06FIsz9S(!b&^)95RZTdgxH8kluC)q`&oX#X-+d!)@7*% z6w=O`DhTt0gHNKjDTeW?I7Ep#_`*y{M%Kh4TwLDlzBagYZ3Of7#i>`*Lw&yTqskE| a(I?AD9`;CxuKZr6|5@&=-P{|T!ZCX0g*&(a literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 b/modules/markup/tests/repo/repo1_filepreview/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 new file mode 100644 index 0000000000000000000000000000000000000000..adf64119a33d7621aeeaa505d30adb58afaa5559 GIT binary patch literal 15 Wcmb)%hIiUR!8gx4luvu~TxC+uKC9{8ioO8p9WH2!R0)>Lak_?9C@a5(goLhI-Yi*tXv1Q+s(!9zd00ff{ EldH%Sg8%>k literal 0 HcmV?d00001 diff --git a/modules/markup/tests/repo/repo1_filepreview/objects/ee/2b1253d9cf407796e2e724926cbe3a974b214d b/modules/markup/tests/repo/repo1_filepreview/objects/ee/2b1253d9cf407796e2e724926cbe3a974b214d new file mode 100644 index 0000000000..e13ca647db --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/objects/ee/2b1253d9cf407796e2e724926cbe3a974b214d @@ -0,0 +1 @@ +x+)JMU06e040031QHหฬIีKฯghQบย/TX'ท7ๆฝŠ็ทsๅห#3‹๔ \ No newline at end of file diff --git a/modules/markup/tests/repo/repo1_filepreview/refs/heads/master b/modules/markup/tests/repo/repo1_filepreview/refs/heads/master new file mode 100644 index 0000000000..49c348b41c --- /dev/null +++ b/modules/markup/tests/repo/repo1_filepreview/refs/heads/master @@ -0,0 +1 @@ +190d9492934af498c3f669d6a2431dc5459e5b20 From 6e98bacbbd3c089b2ccfa725c58184f4dfe5e7fe Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 28 Mar 2024 05:42:25 +0100 Subject: [PATCH 021/565] Format code --- modules/markup/html_test.go | 2 +- modules/markup/renderer.go | 1 - services/markup/processorhelper.go | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 3583894bae..1ecf519f46 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -688,7 +688,7 @@ func TestRender_FilePreview(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, "./tests/repo/repo1_filepreview") require.NoError(t, err) defer gitRepo.Close() - + commit, err := gitRepo.GetCommit("HEAD") require.NoError(t, err) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 163cd5d688..6781d2e552 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - // "html/template" "io" "net/url" "path/filepath" diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index ac751d0e62..40bf1d65da 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -7,16 +7,12 @@ import ( "context" "fmt" - // "html/template" - // "io" - "code.gitea.io/gitea/models/perm/access" "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - // "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" gitea_context "code.gitea.io/gitea/services/context" From cbd067e759b1de054da17c18f92fc934c10eacf8 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Thu, 28 Mar 2024 19:48:56 +0500 Subject: [PATCH 022/565] Fix accessibility and translatability of repo explore counters Progression of: https://codeberg.org/forgejo/forgejo/commit/9e69ef9c51cded6321e4cca39d33a64e9801d910 Regression of: https://codeberg.org/forgejo/forgejo/commit/65e190ae8bd6c72d8701a58d67b256c87b92c189#diff-8d94e33cfe70fa6443d059b9c34e3f8064514816 --- templates/explore/repo_list.tmpl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index 9e5042d650..99c9bc1736 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -38,15 +38,15 @@ {{end}} {{if not $.DisableStars}} - - {{svg "octicon-star" 16}} - {{CountFmt .NumStars}} + + {{svg "octicon-star" 16}} + {{if ge .NumStars 1000}}data-tooltip-content="{{.NumStars}}"{{end}}{{CountFmt .NumStars}} {{end}} {{if not $.DisableForks}} - - {{svg "octicon-git-branch" 16}} - {{CountFmt .NumForks}} + + {{svg "octicon-git-branch" 16}} + {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}{{CountFmt .NumForks}} {{end}}
From 869795a530f6a5a4e28687c003300d39fbaee6e2 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 28 Mar 2024 15:45:51 +0100 Subject: [PATCH 023/565] [RELEASE] GITEA_VERSION is a fallback for FORGEJO_VERSION Existing Forgejo packages may rely on setting GITEA_VERSION to specify the version to build if: * they do not build from the git repository with the proper tag * they build from a source tarbal that does not have a VERSION file With 7.0 the logic of setting the version was modified in the `[RELEASE] Gitea version is for interoperability only` commit and ignores this variable which creates an unecessary breaking change. If GITEA_VERSION is set, the versions will be set on 7.0 exactly as they would have with version before and included 1.21. * If GITEA_VERSION is not set, all versions are the same * If GITEA_VERSION is set, there is a distinction between the version set in the binary are returned by the Gitea API and the version returned by the Forgejo API which includes metadata. Before: $ make GITEA_VERSION=7.0.0 show-version-full 7.0.0-dev-1809-cd6fa771ab+gitea-1.22.0 $ make GITEA_VERSION=7.0.0 show-version-api 7.0.0-dev-1809-cd6fa771ab+gitea-1.22.0 After: $ make GITEA_VERSION=7.0.0 show-version-full 7.0.0 $ make GITEA_VERSION=7.0.0 show-version-api 7.0.0+gitea-1.22.0 --- Makefile | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index b1c4864c7a..16d7edb2dd 100644 --- a/Makefile +++ b/Makefile @@ -88,8 +88,13 @@ STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null) ifneq ($(STORED_VERSION),) FORGEJO_VERSION ?= $(STORED_VERSION) else - # drop the "g" prefix prepended by git describe to the commit hash - FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/')+${GITEA_COMPATIBILITY} + ifneq ($(GITEA_VERSION),) + FORGEJO_VERSION ?= $(GITEA_VERSION) + FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY} + else + # drop the "g" prefix prepended by git describe to the commit hash + FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/')+${GITEA_COMPATIBILITY} + endif endif FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//') FORGEJO_VERSION_MINOR=$(shell echo $(FORGEJO_VERSION) | sed -E -e 's/^([0-9]+\.[0-9]+).*/\1/') @@ -106,7 +111,12 @@ show-version-minor: RELEASE_VERSION ?= ${FORGEJO_VERSION} VERSION ?= ${RELEASE_VERSION} -LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(FORGEJO_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)" +FORGEJO_VERSION_API ?= ${FORGEJO_VERSION} + +show-version-api: + @echo ${FORGEJO_VERSION_API} + +LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(FORGEJO_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION_API)" LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 From 79b70893601c33a33d8d44eb0421797dfd846a47 Mon Sep 17 00:00:00 2001 From: Gusted Date: Thu, 28 Mar 2024 21:41:52 +0100 Subject: [PATCH 024/565] [FEAT] Configure if protected branch rule should apply to admins - Currently protected branch rules do not apply to admins, however in some cases (like in the case of Forgejo project) you might also want to apply these rules to admins to avoid accidental merges. - Add new option to configure this on a per-rule basis. - Adds integration tests. - Resolves #65 --- models/forgejo_migrations/migrate.go | 2 + models/forgejo_migrations/v1_22/v9.go | 15 ++++ models/git/protected_branch.go | 1 + modules/structs/repo_branch.go | 3 + options/locale/locale_en-US.ini | 3 + routers/api/v1/repo/branch.go | 5 ++ routers/private/hook_pre_receive.go | 16 ++-- routers/web/repo/setting/protected_branch.go | 1 + services/convert/convert.go | 1 + services/forms/repo_form.go | 1 + services/pull/check.go | 7 +- services/pull/merge.go | 29 ++++--- templates/repo/issue/view_content/pull.tmpl | 2 +- templates/repo/settings/protected_branch.tmpl | 8 ++ templates/swagger/v1_json.tmpl | 12 +++ tests/integration/proctected_branch_test.go | 87 +++++++++++++++++++ 16 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 models/forgejo_migrations/v1_22/v9.go create mode 100644 tests/integration/proctected_branch_test.go diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index c2ffda5eb7..965b748ac9 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -54,6 +54,8 @@ var migrations = []*Migration{ NewMigration("Add the `enable_repo_unit_hints` column to the `user` table", forgejo_v1_22.AddUserRepoUnitHintsSetting), // v7 -> v8 NewMigration("Modify the `release`.`note` content to remove SSH signatures", forgejo_v1_22.RemoveSSHSignaturesFromReleaseNotes), + // v8 -> v9 + NewMigration("Add the `apply_to_admins` column to the `protected_branch` table", forgejo_v1_22.AddApplyToAdminsSetting), } // GetCurrentDBVersion returns the current Forgejo database version. diff --git a/models/forgejo_migrations/v1_22/v9.go b/models/forgejo_migrations/v1_22/v9.go new file mode 100644 index 0000000000..34c2844c39 --- /dev/null +++ b/models/forgejo_migrations/v1_22/v9.go @@ -0,0 +1,15 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import "xorm.io/xorm" + +func AddApplyToAdminsSetting(x *xorm.Engine) error { + type ProtectedBranch struct { + ID int64 `xorm:"pk autoincr"` + ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync(&ProtectedBranch{}) +} diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index e0ff4d1542..a8b8c81bbe 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -58,6 +58,7 @@ type ProtectedBranch struct { RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` ProtectedFilePatterns string `xorm:"TEXT"` UnprotectedFilePatterns string `xorm:"TEXT"` + ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index e96d276b29..0b3b0bb030 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -47,6 +47,7 @@ type BranchProtection struct { RequireSignedCommits bool `json:"require_signed_commits"` ProtectedFilePatterns string `json:"protected_file_patterns"` UnprotectedFilePatterns string `json:"unprotected_file_patterns"` + ApplyToAdmins bool `json:"apply_to_admins"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -80,6 +81,7 @@ type CreateBranchProtectionOption struct { RequireSignedCommits bool `json:"require_signed_commits"` ProtectedFilePatterns string `json:"protected_file_patterns"` UnprotectedFilePatterns string `json:"unprotected_file_patterns"` + ApplyToAdmins bool `json:"apply_to_admins"` } // EditBranchProtectionOption options for editing a branch protection @@ -106,4 +108,5 @@ type EditBranchProtectionOption struct { RequireSignedCommits *bool `json:"require_signed_commits"` ProtectedFilePatterns *string `json:"protected_file_patterns"` UnprotectedFilePatterns *string `json:"unprotected_file_patterns"` + ApplyToAdmins *bool `json:"apply_to_admins"` } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 17a8180ec9..5042e5467e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2348,6 +2348,7 @@ settings.event_pull_request_review_request = Pull request review requested settings.event_pull_request_review_request_desc = Pull request review requested or review request removed. settings.event_pull_request_approvals = Pull request approvals settings.event_pull_request_merge = Pull request merge +settings.event_pull_request_enforcement = Enforcement settings.event_package = Package settings.event_package_desc = Package created or deleted in a repository. settings.branch_filter = Branch filter @@ -2462,6 +2463,8 @@ settings.block_on_official_review_requests = Block merge on official review requ settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals. settings.block_outdated_branch = Block merge if pull request is outdated settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch. +settings.enforce_on_admins = Enforce this rule for repository admins +settings.enforce_on_admins_desc = Repository admins cannot bypass this rule. settings.default_branch_desc = Select a default repository branch for pull requests and code commits: settings.merge_style_desc = Merge styles settings.default_merge_style_desc = Default merge style diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 5e6b6a8658..c33beee0ae 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -621,6 +621,7 @@ func CreateBranchProtection(ctx *context.APIContext) { ProtectedFilePatterns: form.ProtectedFilePatterns, UnprotectedFilePatterns: form.UnprotectedFilePatterns, BlockOnOutdatedBranch: form.BlockOnOutdatedBranch, + ApplyToAdmins: form.ApplyToAdmins, } err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ @@ -808,6 +809,10 @@ func EditBranchProtection(ctx *context.APIContext) { protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch } + if form.ApplyToAdmins != nil { + protectBranch.ApplyToAdmins = *form.ApplyToAdmins + } + var whitelistUsers []int64 if form.PushWhitelistUsernames != nil { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) diff --git a/routers/private/hook_pre_receive.go b/routers/private/hook_pre_receive.go index f45e57b9e3..0613492845 100644 --- a/routers/private/hook_pre_receive.go +++ b/routers/private/hook_pre_receive.go @@ -337,13 +337,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r return } - // If we're an admin for the repository we can ignore status checks, reviews and override protected files - if ctx.userPerm.IsAdmin() { - return - } - - // Now if we're not an admin - we can't overwrite protected files so fail now - if changedProtectedfiles { + // It's not allowed t overwrite protected files. Unless if the user is an + // admin and the protected branch rule doesn't apply to admins. + if changedProtectedfiles && (!ctx.user.IsAdmin || protectBranch.ApplyToAdmins) { log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) ctx.JSON(http.StatusForbidden, private.Response{ UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), @@ -352,8 +348,12 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r } // Check all status checks and reviews are ok - if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil { + if pb, err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil { if models.IsErrDisallowedToMerge(err) { + // Allow this if the rule doesn't apply to admins and the user is an admin. + if ctx.user.IsAdmin && !pb.ApplyToAdmins { + return + } log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error()) ctx.JSON(http.StatusForbidden, private.Response{ UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 7ee67e5925..25146779de 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -237,6 +237,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) { protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch + protectBranch.ApplyToAdmins = f.ApplyToAdmins err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ UserIDs: whitelistUsers, diff --git a/services/convert/convert.go b/services/convert/convert.go index ca3ec32a40..dd2239458e 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -162,6 +162,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api RequireSignedCommits: bp.RequireSignedCommits, ProtectedFilePatterns: bp.ProtectedFilePatterns, UnprotectedFilePatterns: bp.UnprotectedFilePatterns, + ApplyToAdmins: bp.ApplyToAdmins, Created: bp.CreatedUnix.AsTime(), Updated: bp.UpdatedUnix.AsTime(), } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 0f7665804d..b5ff031f4b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -219,6 +219,7 @@ type ProtectBranchForm struct { RequireSignedCommits bool ProtectedFilePatterns string UnprotectedFilePatterns string + ApplyToAdmins bool } // Validate validates the fields diff --git a/services/pull/check.go b/services/pull/check.go index f4dd332b14..9aab3c94f3 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -104,7 +104,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce return ErrIsChecking } - if err := CheckPullBranchProtections(ctx, pr, false); err != nil { + if pb, err := CheckPullBranchProtections(ctx, pr, false); err != nil { if !models.IsErrDisallowedToMerge(err) { log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err) return err @@ -117,8 +117,9 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce err = nil } - // * if the doer is admin, they could skip the branch protection check - if adminSkipProtectionCheck { + // * if the doer is admin, they could skip the branch protection check, + // if that's allowed by the protected branch rule. + if adminSkipProtectionCheck && !pb.ApplyToAdmins { if isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); errCheckAdmin != nil { log.Error("Unable to check if %-v is a repo admin in %-v: %v", doer, pr.BaseRepo, errCheckAdmin) return errCheckAdmin diff --git a/services/pull/merge.go b/services/pull/merge.go index df8d66e2d4..7f79eca2aa 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -424,63 +424,64 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a return false, nil } -// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks) -func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) { +// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks). +// Returns the protected branch rule when `ErrDisallowedToMerge` is returned as error. +func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (protectedBranchRule *git_model.ProtectedBranch, err error) { if err = pr.LoadBaseRepo(ctx); err != nil { - return fmt.Errorf("LoadBaseRepo: %w", err) + return nil, fmt.Errorf("LoadBaseRepo: %w", err) } pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) if err != nil { - return fmt.Errorf("LoadProtectedBranch: %v", err) + return nil, fmt.Errorf("LoadProtectedBranch: %v", err) } if pb == nil { - return nil + return nil, nil } isPass, err := IsPullCommitStatusPass(ctx, pr) if err != nil { - return err + return nil, err } if !isPass { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "Not all required status checks successful", } } if !issues_model.HasEnoughApprovals(ctx, pb, pr) { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "Does not have enough approvals", } } if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "There are requested changes", } } if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "There are official review requests", } } if issues_model.MergeBlockedByOutdatedBranch(pb, pr) { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "The head branch is behind the base branch", } } if skipProtectedFilesCheck { - return nil + return nil, nil } if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) { - return models.ErrDisallowedToMerge{ + return pb, models.ErrDisallowedToMerge{ Reason: "Changed protected files", } } - return nil + return nil, nil } // MergedManually mark pr as merged manually diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 19f1d3f91d..08f666d210 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -158,7 +158,7 @@ {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}} {{/* admin can merge without checks, writer can merge when checks succeed */}} - {{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + {{$canMergeNow := and (or (and $.IsRepoAdmin (not .ProtectedBranch.ApplyToAdmins)) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} {{/* admin and writer both can make an auto merge schedule */}} {{if $canMergeNow}} diff --git a/templates/repo/settings/protected_branch.tmpl b/templates/repo/settings/protected_branch.tmpl index e95dd831c9..e1ee7b36f5 100644 --- a/templates/repo/settings/protected_branch.tmpl +++ b/templates/repo/settings/protected_branch.tmpl @@ -260,6 +260,14 @@

{{ctx.Locale.Tr "repo.settings.block_outdated_branch_desc"}}

+
{{ctx.Locale.Tr "repo.settings.event_pull_request_enforcement"}}
+
+
+ + +

{{ctx.Locale.Tr "repo.settings.enforce_on_admins_desc"}}

+
+
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 6034a8fbce..ee1ed64eaf 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17756,6 +17756,10 @@ "description": "BranchProtection represents a branch protection for a repository", "type": "object", "properties": { + "apply_to_admins": { + "type": "boolean", + "x-go-name": "ApplyToAdmins" + }, "approvals_whitelist_teams": { "type": "array", "items": { @@ -18406,6 +18410,10 @@ "description": "CreateBranchProtectionOption options for creating a branch protection", "type": "object", "properties": { + "apply_to_admins": { + "type": "boolean", + "x-go-name": "ApplyToAdmins" + }, "approvals_whitelist_teams": { "type": "array", "items": { @@ -19577,6 +19585,10 @@ "description": "EditBranchProtectionOption options for editing a branch protection", "type": "object", "properties": { + "apply_to_admins": { + "type": "boolean", + "x-go-name": "ApplyToAdmins" + }, "approvals_whitelist_teams": { "type": "array", "items": { diff --git a/tests/integration/proctected_branch_test.go b/tests/integration/proctected_branch_test.go new file mode 100644 index 0000000000..9c6e5e3cae --- /dev/null +++ b/tests/integration/proctected_branch_test.go @@ -0,0 +1,87 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "strconv" + "strings" + "testing" + + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestProtectedBranch_AdminEnforcement(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "add-readme", "README.md", "WIP") + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 1, Name: "repo1"}) + + req := NewRequestWithValues(t, "POST", "user1/repo1/compare/master...add-readme", map[string]string{ + "_csrf": GetCSRF(t, session, "user1/repo1/compare/master...add-readme"), + "title": "pull request", + }) + session.MakeRequest(t, req, http.StatusOK) + + t.Run("No protected branch", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req = NewRequest(t, "GET", "/user1/repo1/pulls/1") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + text := strings.TrimSpace(doc.doc.Find(".merge-section").Text()) + assert.Contains(t, text, "This pull request can be merged automatically.") + assert.Contains(t, text, "'canMergeNow': true") + }) + + t.Run("Without admin enforcement", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithValues(t, "POST", "/user1/repo1/settings/branches/edit", map[string]string{ + "_csrf": GetCSRF(t, session, "/user1/repo1/settings/branches/edit"), + "rule_name": "master", + "required_approvals": "1", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + req = NewRequest(t, "GET", "/user1/repo1/pulls/1") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + text := strings.TrimSpace(doc.doc.Find(".merge-section").Text()) + assert.Contains(t, text, "This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.") + assert.Contains(t, text, "'canMergeNow': true") + }) + + t.Run("With admin enforcement", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + protectedBranch := unittest.AssertExistsAndLoadBean(t, &git_model.ProtectedBranch{RuleName: "master", RepoID: repo.ID}) + req := NewRequestWithValues(t, "POST", "/user1/repo1/settings/branches/edit", map[string]string{ + "_csrf": GetCSRF(t, session, "/user1/repo1/settings/branches/edit"), + "rule_name": "master", + "rule_id": strconv.FormatInt(protectedBranch.ID, 10), + "required_approvals": "1", + "apply_to_admins": "true", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + req = NewRequest(t, "GET", "/user1/repo1/pulls/1") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + text := strings.TrimSpace(doc.doc.Find(".merge-section").Text()) + assert.Contains(t, text, "This pull request doesn't have enough approvals yet. 0 of 1 approvals granted.") + assert.Contains(t, text, "'canMergeNow': false") + }) + }) +} From 39b53ef56f5d0d0cef6ff5a9a1204b04eb7f05e7 Mon Sep 17 00:00:00 2001 From: 0ko <0ko@noreply.codeberg.org> Date: Tue, 19 Mar 2024 16:27:01 +0500 Subject: [PATCH 025/565] Provide plural support for issue participants --- options/locale/locale_ar.ini | 2 +- options/locale/locale_bg.ini | 2 +- options/locale/locale_cs-CZ.ini | 2 +- options/locale/locale_de-DE.ini | 2 +- options/locale/locale_el-GR.ini | 2 +- options/locale/locale_en-US.ini | 3 ++- options/locale/locale_es-ES.ini | 2 +- options/locale/locale_fa-IR.ini | 2 +- options/locale/locale_fi-FI.ini | 2 +- options/locale/locale_fr-FR.ini | 3 ++- options/locale/locale_hu-HU.ini | 2 +- options/locale/locale_id-ID.ini | 2 +- options/locale/locale_it-IT.ini | 2 +- options/locale/locale_ja-JP.ini | 2 +- options/locale/locale_ko-KR.ini | 2 +- options/locale/locale_lv-LV.ini | 2 +- options/locale/locale_nl-NL.ini | 2 +- options/locale/locale_pl-PL.ini | 2 +- options/locale/locale_pt-BR.ini | 2 +- options/locale/locale_pt-PT.ini | 2 +- options/locale/locale_ru-RU.ini | 3 ++- options/locale/locale_si-LK.ini | 2 +- options/locale/locale_sv-SE.ini | 2 +- options/locale/locale_tr-TR.ini | 2 +- options/locale/locale_uk-UA.ini | 2 +- options/locale/locale_zh-CN.ini | 2 +- options/locale/locale_zh-HK.ini | 2 +- options/locale/locale_zh-TW.ini | 2 +- templates/repo/issue/view_content/sidebar.tmpl | 2 +- 29 files changed, 32 insertions(+), 29 deletions(-) diff --git a/options/locale/locale_ar.ini b/options/locale/locale_ar.ini index 38e18bec5a..4e0c617907 100644 --- a/options/locale/locale_ar.ini +++ b/options/locale/locale_ar.ini @@ -680,7 +680,7 @@ issues.self_assign_at = `ูƒู„ู‘ู ู†ูุณู‡ ุจู‡ุง %s` issues.label_deletion = ุงุญุฐู ุงู„ุชุตู†ูŠู issues.filter_milestone_all = ูƒู„ ุงู„ุฃู‡ุฏุงู issues.unlock.notice_2 = - ูŠู…ูƒู†ูƒ ุฏูˆู…ุง ุฅู‚ูุงู„ ู‡ุฐู‡ ุงู„ู…ุณุฃู„ุฉ ู…ู† ุฌุฏูŠุฏ ููŠ ุงู„ู…ุณุชู‚ุจู„. -issues.num_participants = %d ู…ุชุญุงูˆุฑ +issues.num_participants_few = %d ู…ุชุญุงูˆุฑ release.title = ุนู†ูˆุงู† ุงู„ุฅุตุฏุงุฑ issues.closed_at = `ุฃุบู„ู‚ ู‡ุฐู‡ ุงู„ู…ุณุฃู„ุฉ %[2]s` issues.lock.title = ุฅู‚ูุงู„ ุงู„ุชุญุงูˆุฑ ููŠ ู‡ุฐู‡ ุงู„ู…ุณุฃู„ุฉ. diff --git a/options/locale/locale_bg.ini b/options/locale/locale_bg.ini index 5b9bb0715e..cea90012b1 100644 --- a/options/locale/locale_bg.ini +++ b/options/locale/locale_bg.ini @@ -629,7 +629,7 @@ issues.filter_milestone_all = ะ’ัะธั‡ะบะธ ะตั‚ะฐะฟะธ issues.filter_milestone_open = ะžั‚ะฒะพั€ะตะฝะธ ะตั‚ะฐะฟะธ issues.filter_milestone_none = ะ‘ะตะท ะตั‚ะฐะฟะธ issues.filter_project = ะŸั€ะพะตะบั‚ -issues.num_participants = %d ัƒั‡ะฐัั‚ะฒะฐั‰ะธ +issues.num_participants_few = %d ัƒั‡ะฐัั‚ะฒะฐั‰ะธ issues.filter_assignee = ะ˜ะทะฟัŠะปะฝะธั‚ะตะป issues.filter_milestone_closed = ะ—ะฐั‚ะฒะพั€ะตะฝะธ ะตั‚ะฐะฟะธ issues.filter_assginee_no_select = ะ’ัะธั‡ะบะธ ะธะทะฟัŠะปะฝะธั‚ะตะปะธ diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 3747ae28c9..03ee22944f 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1603,7 +1603,7 @@ issues.label.filter_sort.alphabetically=Od zaฤรกtku abecedy issues.label.filter_sort.reverse_alphabetically=Od konce abecedy issues.label.filter_sort.by_size=Nejmenลกรญ velikost issues.label.filter_sort.reverse_by_size=Nejvฤ›tลกรญ velikost -issues.num_participants=%d รบฤastnรญkลฏ +issues.num_participants_few=%d รบฤastnรญkลฏ issues.attachment.open_tab=`Kliknฤ›te pro zobrazenรญ โ€ž%sโ€œ v novรฉ zรกloลพce` issues.attachment.download=`Kliknฤ›te pro staลพenรญ โ€ž%sโ€œ` issues.subscribe=Odebรญrat diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index f5aafdf5c9..1bdf6c4d0b 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1596,7 +1596,7 @@ issues.label.filter_sort.alphabetically=Alphabetisch issues.label.filter_sort.reverse_alphabetically=Umgekehrt alphabetisch issues.label.filter_sort.by_size=Kleinste GrรถรŸe issues.label.filter_sort.reverse_by_size=GrรถรŸte GrรถรŸe -issues.num_participants=%d Beteiligte +issues.num_participants_few=%d Beteiligte issues.attachment.open_tab=`Klicken, um โ€ž%sโ€œ in einem neuen Tab zu รถffnen` issues.attachment.download=`Klicken, um โ€ž%sโ€œ herunterzuladen` issues.subscribe=Abonnieren diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 36b7518c62..17362a6036 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1580,7 +1580,7 @@ issues.label.filter_sort.alphabetically=ฮ‘ฮปฯ†ฮฑฮฒฮทฯ„ฮนฮบฮฌ issues.label.filter_sort.reverse_alphabetically=ฮ‘ฮฝฯ„ฮฏฯƒฯ„ฯฮฟฯ†ฮฑ ฮฑฮปฯ†ฮฑฮฒฮทฯ„ฮนฮบฮฌ issues.label.filter_sort.by_size=ฮœฮนฮบฯฯŒฯ„ฮตฯฮฟ ฮผฮญฮณฮตฮธฮฟฯ‚ issues.label.filter_sort.reverse_by_size=ฮœฮตฮณฮฑฮปฯฯ„ฮตฯฮฟ ฮผฮญฮณฮตฮธฮฟฯ‚ -issues.num_participants=%d ฮฃฯ…ฮผฮผฮตฯ„ฮญฯ‡ฮฟฮฝฯ„ฮตฯ‚ +issues.num_participants_few=%d ฮฃฯ…ฮผฮผฮตฯ„ฮญฯ‡ฮฟฮฝฯ„ฮตฯ‚ issues.attachment.open_tab=`ฮ ฮฑฯ„ฮฎฯƒฯ„ฮต ฮตฮดฯŽ ฮณฮนฮฑ ฮฝฮฑ ฮฑฮฝฮฟฮฏฮพฮตฯ„ฮต ฯ„ฮฟ ยซ%sยป ฯƒฮต ฮผฮนฮฑ ฮฝฮญฮฑ ฮบฮฑฯฯ„ฮญฮปฮฑ` issues.attachment.download=`ฮ ฮฑฯ„ฮฎฯƒฯ„ฮต ฮตฮดฯŽ ฮณฮนฮฑ ฮฝฮฑ ฮบฮฑฯ„ฮตฮฒฮฌฯƒฮตฯ„ฮต ฯ„ฮฟ ยซ%sยป` issues.subscribe=ฮ•ฮณฮณฯฮฑฯ†ฮฎ diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 17a8180ec9..bb7c716e49 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1636,7 +1636,8 @@ issues.label.filter_sort.alphabetically = Alphabetically issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically issues.label.filter_sort.by_size = Smallest size issues.label.filter_sort.reverse_by_size = Largest size -issues.num_participants = %d participants +issues.num_participants_one = %d participant +issues.num_participants_few = %d participants issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` issues.subscribe = Subscribe diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 3dbca87381..78e06b8f09 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1561,7 +1561,7 @@ issues.label.filter_sort.alphabetically=Alfabรฉticamente issues.label.filter_sort.reverse_alphabetically=Invertir alfabรฉticamente issues.label.filter_sort.by_size=Tamaรฑo mรกs pequeรฑo issues.label.filter_sort.reverse_by_size=Tamaรฑo mรกs grande -issues.num_participants=%d participantes +issues.num_participants_few=%d participantes issues.attachment.open_tab='Haga clic para ver "%s" en una pestaรฑa nueva' issues.attachment.download=`Haga clic para descargar "%s"` issues.subscribe=Suscribir diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 598c1636dc..7f394176d0 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1189,7 +1189,7 @@ issues.label.filter_sort.alphabetically=ุงู„ูุจุงŒŒ issues.label.filter_sort.reverse_alphabetically=ุจุฑุนฺฉุณ ุชุฑุชŒุจ ุงู„ูุจุง issues.label.filter_sort.by_size=ฺฉูˆฺ†ฺฉุชุฑŒู† ุงู†ุฏุงุฒู‡ issues.label.filter_sort.reverse_by_size=ุจุฒุฑฺฏุชุฑŒู† ุงู†ุฏุงุฒู‡ -issues.num_participants=%d ู…ุดุงุฑฺฉุช ฺฉู†ู†ุฏู‡ +issues.num_participants_few=%d ู…ุดุงุฑฺฉุช ฺฉู†ู†ุฏู‡ issues.attachment.open_tab=ุจุฑุงŒ ู…ุดุงู‡ุฏู‡ "%s" ุฏุฑ ุฒุจุงู†ู‡ ุฌุฏŒุฏุŒ ฺฉู„Œฺฉ ฺฉู†Œุฏ issues.attachment.download=`ุจุฑุงŒ ุฏุฑŒุงูุช "%s" ฺฉู„Œฺฉ ฺฉู†Œุฏ` issues.subscribe=ู…ุดุชุฑฺฉ ุดุฏู† diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index c6c64ad6ce..60b6e93f00 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -920,7 +920,7 @@ issues.label.filter_sort.alphabetically=Aakkosjรคrjestyksessรค issues.label.filter_sort.reverse_alphabetically=Kรครคnteisessรค aakkosjรคrjestyksessรค issues.label.filter_sort.by_size=Pienin koko issues.label.filter_sort.reverse_by_size=Suurin koko -issues.num_participants=%d osallistujaa +issues.num_participants_few=%d osallistujaa issues.subscribe=Tilaa issues.unsubscribe=Lopeta tilaus issues.lock=Lukitse keskustelu diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 659615e6ff..fe527fe3d3 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1599,7 +1599,7 @@ issues.label.filter_sort.alphabetically=Par ordre alphabรฉtique issues.label.filter_sort.reverse_alphabetically=Par ordre alphabรฉtique inversรฉ issues.label.filter_sort.by_size=Plus petite taille issues.label.filter_sort.reverse_by_size=Plus grande taille -issues.num_participants=%d participants +issues.num_participants_few=%d participants issues.attachment.open_tab=`Cliquez ici pour voir ยซย %sย ยป dans un nouvel onglet.` issues.attachment.download=`Cliquez pour tรฉlรฉcharger ยซย %sย ยป.` issues.subscribe=Sโ€™abonner @@ -3754,6 +3754,7 @@ component_failed_to_load = Une erreur inattendue s'est produite. contributors.what = contributions component_loading = Chargement %s... component_loading_failed = ร‰chec de chargement de %s + code_frequency.what = fล•equence de code recent_commits.what = commits rรฉcents diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 3764f6fa61..5be3fa9c6f 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -857,7 +857,7 @@ issues.label.filter_sort.alphabetically=Betลฑrendben issues.label.filter_sort.reverse_alphabetically=Fordรญtott betลฑrendben issues.label.filter_sort.by_size=Legkisebb mรฉret issues.label.filter_sort.reverse_by_size=Legnagyobb mรฉret -issues.num_participants=%d Rรฉsztvevล‘ +issues.num_participants_few=%d Rรฉsztvevล‘ issues.attachment.open_tab=`A(z) "%s" megnyitรกsa รบj fรผlรถn` issues.attachment.download=`Kattintson a(z) "%s" letรถltรฉsรฉhez` issues.subscribe=Feliratkozรกs diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 574063bcaa..3ab9991f4a 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -729,7 +729,7 @@ issues.label_edit=Sunting issues.label_delete=Hapus issues.label.filter_sort.alphabetically=Urutan abjad issues.label.filter_sort.reverse_alphabetically=Membalikkan menurut abjad -issues.num_participants=%d peserta +issues.num_participants_few=%d peserta issues.attachment.open_tab=`Klik untuk melihat "%s" di tab baru` issues.attachment.download=`Klik untuk mengunduh "%s"` issues.subscribe=Berlangganan diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index dc6f4c127e..cb30d51e2f 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1478,7 +1478,7 @@ issues.label.filter_sort.alphabetically=In ordine alfabetico issues.label.filter_sort.reverse_alphabetically=In ordine alfabetico inverso issues.label.filter_sort.by_size=Dimensione piรน piccola issues.label.filter_sort.reverse_by_size=Dimensione piรน grande -issues.num_participants=%d partecipanti +issues.num_participants_few=%d partecipanti issues.attachment.open_tab=`Clicca per vedere "%s" in una nuova scheda` issues.attachment.download=`Clicca qui per scaricare "%s"` issues.subscribe=Iscriviti diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 7f3e1ded36..4774ef78cb 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1587,7 +1587,7 @@ issues.label.filter_sort.alphabetically=ใ‚ขใƒซใƒ•ใ‚กใƒ™ใƒƒใƒˆ้ † issues.label.filter_sort.reverse_alphabetically=้€†ใ‚ขใƒซใƒ•ใ‚กใƒ™ใƒƒใƒˆ้ † issues.label.filter_sort.by_size=ใ‚ตใ‚คใ‚บใฎๅฐใ•ใ„้ † issues.label.filter_sort.reverse_by_size=ใ‚ตใ‚คใ‚บใฎๅคงใใ„้ † -issues.num_participants=%d ไบบใฎๅ‚ๅŠ ่€… +issues.num_participants_few=%d ไบบใฎๅ‚ๅŠ ่€… issues.attachment.open_tab=`ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆๆ–ฐใ—ใ„ใ‚ฟใƒ–ใง "%s" ใ‚’่ฆ‹ใ‚‹` issues.attachment.download=`ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆ "%s" ใ‚’ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰` issues.subscribe=่ณผ่ชญใ™ใ‚‹ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index fa6df2d2a1..0903f5eb3e 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -782,7 +782,7 @@ issues.label_deletion_desc=๋ผ๋ฒจ์„ ์‚ญ์ œํ•˜๋ฉด ๋ชจ๋“  ์ด์Šˆ๋กœ๋ถ€ํ„ฐ๋„ ์‚ญ issues.label_deletion_success=๋ผ๋ฒจ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. issues.label.filter_sort.alphabetically=์•ŒํŒŒ๋ฒณ์ˆœ issues.label.filter_sort.reverse_alphabetically=์ด๋ฆ„ ์—ญ์ˆœ์œผ๋กœ ์ •๋ ฌ -issues.num_participants=์ฐธ์—ฌ์ž %d๋ช… +issues.num_participants_few=์ฐธ์—ฌ์ž %d๋ช… issues.attachment.open_tab=`ํด๋ฆญํ•˜์—ฌ "%s" ์ƒˆํƒญ์œผ๋กœ ๋ณด๊ธฐ` issues.attachment.download=' "%s"๋ฅผ ๋‹ค์šด๋กœ๋“œ ํ•˜๋ ค๋ฉด ํด๋ฆญ ํ•˜์‹ญ์‹œ์˜ค ' issues.subscribe=๊ตฌ๋…ํ•˜๊ธฐ diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index ce8f05e4b5..587143c9c3 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1540,7 +1540,7 @@ issues.label.filter_sort.alphabetically=Alfabฤ“tiski issues.label.filter_sort.reverse_alphabetically=Pretฤ“ji alfabฤ“tiski issues.label.filter_sort.by_size=Mazฤkais izmฤ“rs issues.label.filter_sort.reverse_by_size=Lielฤkais izmฤ“rs -issues.num_participants=%d dalฤซbnieki +issues.num_participants_few=%d dalฤซbnieki issues.attachment.open_tab=`Noklikลกฤทiniet, lai apskatฤซtos "%s" jaunฤ logฤ` issues.attachment.download=`Noklikลกฤทiniet, lai lejupielฤdฤ“tu "%s"` issues.subscribe=Abonฤ“t diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 4358cc1e81..d5071d98bf 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1482,7 +1482,7 @@ issues.label.filter_sort.alphabetically=Alfabetisch issues.label.filter_sort.reverse_alphabetically=Omgekeerd alfabetisch issues.label.filter_sort.by_size=Kleinste grootte issues.label.filter_sort.reverse_by_size=Grootste grootte -issues.num_participants=%d deelnemers +issues.num_participants_few=%d deelnemers issues.attachment.open_tab=`Klik om "%s" in een nieuw tabblad te bekijken` issues.attachment.download=`Klik om "%s" te downloaden` issues.subscribe=Abonneren diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index a254a912bd..f23f68f7c8 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1179,7 +1179,7 @@ issues.label.filter_sort.alphabetically=Alfabetycznie issues.label.filter_sort.reverse_alphabetically=Alfabetycznie odwrotnie issues.label.filter_sort.by_size=Najmniejszy rozmiar issues.label.filter_sort.reverse_by_size=Najwiฤ™kszy rozmiar -issues.num_participants=Uczestnicy %d +issues.num_participants_few=Uczestnicy %d issues.attachment.open_tab=`Kliknij, aby zobaczyฤ‡ "%s" w nowej karcie` issues.attachment.download=`Kliknij, aby pobraฤ‡ "%s"` issues.subscribe=Subskrybuj diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 086cea74fa..ac30ff3e74 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1552,7 +1552,7 @@ issues.label.filter_sort.alphabetically=Alfabeticamente issues.label.filter_sort.reverse_alphabetically=Alfabeticamente inverso issues.label.filter_sort.by_size=Menor tamanho issues.label.filter_sort.reverse_by_size=Maior tamanho -issues.num_participants=%d participante(s) +issues.num_participants_few=%d participante(s) issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba` issues.attachment.download=`Clique para baixar "%s"` issues.subscribe=Inscrever-se diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 3538ac9460..09b3fc3153 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1548,7 +1548,7 @@ issues.label.filter_sort.alphabetically=por ordem alfabรฉtica issues.label.filter_sort.reverse_alphabetically=por ordem alfabรฉtica inversa issues.label.filter_sort.by_size=Menor tamanho issues.label.filter_sort.reverse_by_size=Maior tamanho -issues.num_participants=%d Participantes +issues.num_participants_few=%d Participantes issues.attachment.open_tab=`Clique para ver "%s" num separador novo` issues.attachment.download=`Clique para descarregar "%s"` issues.subscribe=Subscrever diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index addc2f8110..34321b18f9 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1581,7 +1581,8 @@ issues.label.filter_sort.alphabetically=ะŸะพ ะฐะปั„ะฐะฒะธั‚ัƒ issues.label.filter_sort.reverse_alphabetically=ะก ะบะพะฝั†ะฐ ะฐะปั„ะฐะฒะธั‚ะฐ issues.label.filter_sort.by_size=ะœะตะฝัŒัˆะธะน ั€ะฐะทะผะตั€ issues.label.filter_sort.reverse_by_size=ะ‘ะพะปัŒัˆะธะน ั€ะฐะทะผะตั€ -issues.num_participants=%d ัƒั‡ะฐัั‚ะฒัƒัŽั‰ะธั… +issues.num_participants_one=%d ัƒั‡ะฐัั‚ะฝะธะบ +issues.num_participants_few=%d ัƒั‡ะฐัั‚ะฝะธะบะพะฒ issues.attachment.open_tab=`ะะฐะถะผะธั‚ะต, ั‡ั‚ะพะฑั‹ ัƒะฒะธะดะตั‚ัŒ ยซ%sยป ะฒ ะฝะพะฒะพะน ะฒะบะปะฐะดะบะต` issues.attachment.download=`ะะฐะถะผะธั‚ะต, ั‡ั‚ะพะฑั‹ ัะบะฐั‡ะฐั‚ัŒ ยซ%sยป` issues.subscribe=ะŸะพะดะฟะธัะฐั‚ัŒัั diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 7cb1768d22..ac837173e4 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -1132,7 +1132,7 @@ issues.label.filter_sort.alphabetically=เถ…เถšเทเถปเทเถฏเท“ issues.label.filter_sort.reverse_alphabetically=เถ…เถšเทเถปเทเถฏเท“ เถดเทŠเถปเถญเท’เท€เท’เถšเท”เถซเท”เถธเทŠ issues.label.filter_sort.by_size=เถšเท”เถฉเทเถธ เถดเทŠโ€เถปเถธเทเถซเถบ issues.label.filter_sort.reverse_by_size=เท€เท’เทเทเถฝเถธ เถดเทŠโ€เถปเถธเทเถซเถบ -issues.num_participants=เทƒเท„เถทเทเถœเท’เท€เถฑเทŠเถฑเถฑเทŠ %d +issues.num_participants_few=เทƒเท„เถทเทเถœเท’เท€เถฑเทŠเถฑเถฑเทŠ %d issues.attachment.open_tab=`เถฑเท€ เท€เถœเท”เท€เถš "%s" เถถเทเถฝเท“เถธเถง เถšเทŠเถฝเท’เถšเทŠ เถšเถปเถฑเทŠเถฑ` issues.attachment.download=`"%s" เถถเทเถœเทเถฑเท“เถธเถง เถ”เถถเถฑเทŠเถฑ` issues.subscribe=เถฏเทเถบเถš เท€เถฑเทŠเถฑ diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 38382a6f66..e7cb248aaa 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -972,7 +972,7 @@ issues.label.filter_sort.alphabetically=Alfabetiskt A-ร– issues.label.filter_sort.reverse_alphabetically=Alfabetiskt ร–-A issues.label.filter_sort.by_size=Minsta storlek issues.label.filter_sort.reverse_by_size=Stรถrsta storlek -issues.num_participants=%d Deltagare +issues.num_participants_few=%d Deltagare issues.attachment.open_tab=`Klicka fรถr att se "%s" i en ny flik` issues.attachment.download=`Klicka fรถr att hรคmta "%s"` issues.subscribe=Prenumerera diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 74ef77eb19..fa8a1b687f 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1539,7 +1539,7 @@ issues.label.filter_sort.alphabetically=Alfabetik issues.label.filter_sort.reverse_alphabetically=Ters alfabetik issues.label.filter_sort.by_size=En kรผรงรผk boyut issues.label.filter_sort.reverse_by_size=En bรผyรผk boyut -issues.num_participants=%d Katฤฑlฤฑmcฤฑ +issues.num_participants_few=%d Katฤฑlฤฑmcฤฑ issues.attachment.open_tab=`Yeni bir sekmede "%s" gรถrmek iรงin tฤฑkla` issues.attachment.download=`"%s" indirmek iรงin tฤฑkla` issues.subscribe=Abone Ol diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 0a79e54010..d1040ac2b5 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1248,7 +1248,7 @@ issues.label.filter_sort.alphabetically=ะ—ะฐ ะฐะปั„ะฐะฒั–ั‚ะพะผ issues.label.filter_sort.reverse_alphabetically=ะ— ะบั–ะฝั†ั ะฐะปั„ะฐะฒั–ั‚ัƒ issues.label.filter_sort.by_size=ะะฐะนะผะตะฝัˆะธะน ั€ะพะทะผั–ั€ issues.label.filter_sort.reverse_by_size=ะะฐะนะฑั–ะปัŒัˆะธะน ั€ะพะทะผั–ั€ -issues.num_participants=%d ัƒั‡ะฐัะฝะธะบั–ะฒ +issues.num_participants_few=%d ัƒั‡ะฐัะฝะธะบั–ะฒ issues.attachment.open_tab=`ะะฐั‚ะธัะฝั–ั‚ัŒ ั‰ะพะฑ ะฟะพะฑะฐั‡ะธั‚ะธ "%s" ัƒ ะฝะพะฒั–ะน ะฒะบะปะฐะดั†ั–` issues.attachment.download=`ะะฐั‚ะธัะฝั–ั‚ัŒ ั‰ะพะฑ ะทะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ "%s"` issues.subscribe=ะŸั–ะดะฟะธัะฐั‚ะธัั diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 4ca2e70f21..b0e3b383b6 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1597,7 +1597,7 @@ issues.label.filter_sort.alphabetically=ๆŒ‰ๅญ—ๆฏ้กบๅบๆŽ’ๅบ issues.label.filter_sort.reverse_alphabetically=ๆŒ‰ๅญ—ๆฏ้€†ๅบๆŽ’ๅบ issues.label.filter_sort.by_size=ๆœ€ๅฐๅฐบๅฏธ issues.label.filter_sort.reverse_by_size=ๆœ€ๅคงๅฐบๅฏธ -issues.num_participants=%d ๅๅ‚ไธŽ่€… +issues.num_participants_few=%d ๅๅ‚ไธŽ่€… issues.attachment.open_tab=`ๅœจๆ–ฐ็š„ๆ ‡็ญพ้กตไธญๆŸฅ็œ‹ '%s'` issues.attachment.download=`็‚นๅ‡ปไธ‹่ฝฝ '%s'` issues.subscribe=่ฎข้˜… diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 5c1e234392..ab0a075d42 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -467,7 +467,7 @@ issues.label_edit=็ทจ่ผฏ issues.label_delete=ๅˆช้™ค issues.label.filter_sort.alphabetically=ๆŒ‰ๅญ—ๆฏ้กบๅบๆŽ’ๅบ issues.label.filter_sort.reverse_alphabetically=ๆŒ‰ๅญ—ๆฏๅๅ‘ๆŽ’ๅบ -issues.num_participants=%d ๅƒ่ˆ‡่€… +issues.num_participants_few=%d ๅƒ่ˆ‡่€… issues.attachment.open_tab=`ๅœจๆ–ฐ็š„ๆจ™็ฑค้ ไธญๆŸฅ็œ‹ '%s'` issues.attachment.download=`้ปžๆ“Šไธ‹่ผ‰ '%s'` issues.subscribe=่จ‚้–ฑ diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 5bfff77fd2..25e98ed025 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1433,7 +1433,7 @@ issues.label.filter_sort.alphabetically=ๆŒ‰ๅญ—ๆฏ้ †ๅบๆŽ’ๅบ issues.label.filter_sort.reverse_alphabetically=ๆŒ‰ๅญ—ๆฏๅๅ‘ๆŽ’ๅบ issues.label.filter_sort.by_size=ๆช”ๆกˆ็”ฑๅฐๅˆฐๅคง issues.label.filter_sort.reverse_by_size=ๆช”ๆกˆ็”ฑๅคงๅˆฐๅฐ -issues.num_participants=%d ๅƒ่ˆ‡่€… +issues.num_participants_few=%d ๅƒ่ˆ‡่€… issues.attachment.open_tab=`ๅœจๆ–ฐๅˆ†้ ไธญๆŸฅ็œ‹ใ€Œ%sใ€` issues.attachment.download=`้ปžๆ“Šไธ‹่ผ‰ใ€Œ%sใ€` issues.subscribe=่จ‚้–ฑ diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 1414ac45ee..badad6ac47 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -263,7 +263,7 @@
{{if .Participants}} - {{ctx.Locale.Tr "repo.issues.num_participants" .NumParticipants}} + {{ctx.Locale.TrN .NumParticipants "repo.issues.num_participants_one" "repo.issues.num_participants_few" .NumParticipants}}
{{range .Participants}} From a517e4aeb1146aea392d6e7f5bdfe5ed8513a61d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 30 Mar 2024 06:05:07 +0000 Subject: [PATCH 026/565] Update module github.com/minio/minio-go/v7 to v7.0.69 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5c39436eca..db3560d3fb 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/meilisearch/meilisearch-go v0.26.1 github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.26 - github.com/minio/minio-go/v7 v7.0.66 + github.com/minio/minio-go/v7 v7.0.69 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.52 github.com/niklasfasching/go-org v1.7.0 diff --git a/go.sum b/go.sum index 5ab9cfc5f9..44d5f1a268 100644 --- a/go.sum +++ b/go.sum @@ -614,8 +614,8 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= -github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= +github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= +github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= From 02dcd07437c49430e4e858c36c12a954112ff1f1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 30 Mar 2024 10:11:45 +0000 Subject: [PATCH 027/565] Update module github.com/felixge/fgprof to v0.9.4 --- go.mod | 4 ++-- go.sum | 24 +++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index cc3528800d..41acfdcf39 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/editorconfig/editorconfig-core-go/v2 v2.6.1 github.com/emersion/go-imap v1.2.1 github.com/emirpasic/gods v1.18.1 - github.com/felixge/fgprof v0.9.3 + github.com/felixge/fgprof v0.9.4 github.com/fsnotify/fsnotify v1.7.0 github.com/gliderlabs/ssh v0.3.7 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 @@ -53,7 +53,7 @@ require ( github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/go-github/v57 v57.0.0 - github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 + github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.1.2 github.com/gorilla/sessions v1.2.2 diff --git a/go.sum b/go.sum index b3fae2a8d8..3bc626042e 100644 --- a/go.sum +++ b/go.sum @@ -186,9 +186,15 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ= github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= @@ -256,8 +262,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= +github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -353,6 +359,9 @@ github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ= github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -441,9 +450,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8= -github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -494,7 +502,7 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -567,6 +575,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= @@ -670,6 +679,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= @@ -1034,9 +1044,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From ddc24177dd080229ab3b7d80a1231e59d270a064 Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 29 Mar 2024 20:41:13 +0100 Subject: [PATCH 028/565] [BUG] Render emojis in labels in issue info popup - Currently emojis that are part of the label's name aren't rendered when shown in the popup that you get when you hover over issue references. - This patch fixes that by rendering the emoji. - Adds CSS to not make the emoji big in the label. - Resolves #1531 --- package-lock.json | 171 ++++++++++++++++++++- package.json | 1 + web_src/css/repo.css | 4 + web_src/js/components/ContextPopup.test.js | 39 +++++ web_src/js/components/ContextPopup.vue | 18 +-- web_src/js/vitest.setup.js | 1 + 6 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 web_src/js/components/ContextPopup.test.js diff --git a/package-lock.json b/package-lock.json index 5358428509..3b2f868ec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "@stylistic/eslint-plugin-js": "1.7.0", "@stylistic/stylelint-plugin": "2.1.0", "@vitejs/plugin-vue": "5.0.4", + "@vue/test-utils": "2.4.5", "eslint": "8.57.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "4.10.2", @@ -1329,6 +1330,12 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2680,6 +2687,16 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, + "node_modules/@vue/test-utils": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz", + "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==", + "dev": true, + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -2862,6 +2879,15 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -3799,6 +3825,22 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, "node_modules/core-js-compat": { "version": "3.36.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", @@ -4775,6 +4817,48 @@ "marked": "^4.1.0" } }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.716", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz", @@ -7398,6 +7482,58 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/js-levenshtein-esm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", @@ -8801,6 +8937,21 @@ "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" }, + "node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -9140,11 +9291,11 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -9727,6 +9878,12 @@ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", "dev": true }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, "node_modules/proto-props": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/proto-props/-/proto-props-2.0.0.tgz", @@ -12089,6 +12246,12 @@ "vue": "^3.0.0-0 || ^2.7.0" } }, + "node_modules/vue-component-type-helpers": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.7.tgz", + "integrity": "sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==", + "dev": true + }, "node_modules/vue-eslint-parser": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", diff --git a/package.json b/package.json index b5bfda9dc6..7b40976968 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@stylistic/eslint-plugin-js": "1.7.0", "@stylistic/stylelint-plugin": "2.1.0", "@vitejs/plugin-vue": "5.0.4", + "@vue/test-utils": "2.4.5", "eslint": "8.57.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "4.10.2", diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 0dfd132a73..d28dc4b96d 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -3010,3 +3010,7 @@ tbody.commit-list { margin-top: -1px; border-top: 1px solid var(--color-secondary); } +#issue-info-popup .emoji { + font-size: inherit; + line-height: inherit; +} diff --git a/web_src/js/components/ContextPopup.test.js b/web_src/js/components/ContextPopup.test.js new file mode 100644 index 0000000000..1db6c38301 --- /dev/null +++ b/web_src/js/components/ContextPopup.test.js @@ -0,0 +1,39 @@ +import {mount, flushPromises} from '@vue/test-utils'; +import ContextPopup from './ContextPopup.vue'; + +test('renders a issue info popup', async () => { + const owner = 'user2'; + const repo = 'repo1'; + const index = 1; + vi.spyOn(global, 'fetch').mockResolvedValue({ + json: vi.fn().mockResolvedValue({ + ok: true, + created_at: '2023-09-30T19:00:00Z', + repository: {full_name: owner}, + pull_request: null, + state: 'open', + title: 'Normal issue', + body: 'Lorem ipsum...', + number: index, + labels: [{color: 'ee0701', name: "Bug :+1: "}], + }), + ok: true, + }); + + const wrapper = mount(ContextPopup); + wrapper.vm.$el.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}})); + await flushPromises(); + + // Header + expect(wrapper.get('p:nth-of-type(1)').text()).toEqual('user2 on Sep 30, 2023'); + // Title + expect(wrapper.get('p:nth-of-type(2)').text()).toEqual('Normal issue #1'); + // Body + expect(wrapper.get('p:nth-of-type(3)').text()).toEqual('Lorem ipsum...'); + // Check that the state is correct. + expect(wrapper.get('svg').classes()).toContain('octicon-issue-opened'); + // Ensure that script is not an element. + expect(() => wrapper.get('.evil')).toThrowError(); + // Check content of label + expect(wrapper.get('.ui.label').text()).toContain("Bug ๐Ÿ‘ "); +}); diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index d87eb1a180..ac6a8f3bb6 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -3,6 +3,8 @@ import {SvgIcon} from '../svg.js'; import {useLightTextOnBackground} from '../utils/color.js'; import tinycolor from 'tinycolor2'; import {GET} from '../modules/fetch.js'; +import {emojiHTML} from '../features/emoji.js'; +import {htmlEscape} from 'escape-goat'; const {appSubUrl, i18n} = window.config; @@ -67,6 +69,10 @@ export default { } else { textColor = '#111111'; } + label.name = htmlEscape(label.name); + label.name = label.name.replaceAll(/:[-+\w]+:/g, (emoji) => { + return emojiHTML(emoji.substring(1, emoji.length - 1)); + }); return {name: label.name, color: `#${label.color}`, textColor}; }); }, @@ -104,19 +110,13 @@ export default {