From 188e515efc2108a4d0b2d1155597518b2d61e8fa Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 15 Jun 2024 00:21:40 +0800 Subject: [PATCH 1/5] Fix repo graph JS (#31377) Fix #31376 Regression of #30395 --- web_src/js/features/repo-graph.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/features/repo-graph.js b/web_src/js/features/repo-graph.js index 0086b92021..689b6f1369 100644 --- a/web_src/js/features/repo-graph.js +++ b/web_src/js/features/repo-graph.js @@ -69,9 +69,9 @@ export function initRepoGraphGit() { const html = await response.text(); const div = document.createElement('div'); div.innerHTML = html; - document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML; - document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML; - document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML; + document.getElementById('pagination').innerHTML = div.querySelector('#pagination').innerHTML; + document.getElementById('rel-container').innerHTML = div.querySelector('#rel-container').innerHTML; + document.getElementById('rev-container').innerHTML = div.querySelector('#rev-container').innerHTML; hideElem('#loading-indicator'); showElem('#rel-container'); showElem('#rev-container'); From 52925e9c7c22163ea66729ebfc091292e8a22eee Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 15 Jun 2024 11:44:44 +0800 Subject: [PATCH 2/5] Fix duplicate sub-path for avatars (#31365) (#31368) Backport #31365, only backport necessary changes. --- models/repo/avatar_test.go | 28 +++++++++++++++++++++ models/user/avatar.go | 6 +++-- models/user/avatar_test.go | 28 +++++++++++++++++++++ modules/httplib/url.go | 28 +++++++++++++++------ modules/httplib/url_test.go | 10 ++++---- routers/api/packages/container/container.go | 2 +- 6 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 models/repo/avatar_test.go create mode 100644 models/user/avatar_test.go diff --git a/models/repo/avatar_test.go b/models/repo/avatar_test.go new file mode 100644 index 0000000000..fc1f8baeca --- /dev/null +++ b/models/repo/avatar_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRepoAvatarLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost/")() + defer test.MockVariableValue(&setting.AppSubURL, "")() + + repo := &Repository{ID: 1, Avatar: "avatar.png"} + link := repo.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link) + + setting.AppURL = "https://localhost/sub-path/" + setting.AppSubURL = "/sub-path" + link = repo.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link) +} diff --git a/models/user/avatar.go b/models/user/avatar.go index 921bc1b1a1..5453c78fc6 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size) } -// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment +// AvatarLink returns the full avatar url with http host. +// TODO: refactor it to a relative URL, but it is still used in API response at the moment func (u *User) AvatarLink(ctx context.Context) string { - return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0)) + relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty + return httplib.MakeAbsoluteURL(ctx, relLink) } // IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go new file mode 100644 index 0000000000..1078875ee1 --- /dev/null +++ b/models/user/avatar_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestUserAvatarLink(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://localhost/")() + defer test.MockVariableValue(&setting.AppSubURL, "")() + + u := &User{ID: 1, Avatar: "avatar.png"} + link := u.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/avatars/avatar.png", link) + + setting.AppURL = "https://localhost/sub-path/" + setting.AppSubURL = "/sub-path" + link = u.AvatarLink(db.DefaultContext) + assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link) +} diff --git a/modules/httplib/url.go b/modules/httplib/url.go index 8dc5b71181..219dfe695c 100644 --- a/modules/httplib/url.go +++ b/modules/httplib/url.go @@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string { return req.Header.Get("X-Forwarded-Host") } -// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL +// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL func GuessCurrentAppURL(ctx context.Context) string { + return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/" +} + +// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash. +func GuessCurrentHostURL(ctx context.Context) string { req, ok := ctx.Value(RequestContextKey).(*http.Request) if !ok { - return setting.AppURL + return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } // If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one. // At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong. @@ -74,20 +79,27 @@ func GuessCurrentAppURL(ctx context.Context) string { // So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL. reqScheme := getRequestScheme(req) if reqScheme == "" { - return setting.AppURL + return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") } reqHost := getForwardedHost(req) if reqHost == "" { reqHost = req.Host } - return reqScheme + "://" + reqHost + setting.AppSubURL + "/" + return reqScheme + "://" + reqHost } -func MakeAbsoluteURL(ctx context.Context, s string) string { - if IsRelativeURL(s) { - return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/") +// MakeAbsoluteURL tries to make a link to an absolute URL: +// * If link is empty, it returns the current app URL. +// * If link is absolute, it returns the link. +// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed. +func MakeAbsoluteURL(ctx context.Context, link string) string { + if link == "" { + return GuessCurrentAppURL(ctx) } - return s + if !IsRelativeURL(link) { + return link + } + return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/") } func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool { diff --git a/modules/httplib/url_test.go b/modules/httplib/url_test.go index 9980cb74e8..28aaee6e12 100644 --- a/modules/httplib/url_test.go +++ b/modules/httplib/url_test.go @@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) { ctx := context.Background() assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, "")) - assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo")) - assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "foo")) + assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", }) - assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", @@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) { "X-Forwarded-Host": {"forwarded-host"}, }, }) - assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo")) ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ Host: "user-host", @@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) { "X-Forwarded-Proto": {"https"}, }, }) - assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) + assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo")) } func TestIsCurrentGiteaSiteURL(t *testing.T) { diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index b0c4458d51..5007037bee 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -117,7 +117,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) { func apiUnauthorizedError(ctx *context.Context) { // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed - realmURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), setting.AppSubURL+"/") + "/v2/token" + realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token" ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`) apiErrorDefined(ctx, errUnauthorized) } From 3f44844244dd5e6329bbfaf29a3d5b4b244ab84f Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 16 Jun 2024 20:55:14 +0800 Subject: [PATCH 3/5] Allow downloading attachments of draft releases (#31369) (#31380) Backport #31369 by Zettat123 Fix #31362 Co-authored-by: Zettat123 --- routers/web/repo/repo.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index f54b35c3e0..c1eda8b674 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -418,8 +418,9 @@ func RedirectDownload(ctx *context.Context) { tagNames := []string{vTag} curRepo := ctx.Repo.Repository releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ - RepoID: curRepo.ID, - TagNames: tagNames, + IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases), + RepoID: curRepo.ID, + TagNames: tagNames, }) if err != nil { ctx.ServerError("RedirectDownload", err) From fa307167f97a185fefd58f016a96ccdf55783b1c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 17 Jun 2024 15:07:21 +0800 Subject: [PATCH 4/5] Fix missing images in editor preview due to wrong links (#31299) (#31393) Backport #31299 Parse base path and tree path so that media links can be correctly created with /media/. Resolves #31294 --------- Co-authored-by: Brecht Van Lommel --- modules/markup/renderer.go | 8 +-- modules/structs/miscellaneous.go | 6 +- routers/api/v1/misc/markup_test.go | 100 ++++++++++++++++++----------- routers/common/markup.go | 61 +++++++++--------- templates/swagger/v1_json.tmpl | 4 +- 5 files changed, 102 insertions(+), 77 deletions(-) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index f372dcd5b7..3284a8194e 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -84,10 +84,10 @@ type RenderContext struct { } type Links struct { - AbsolutePrefix bool - Base string - BranchPath string - TreePath string + AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias + Base string // base prefix for pre-provided links and medias (images, videos) + BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0" + TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered } func (l *Links) Prefix() string { diff --git a/modules/structs/miscellaneous.go b/modules/structs/miscellaneous.go index bff10f95b7..3b206c1dd7 100644 --- a/modules/structs/miscellaneous.go +++ b/modules/structs/miscellaneous.go @@ -25,7 +25,8 @@ type MarkupOption struct { // // in: body Mode string - // Context to render + // URL path for rendering issue, media and file links + // Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir} // // in: body Context string @@ -53,7 +54,8 @@ type MarkdownOption struct { // // in: body Mode string - // Context to render + // URL path for rendering issue, media and file links + // Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir} // // in: body Context string diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index 5236fd06ae..e2ab7141b7 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -7,6 +7,7 @@ import ( go_context "context" "io" "net/http" + "path" "strings" "testing" @@ -19,36 +20,40 @@ import ( "github.com/stretchr/testify/assert" ) -const ( - AppURL = "http://localhost:3000/" - Repo = "gogits/gogs" - FullURL = AppURL + Repo + "/" -) +const AppURL = "http://localhost:3000/" -func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { +func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) { setting.AppURL = AppURL + context := "/gogits/gogs" + if !wiki { + context += path.Join("/src/branch/main", path.Dir(filePath)) + } options := api.MarkupOption{ Mode: mode, Text: text, - Context: Repo, - Wiki: true, + Context: context, + Wiki: wiki, FilePath: filePath, } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") web.SetForm(ctx, &options) Markup(ctx) - assert.Equal(t, responseBody, resp.Body.String()) - assert.Equal(t, responseCode, resp.Code) + assert.Equal(t, expectedBody, resp.Body.String()) + assert.Equal(t, expectedCode, resp.Code) resp.Body.Reset() } -func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { +func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) { setting.AppURL = AppURL + context := "/gogits/gogs" + if !wiki { + context += "/src/branch/main" + } options := api.MarkdownOption{ Mode: mode, Text: text, - Context: Repo, - Wiki: true, + Context: context, + Wiki: wiki, } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") web.SetForm(ctx, &options) @@ -65,7 +70,7 @@ func TestAPI_RenderGFM(t *testing.T) { }, }) - testCasesCommon := []string{ + testCasesWiki := []string{ // dear imgui wiki markdown extract: special wiki syntax `Wiki! Enjoy :) - [[Links, Language bindings, Engine bindings|Links]] @@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) { // rendered `

Wiki! Enjoy :)

`, // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `

Guardfile-DSL / Configuring-Guard

+ `

Guardfile-DSL / Configuring-Guard

`, // special syntax `[[Name|Link]]`, // rendered - `

Name

+ `

Name

`, // empty ``, @@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) { ``, } - testCasesDocument := []string{ + testCasesWikiDocument := []string{ // wine-staging wiki home extract: special wiki syntax, images `## What is Wine Staging? **Wine Staging** on website [wine-staging.com](http://wine-staging.com). @@ -111,31 +116,48 @@ Here are some links to the most important topics. You can find the full list of

Wine Staging on website wine-staging.com.

Here are some links to the most important topics. You can find the full list of pages at the sidebar.

-

Configuration -images/icon-bug.png

+

Configuration +images/icon-bug.png

`, } - for i := 0; i < len(testCasesCommon); i += 2 { - text := testCasesCommon[i] - response := testCasesCommon[i+1] - testRenderMarkdown(t, "gfm", text, response, http.StatusOK) - testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) - testRenderMarkdown(t, "comment", text, response, http.StatusOK) - testRenderMarkup(t, "comment", "", text, response, http.StatusOK) - testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) + for i := 0; i < len(testCasesWiki); i += 2 { + text := testCasesWiki[i] + response := testCasesWiki[i+1] + testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK) + testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK) + testRenderMarkdown(t, "comment", true, text, response, http.StatusOK) + testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK) + testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK) } - for i := 0; i < len(testCasesDocument); i += 2 { - text := testCasesDocument[i] - response := testCasesDocument[i+1] - testRenderMarkdown(t, "gfm", text, response, http.StatusOK) - testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) - testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) + for i := 0; i < len(testCasesWikiDocument); i += 2 { + text := testCasesWikiDocument[i] + response := testCasesWikiDocument[i+1] + testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK) + testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK) + testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK) } - testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) - testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) + input := "[Link](test.md)\n![Image](image.png)" + testRenderMarkdown(t, "gfm", false, input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkdown(t, "gfm", false, input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "gfm", false, "", input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "file", false, "path/new-file.md", input, `

