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/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
Guardfile-DSL / Configuring-Guard
+ `Guardfile-DSL / Configuring-Guard
`, // special syntax `[[Name|Link]]`, // rendered - ` + ` `, // 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 ofWine 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.
- + `, } - 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 +
+`, http.StatusOK) + + testRenderMarkdown(t, "gfm", false, input, `Link +
+`, http.StatusOK) + + testRenderMarkup(t, "gfm", false, "", input, `Link +
+`, http.StatusOK) + + testRenderMarkup(t, "file", false, "path/new-file.md", input, `Link +
+`, 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/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) 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": { 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');