mirror of
https://github.com/go-gitea/gitea.git
synced 2024-10-01 03:36:12 -04:00
Merge branch 'release/v1.22' into lunny/fix_issue_filter_project2
This commit is contained in:
commit
df5380b240
28
models/repo/avatar_test.go
Normal file
28
models/repo/avatar_test.go
Normal file
@ -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)
|
||||||
|
}
|
@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
|||||||
return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
|
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 {
|
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
|
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data
|
||||||
|
28
models/user/avatar_test.go
Normal file
28
models/user/avatar_test.go
Normal file
@ -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)
|
||||||
|
}
|
@ -4,12 +4,67 @@
|
|||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"golang.org/x/text/collate"
|
"golang.org/x/text/collate"
|
||||||
"golang.org/x/text/language"
|
"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
|
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||||
func NaturalSortLess(s1, s2 string) bool {
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,36 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNaturalSortLess(t *testing.T) {
|
func TestNaturalSortLess(t *testing.T) {
|
||||||
test := func(s1, s2 string, less bool) {
|
testLess := func(s1, s2 string) {
|
||||||
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
|
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
||||||
|
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||||
}
|
}
|
||||||
test("v1.20.0", "v1.2.0", false)
|
testEqual := func(s1, s2 string) {
|
||||||
test("v1.20.0", "v1.29.0", true)
|
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
||||||
test("v1.20.0", "v1.20.0", false)
|
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||||
test("abc", "bcd", true)
|
}
|
||||||
test("a-1-a", "a-1-b", true)
|
|
||||||
test("2", "12", true)
|
testEqual("", "")
|
||||||
test("a", "ab", true)
|
testLess("", "a")
|
||||||
|
testLess("", "1")
|
||||||
test("A", "b", true)
|
|
||||||
test("a", "B", true)
|
testLess("v1.2", "v1.2.0")
|
||||||
|
testLess("v1.2.0", "v1.10.0")
|
||||||
test("cafe", "café", true)
|
testLess("v1.20.0", "v1.29.0")
|
||||||
test("café", "cafe", false)
|
testEqual("v1.20.0", "v1.20.0")
|
||||||
test("caff", "café", false)
|
|
||||||
|
testLess("a", "A")
|
||||||
|
testLess("a", "B")
|
||||||
|
testLess("A", "b")
|
||||||
|
testLess("A", "ab")
|
||||||
|
|
||||||
|
testLess("abc", "bcd")
|
||||||
|
testLess("a-1-a", "a-1-b")
|
||||||
|
testLess("2", "12")
|
||||||
|
|
||||||
|
testLess("cafe", "café")
|
||||||
|
testLess("café", "caff")
|
||||||
|
|
||||||
|
testLess("A-2", "A-11")
|
||||||
|
testLess("0.txt", "1.txt")
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string {
|
|||||||
return req.Header.Get("X-Forwarded-Host")
|
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 {
|
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)
|
req, ok := ctx.Value(RequestContextKey).(*http.Request)
|
||||||
if !ok {
|
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.
|
// 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.
|
// 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.
|
// 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)
|
reqScheme := getRequestScheme(req)
|
||||||
if reqScheme == "" {
|
if reqScheme == "" {
|
||||||
return setting.AppURL
|
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||||
}
|
}
|
||||||
reqHost := getForwardedHost(req)
|
reqHost := getForwardedHost(req)
|
||||||
if reqHost == "" {
|
if reqHost == "" {
|
||||||
reqHost = req.Host
|
reqHost = req.Host
|
||||||
}
|
}
|
||||||
return reqScheme + "://" + reqHost + setting.AppSubURL + "/"
|
return reqScheme + "://" + reqHost
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeAbsoluteURL(ctx context.Context, s string) string {
|
// MakeAbsoluteURL tries to make a link to an absolute URL:
|
||||||
if IsRelativeURL(s) {
|
// * If link is empty, it returns the current app URL.
|
||||||
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/")
|
// * 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 {
|
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
|
||||||
|
@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
|
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/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://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
|
assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||||
Host: "user-host",
|
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{
|
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||||
Host: "user-host",
|
Host: "user-host",
|
||||||
@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||||||
"X-Forwarded-Host": {"forwarded-host"},
|
"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{
|
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||||
Host: "user-host",
|
Host: "user-host",
|
||||||
@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||||||
"X-Forwarded-Proto": {"https"},
|
"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) {
|
func TestIsCurrentGiteaSiteURL(t *testing.T) {
|
||||||
|
@ -84,10 +84,10 @@ type RenderContext struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Links struct {
|
type Links struct {
|
||||||
AbsolutePrefix bool
|
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
||||||
Base string
|
Base string // base prefix for pre-provided links and medias (images, videos)
|
||||||
BranchPath string
|
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
||||||
TreePath string
|
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Links) Prefix() string {
|
func (l *Links) Prefix() string {
|
||||||
|
@ -25,7 +25,8 @@ type MarkupOption struct {
|
|||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Mode string
|
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
|
// in: body
|
||||||
Context string
|
Context string
|
||||||
@ -53,7 +54,8 @@ type MarkdownOption struct {
|
|||||||
//
|
//
|
||||||
// in: body
|
// in: body
|
||||||
Mode string
|
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
|
// in: body
|
||||||
Context string
|
Context string
|
||||||
|
@ -117,7 +117,7 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
|
|||||||
|
|
||||||
func apiUnauthorizedError(ctx *context.Context) {
|
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
|
// 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="*"`)
|
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
|
||||||
apiErrorDefined(ctx, errUnauthorized)
|
apiErrorDefined(ctx, errUnauthorized)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
go_context "context"
|
go_context "context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -19,36 +20,40 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const AppURL = "http://localhost:3000/"
|
||||||
AppURL = "http://localhost:3000/"
|
|
||||||
Repo = "gogits/gogs"
|
|
||||||
FullURL = AppURL + Repo + "/"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
setting.AppURL = AppURL
|
||||||
|
context := "/gogits/gogs"
|
||||||
|
if !wiki {
|
||||||
|
context += path.Join("/src/branch/main", path.Dir(filePath))
|
||||||
|
}
|
||||||
options := api.MarkupOption{
|
options := api.MarkupOption{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Text: text,
|
Text: text,
|
||||||
Context: Repo,
|
Context: context,
|
||||||
Wiki: true,
|
Wiki: wiki,
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
}
|
}
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
||||||
web.SetForm(ctx, &options)
|
web.SetForm(ctx, &options)
|
||||||
Markup(ctx)
|
Markup(ctx)
|
||||||
assert.Equal(t, responseBody, resp.Body.String())
|
assert.Equal(t, expectedBody, resp.Body.String())
|
||||||
assert.Equal(t, responseCode, resp.Code)
|
assert.Equal(t, expectedCode, resp.Code)
|
||||||
resp.Body.Reset()
|
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
|
setting.AppURL = AppURL
|
||||||
|
context := "/gogits/gogs"
|
||||||
|
if !wiki {
|
||||||
|
context += "/src/branch/main"
|
||||||
|
}
|
||||||
options := api.MarkdownOption{
|
options := api.MarkdownOption{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Text: text,
|
Text: text,
|
||||||
Context: Repo,
|
Context: context,
|
||||||
Wiki: true,
|
Wiki: wiki,
|
||||||
}
|
}
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||||
web.SetForm(ctx, &options)
|
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
|
// dear imgui wiki markdown extract: special wiki syntax
|
||||||
`Wiki! Enjoy :)
|
`Wiki! Enjoy :)
|
||||||
- [[Links, Language bindings, Engine bindings|Links]]
|
- [[Links, Language bindings, Engine bindings|Links]]
|
||||||
@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) {
|
|||||||
// rendered
|
// rendered
|
||||||
`<p>Wiki! Enjoy :)</p>
|
`<p>Wiki! Enjoy :)</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
<li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||||
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
|
<li><a href="http://localhost:3000/gogits/gogs/wiki/Tips" rel="nofollow">Tips</a></li>
|
||||||
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
// Guard wiki sidebar: special syntax
|
// Guard wiki sidebar: special syntax
|
||||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||||
// rendered
|
// rendered
|
||||||
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||||
`,
|
`,
|
||||||
// special syntax
|
// special syntax
|
||||||
`[[Name|Link]]`,
|
`[[Name|Link]]`,
|
||||||
// rendered
|
// rendered
|
||||||
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
|
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p>
|
||||||
`,
|
`,
|
||||||
// empty
|
// empty
|
||||||
``,
|
``,
|
||||||
@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) {
|
|||||||
``,
|
``,
|
||||||
}
|
}
|
||||||
|
|
||||||
testCasesDocument := []string{
|
testCasesWikiDocument := []string{
|
||||||
// wine-staging wiki home extract: special wiki syntax, images
|
// wine-staging wiki home extract: special wiki syntax, images
|
||||||
`## What is Wine Staging?
|
`## What is Wine Staging?
|
||||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
|
**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
|
|||||||
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
||||||
<h2 id="user-content-quick-links">Quick Links</h2>
|
<h2 id="user-content-quick-links">Quick Links</h2>
|
||||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||||
<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
|
<p><a href="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a>
|
||||||
<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
<a href="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(testCasesCommon); i += 2 {
|
for i := 0; i < len(testCasesWiki); i += 2 {
|
||||||
text := testCasesCommon[i]
|
text := testCasesWiki[i]
|
||||||
response := testCasesCommon[i+1]
|
response := testCasesWiki[i+1]
|
||||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
|
||||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
|
||||||
testRenderMarkdown(t, "comment", text, response, http.StatusOK)
|
testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
|
||||||
testRenderMarkup(t, "comment", "", text, response, http.StatusOK)
|
testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
|
||||||
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
|
testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < len(testCasesDocument); i += 2 {
|
for i := 0; i < len(testCasesWikiDocument); i += 2 {
|
||||||
text := testCasesDocument[i]
|
text := testCasesWikiDocument[i]
|
||||||
response := testCasesDocument[i+1]
|
response := testCasesWikiDocument[i+1]
|
||||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
|
||||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
|
||||||
testRenderMarkup(t, "file", "path/test.md", 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)
|
input := "[Link](test.md)\n![Image](image.png)"
|
||||||
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
|
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
|
`, http.StatusOK)
|
||||||
|
|
||||||
|
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
|
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
|
`, http.StatusOK)
|
||||||
|
|
||||||
|
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
|
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
|
`, http.StatusOK)
|
||||||
|
|
||||||
|
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/path/test.md" rel="nofollow">Link</a>
|
||||||
|
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
|
||||||
|
`, 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{
|
var simpleCases = []string{
|
||||||
@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) {
|
|||||||
options := api.MarkdownOption{
|
options := api.MarkdownOption{
|
||||||
Mode: "markdown",
|
Mode: "markdown",
|
||||||
Text: "",
|
Text: "",
|
||||||
Context: Repo,
|
Context: "/gogits/gogs",
|
||||||
}
|
}
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||||
for i := 0; i < len(simpleCases); i += 2 {
|
for i := 0; i < len(simpleCases); i += 2 {
|
||||||
|
@ -7,25 +7,30 @@ package common
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
|
|
||||||
"mvdan.cc/xurls/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderMarkup renders markup text for the /markup and /markdown endpoints
|
// 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) {
|
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
|
||||||
var markupType string
|
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
|
||||||
relativePath := ""
|
// 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 {
|
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
|
||||||
_, _ = ctx.Write([]byte(""))
|
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
|
||||||
return
|
|
||||||
|
var markupType, relativePath string
|
||||||
|
|
||||||
|
links := markup.Links{AbsolutePrefix: true}
|
||||||
|
if urlPathContext != "" {
|
||||||
|
links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
@ -33,36 +38,35 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
|
|||||||
// Raw markdown
|
// Raw markdown
|
||||||
if err := markdown.RenderRaw(&markup.RenderContext{
|
if err := markdown.RenderRaw(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Links: markup.Links{
|
Links: links,
|
||||||
AbsolutePrefix: true,
|
|
||||||
Base: urlPrefix,
|
|
||||||
},
|
|
||||||
}, strings.NewReader(text), ctx.Resp); err != nil {
|
}, strings.NewReader(text), ctx.Resp); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case "comment":
|
case "comment":
|
||||||
// Comment as markdown
|
// Issue & comment content
|
||||||
markupType = markdown.MarkupName
|
markupType = markdown.MarkupName
|
||||||
case "gfm":
|
case "gfm":
|
||||||
// Github Flavored Markdown as document
|
// GitHub Flavored Markdown
|
||||||
markupType = markdown.MarkupName
|
markupType = markdown.MarkupName
|
||||||
case "file":
|
case "file":
|
||||||
// File as document based on file extension
|
markupType = "" // render the repo file content by its extension
|
||||||
markupType = ""
|
|
||||||
relativePath = filePath
|
relativePath = filePath
|
||||||
default:
|
default:
|
||||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
|
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
|
fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5)
|
||||||
// check if urlPrefix is already set to a URL
|
if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
|
||||||
linkRegex, _ := xurls.StrictMatchingScheme("https?://")
|
// absolute base prefix is something like "https://host/subpath/{user}/{repo}"
|
||||||
m := linkRegex.FindStringIndex(urlPrefix)
|
absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1])
|
||||||
if m == nil {
|
|
||||||
urlPrefix = util.URLJoin(setting.AppURL, urlPrefix)
|
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{}
|
meta := map[string]string{}
|
||||||
@ -79,10 +83,7 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
|
|||||||
|
|
||||||
if err := markup.Render(&markup.RenderContext{
|
if err := markup.Render(&markup.RenderContext{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
Links: markup.Links{
|
Links: links,
|
||||||
AbsolutePrefix: true,
|
|
||||||
Base: urlPrefix,
|
|
||||||
},
|
|
||||||
Metas: meta,
|
Metas: meta,
|
||||||
IsWiki: wiki,
|
IsWiki: wiki,
|
||||||
Type: markupType,
|
Type: markupType,
|
||||||
|
@ -418,6 +418,7 @@ func RedirectDownload(ctx *context.Context) {
|
|||||||
tagNames := []string{vTag}
|
tagNames := []string{vTag}
|
||||||
curRepo := ctx.Repo.Repository
|
curRepo := ctx.Repo.Repository
|
||||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||||
|
IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
|
||||||
RepoID: curRepo.ID,
|
RepoID: curRepo.ID,
|
||||||
TagNames: tagNames,
|
TagNames: tagNames,
|
||||||
})
|
})
|
||||||
|
4
templates/swagger/v1_json.tmpl
generated
4
templates/swagger/v1_json.tmpl
generated
@ -21954,7 +21954,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Context": {
|
"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"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"Mode": {
|
"Mode": {
|
||||||
@ -21977,7 +21977,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Context": {
|
"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"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"FilePath": {
|
"FilePath": {
|
||||||
|
@ -69,9 +69,9 @@ export function initRepoGraphGit() {
|
|||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
document.getElementById('pagination').innerHTML = div.getElementById('pagination').innerHTML;
|
document.getElementById('pagination').innerHTML = div.querySelector('#pagination').innerHTML;
|
||||||
document.getElementById('rel-container').innerHTML = div.getElementById('rel-container').innerHTML;
|
document.getElementById('rel-container').innerHTML = div.querySelector('#rel-container').innerHTML;
|
||||||
document.getElementById('rev-container').innerHTML = div.getElementById('rev-container').innerHTML;
|
document.getElementById('rev-container').innerHTML = div.querySelector('#rev-container').innerHTML;
|
||||||
hideElem('#loading-indicator');
|
hideElem('#loading-indicator');
|
||||||
showElem('#rel-container');
|
showElem('#rel-container');
|
||||||
showElem('#rev-container');
|
showElem('#rev-container');
|
||||||
|
Loading…
Reference in New Issue
Block a user