Link +Image

+`, http.StatusOK) + + testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) + testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) } var simpleCases = []string{ @@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) { options := api.MarkdownOption{ Mode: "markdown", Text: "", - Context: Repo, + Context: "/gogits/gogs", } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") for i := 0; i < len(simpleCases); i += 2 { diff --git a/routers/common/markup.go b/routers/common/markup.go index 2d5638ef61..242e9a3754 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -7,62 +7,66 @@ package common import ( "fmt" "net/http" + "path" "strings" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" - - "mvdan.cc/xurls/v2" ) // RenderMarkup renders markup text for the /markup and /markdown endpoints -func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) { - var markupType string - relativePath := "" +func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) { + // urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}" + // filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file") + // filePath will be used as RenderContext.RelativePath - if len(text) == 0 { - _, _ = ctx.Write([]byte("")) - return + // for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md" + // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc" + + var markupType, relativePath string + + links := markup.Links{AbsolutePrefix: true} + if urlPathContext != "" { + links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext) } switch mode { case "markdown": // Raw markdown if err := markdown.RenderRaw(&markup.RenderContext{ - Ctx: ctx, - Links: markup.Links{ - AbsolutePrefix: true, - Base: urlPrefix, - }, + Ctx: ctx, + Links: links, }, strings.NewReader(text), ctx.Resp); err != nil { ctx.Error(http.StatusInternalServerError, err.Error()) } return case "comment": - // Comment as markdown + // Issue & comment content markupType = markdown.MarkupName case "gfm": - // Github Flavored Markdown as document + // GitHub Flavored Markdown markupType = markdown.MarkupName case "file": - // File as document based on file extension - markupType = "" + markupType = "" // render the repo file content by its extension relativePath = filePath default: ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) return } - if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { - // check if urlPrefix is already set to a URL - linkRegex, _ := xurls.StrictMatchingScheme("https?://") - m := linkRegex.FindStringIndex(urlPrefix) - if m == nil { - urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) - } + fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5) + if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") { + // absolute base prefix is something like "https://host/subpath/{user}/{repo}" + absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1]) + + fileDir := path.Dir(filePath) // it is "doc" if filePath is "doc/CHANGE.md" + refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc" + refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12" + + links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir} } meta := map[string]string{} @@ -78,11 +82,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr } if err := markup.Render(&markup.RenderContext{ - Ctx: ctx, - Links: markup.Links{ - AbsolutePrefix: true, - Base: urlPrefix, - }, + Ctx: ctx, + Links: links, Metas: meta, IsWiki: wiki, Type: markupType, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 9ea6763a1a..fb117ea6cc 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -21954,7 +21954,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "Mode": { @@ -21977,7 +21977,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "FilePath": { From ed0fc0ec46466eb25e77cfefbf7f6d05f6f902b8 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 17 Jun 2024 15:41:47 +0800 Subject: [PATCH 5/5] Fix natural sort (#31384) (#31394) Backport #31384 by wxiaoguang Fix #31374 Co-authored-by: wxiaoguang --- modules/base/natural_sort.go | 57 ++++++++++++++++++++++++++++++- modules/base/natural_sort_test.go | 43 +++++++++++++++-------- 2 files changed, 85 insertions(+), 15 deletions(-) diff --git a/modules/base/natural_sort.go b/modules/base/natural_sort.go index 0f90ec70ce..acb9002276 100644 --- a/modules/base/natural_sort.go +++ b/modules/base/natural_sort.go @@ -4,12 +4,67 @@ package base import ( + "unicode/utf8" + "golang.org/x/text/collate" "golang.org/x/text/language" ) +func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) { + if pos >= len(str) { + return 0, 0, false + } + r, size = utf8.DecodeRuneInString(str[pos:]) + if r == utf8.RuneError { + r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii + } + return r, size, true +} + +func naturalSortAdvance(str string, pos int) (end int, isNumber bool) { + end = pos + for { + r, size, has := naturalSortGetRune(str, end) + if !has { + break + } + isCurRuneNum := '0' <= r && r <= '9' + if end == pos { + isNumber = isCurRuneNum + end += size + } else if isCurRuneNum == isNumber { + end += size + } else { + break + } + } + return end, isNumber +} + // NaturalSortLess compares two strings so that they could be sorted in natural order func NaturalSortLess(s1, s2 string) bool { + // There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997 + // text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997 + // So we need to handle the number parts by ourselves c := collate.New(language.English, collate.Numeric) - return c.CompareString(s1, s2) < 0 + pos1, pos2 := 0, 0 + for pos1 < len(s1) && pos2 < len(s2) { + end1, isNum1 := naturalSortAdvance(s1, pos1) + end2, isNum2 := naturalSortAdvance(s2, pos2) + part1, part2 := s1[pos1:end1], s2[pos2:end2] + if isNum1 && isNum2 { + if part1 != part2 { + if len(part1) != len(part2) { + return len(part1) < len(part2) + } + return part1 < part2 + } + } else { + if cmp := c.CompareString(part1, part2); cmp != 0 { + return cmp < 0 + } + } + pos1, pos2 = end1, end2 + } + return len(s1) < len(s2) } diff --git a/modules/base/natural_sort_test.go b/modules/base/natural_sort_test.go index f27a4eb53a..b001bc4ac9 100644 --- a/modules/base/natural_sort_test.go +++ b/modules/base/natural_sort_test.go @@ -10,21 +10,36 @@ import ( ) func TestNaturalSortLess(t *testing.T) { - test := func(s1, s2 string, less bool) { - assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2) + testLess := func(s1, s2 string) { + assert.True(t, NaturalSortLess(s1, s2), "s1