mirror of
https://github.com/go-gitea/gitea
synced 2025-01-03 12:15:58 +01:00
Merge branch 'main' into lunny/automerge_support_delete_branch
This commit is contained in:
commit
2f7484487a
@ -524,6 +524,7 @@ rules:
|
||||
no-jquery/no-data: [0]
|
||||
no-jquery/no-deferred: [2]
|
||||
no-jquery/no-delegate: [2]
|
||||
no-jquery/no-done-fail: [2]
|
||||
no-jquery/no-each-collection: [0]
|
||||
no-jquery/no-each-util: [0]
|
||||
no-jquery/no-each: [0]
|
||||
@ -538,6 +539,7 @@ rules:
|
||||
no-jquery/no-find-util: [2]
|
||||
no-jquery/no-find: [0]
|
||||
no-jquery/no-fx-interval: [2]
|
||||
no-jquery/no-fx: [2]
|
||||
no-jquery/no-global-eval: [2]
|
||||
no-jquery/no-global-selector: [0]
|
||||
no-jquery/no-grep: [2]
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.tsbuildInfo
|
||||
|
||||
*coverage.out
|
||||
coverage.all
|
||||
|
@ -63,3 +63,4 @@ Tim-Niclas Oelschläger <zokki.softwareschmiede@gmail.com> (@zokkis)
|
||||
Yu Liu <1240335630@qq.com> (@HEREYUA)
|
||||
Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
|
||||
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
|
||||
hiifong <i@hiif.ong> (@hiifong)
|
||||
|
8
Makefile
8
Makefile
@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
|
||||
.PHONY: lint-js
|
||||
lint-js: node_modules
|
||||
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
|
||||
# npx tsc
|
||||
# npx vue-tsc
|
||||
|
||||
.PHONY: lint-js-fix
|
||||
lint-js-fix: node_modules
|
||||
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
|
||||
# npx tsc
|
||||
# npx vue-tsc
|
||||
|
||||
.PHONY: lint-css
|
||||
lint-css: node_modules
|
||||
@ -451,6 +451,10 @@ lint-templates: .venv node_modules
|
||||
lint-yaml: .venv
|
||||
@poetry run yamllint .
|
||||
|
||||
.PHONY: tsc
|
||||
tsc:
|
||||
npx vue-tsc
|
||||
|
||||
.PHONY: watch
|
||||
watch:
|
||||
@bash tools/watch.sh
|
||||
|
19
assets/go-licenses.json
generated
19
assets/go-licenses.json
generated
@ -1090,8 +1090,8 @@
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/stretchr/testify/assert",
|
||||
"path": "github.com/stretchr/testify/assert/LICENSE",
|
||||
"name": "github.com/stretchr/testify",
|
||||
"path": "github.com/stretchr/testify/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
@ -1124,6 +1124,16 @@
|
||||
"path": "github.com/valyala/fastjson/LICENSE",
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/wneessen/go-mail",
|
||||
"path": "github.com/wneessen/go-mail/LICENSE",
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2022-2023 The go-mail Authors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "github.com/wneessen/go-mail/smtp",
|
||||
"path": "github.com/wneessen/go-mail/smtp/LICENSE",
|
||||
"licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
},
|
||||
{
|
||||
"name": "github.com/x448/float16",
|
||||
"path": "github.com/x448/float16/LICENSE",
|
||||
@ -1259,11 +1269,6 @@
|
||||
"path": "google.golang.org/protobuf/LICENSE",
|
||||
"licenseText": "Copyright (c) 2018 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
|
||||
},
|
||||
{
|
||||
"name": "gopkg.in/gomail.v2",
|
||||
"path": "gopkg.in/gomail.v2/LICENSE",
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Alexandre Cesaro\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
|
||||
},
|
||||
{
|
||||
"name": "gopkg.in/ini.v1",
|
||||
"path": "gopkg.in/ini.v1/LICENSE",
|
||||
|
@ -1,80 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//nolint:forbidigo
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
)
|
||||
|
||||
// To generate derivative fixtures, execute the following from Gitea's repository base dir:
|
||||
// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...]
|
||||
|
||||
var (
|
||||
generators = []struct {
|
||||
gen func(ctx context.Context) (string, error)
|
||||
name string
|
||||
}{
|
||||
{
|
||||
models.GetYamlFixturesAccess, "access",
|
||||
},
|
||||
}
|
||||
fixturesDir string
|
||||
)
|
||||
|
||||
func main() {
|
||||
pathToGiteaRoot := "."
|
||||
fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures")
|
||||
if err := unittest.CreateTestEngine(unittest.FixturesOptions{
|
||||
Dir: fixturesDir,
|
||||
}); err != nil {
|
||||
fmt.Printf("CreateTestEngine: %+v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unittest.PrepareTestDatabase(); err != nil {
|
||||
fmt.Printf("PrepareTestDatabase: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if len(os.Args) == 0 {
|
||||
for _, r := range os.Args {
|
||||
if err := generate(ctx, r); err != nil {
|
||||
fmt.Printf("generate '%s': %+v\n", r, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, g := range generators {
|
||||
if err := generate(ctx, g.name); err != nil {
|
||||
fmt.Printf("generate '%s': %+v\n", g.name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generate(ctx context.Context, name string) error {
|
||||
for _, g := range generators {
|
||||
if g.name == name {
|
||||
data, err := g.gen(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(fixturesDir, name+".yml")
|
||||
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
|
||||
return fmt.Errorf("%s: %+v", path, err)
|
||||
}
|
||||
fmt.Printf("%s created.\n", path)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("generator not found")
|
||||
}
|
@ -784,6 +784,10 @@ LEVEL = Info
|
||||
;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
|
||||
;ENABLE_BASIC_AUTHENTICATION = true
|
||||
;;
|
||||
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods.
|
||||
;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication.
|
||||
;ENABLE_PASSWORD_SIGNIN_FORM = true
|
||||
;;
|
||||
;; More detail: https://github.com/gogits/gogs/issues/165
|
||||
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
|
||||
; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
|
||||
@ -1944,6 +1948,13 @@ LEVEL = Info
|
||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_SECRET_ACCESS_KEY =
|
||||
;;
|
||||
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
|
||||
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
|
||||
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
|
||||
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
|
||||
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
|
||||
;MINIO_IAM_ENDPOINT =
|
||||
;;
|
||||
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_BUCKET = gitea
|
||||
;;
|
||||
@ -2688,6 +2699,13 @@ LEVEL = Info
|
||||
;; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_SECRET_ACCESS_KEY =
|
||||
;;
|
||||
;; Preferred IAM Endpoint to override Minio's default IAM Endpoint resolution only available when STORAGE_TYPE is `minio`.
|
||||
;; If not provided and STORAGE_TYPE is `minio`, will search for and derive endpoint from known environment variables
|
||||
;; (AWS_CONTAINER_AUTHORIZATION_TOKEN, AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
|
||||
;; AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_REGION),
|
||||
;; or the DefaultIAMRoleEndpoint if not provided otherwise.
|
||||
;MINIO_IAM_ENDPOINT =
|
||||
;;
|
||||
;; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
|
||||
;MINIO_BUCKET = gitea
|
||||
;;
|
||||
|
3
go.mod
3
go.mod
@ -114,6 +114,7 @@ require (
|
||||
github.com/tstranex/u2f v1.0.0
|
||||
github.com/ulikunitz/xz v0.5.12
|
||||
github.com/urfave/cli/v2 v2.27.5
|
||||
github.com/wneessen/go-mail v0.5.2
|
||||
github.com/xanzy/go-gitlab v0.112.0
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/yohcop/openid-go v1.0.1
|
||||
@ -130,7 +131,6 @@ require (
|
||||
golang.org/x/tools v0.26.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
google.golang.org/protobuf v1.35.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
@ -319,7 +319,6 @@ require (
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
25
go.sum
25
go.sum
@ -815,6 +815,8 @@ github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT8=
|
||||
github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+DKw=
|
||||
@ -887,8 +889,10 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
@ -901,6 +905,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -920,8 +927,10 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
@ -934,6 +943,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -966,10 +978,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
@ -977,8 +992,10 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -989,7 +1006,9 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
@ -1004,6 +1023,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1024,8 +1045,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -1033,8 +1052,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
@ -200,7 +200,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Action) loadRepo(ctx context.Context) {
|
||||
func (a *Action) LoadRepo(ctx context.Context) {
|
||||
if a.Repo != nil {
|
||||
return
|
||||
}
|
||||
@ -250,7 +250,7 @@ func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
|
||||
|
||||
// GetRepoUserName returns the name of the action repository owner.
|
||||
func (a *Action) GetRepoUserName(ctx context.Context) string {
|
||||
a.loadRepo(ctx)
|
||||
a.LoadRepo(ctx)
|
||||
if a.Repo == nil {
|
||||
return "(non-existing-repo)"
|
||||
}
|
||||
@ -265,7 +265,7 @@ func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
||||
|
||||
// GetRepoName returns the name of the action repository.
|
||||
func (a *Action) GetRepoName(ctx context.Context) string {
|
||||
a.loadRepo(ctx)
|
||||
a.LoadRepo(ctx)
|
||||
if a.Repo == nil {
|
||||
return "(non-existing-repo)"
|
||||
}
|
||||
@ -448,65 +448,13 @@ type GetFeedsOptions struct {
|
||||
Date string // the day we want activity for: YYYY-MM-DD
|
||||
}
|
||||
|
||||
// GetFeeds returns actions according to the provided options
|
||||
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
|
||||
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
|
||||
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
|
||||
}
|
||||
|
||||
cond, err := activityQueryCondition(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
actions := make([]*Action, 0, opts.PageSize)
|
||||
var count int64
|
||||
opts.SetDefaultValues()
|
||||
|
||||
if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value.
|
||||
sess := db.GetEngine(ctx).Where(cond)
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
|
||||
}
|
||||
} else {
|
||||
// First, only query which IDs are necessary, and only then query all actions to speed up the overall query
|
||||
sess := db.GetEngine(ctx).Where(cond).Select("`action`.id")
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
actionIDs := make([]int64, 0, opts.PageSize)
|
||||
if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
|
||||
}
|
||||
|
||||
count, err = db.GetEngine(ctx).Where(cond).
|
||||
Table("action").
|
||||
Cols("`action`.id").Count()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("Count: %w", err)
|
||||
}
|
||||
|
||||
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ActionList(actions).LoadAttributes(ctx); err != nil {
|
||||
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
|
||||
return actions, count, nil
|
||||
}
|
||||
|
||||
// ActivityReadable return whether doer can read activities of user
|
||||
func ActivityReadable(user, doer *user_model.User) bool {
|
||||
return !user.KeepActivityPrivate ||
|
||||
doer != nil && (doer.IsAdmin || user.ID == doer.ID)
|
||||
}
|
||||
|
||||
func activityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
|
||||
func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if opts.RequestedTeam != nil && opts.RequestedUser == nil {
|
||||
@ -644,7 +592,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
|
||||
}
|
||||
|
||||
if repoChanged {
|
||||
act.loadRepo(ctx)
|
||||
act.LoadRepo(ctx)
|
||||
repo = act.Repo
|
||||
|
||||
// check repo owner exist.
|
||||
@ -770,7 +718,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64)
|
||||
// CountActionCreatedUnixString count actions where created_unix is an empty string
|
||||
func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
|
||||
if setting.Database.Type.IsSQLite3() {
|
||||
return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action))
|
||||
return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action))
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@ -778,7 +726,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
|
||||
// FixActionCreatedUnixString set created_unix to zero if it is an empty string
|
||||
func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
|
||||
if setting.Database.Type.IsSQLite3() {
|
||||
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`)
|
||||
res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -201,3 +201,55 @@ func (actions ActionList) LoadIssues(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFeeds returns actions according to the provided options
|
||||
func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, error) {
|
||||
if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
|
||||
return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
|
||||
}
|
||||
|
||||
cond, err := ActivityQueryCondition(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
actions := make([]*Action, 0, opts.PageSize)
|
||||
var count int64
|
||||
opts.SetDefaultValues()
|
||||
|
||||
if opts.Page < 10 { // TODO: why it's 10 but other values? It's an experience value.
|
||||
sess := db.GetEngine(ctx).Where(cond)
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
|
||||
}
|
||||
} else {
|
||||
// First, only query which IDs are necessary, and only then query all actions to speed up the overall query
|
||||
sess := db.GetEngine(ctx).Where(cond).Select("`action`.id")
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
actionIDs := make([]int64, 0, opts.PageSize)
|
||||
if err := sess.Table("action").Desc("`action`.created_unix").Find(&actionIDs); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
|
||||
}
|
||||
|
||||
count, err = db.GetEngine(ctx).Where(cond).
|
||||
Table("action").
|
||||
Cols("`action`.id").Count()
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("Count: %w", err)
|
||||
}
|
||||
|
||||
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
|
||||
return nil, 0, fmt.Errorf("Find: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ActionList(actions).LoadAttributes(ctx); err != nil {
|
||||
return nil, 0, fmt.Errorf("LoadAttributes: %w", err)
|
||||
}
|
||||
|
||||
return actions, count, nil
|
||||
}
|
||||
|
@ -42,114 +42,6 @@ func TestAction_GetRepoLink(t *testing.T) {
|
||||
assert.Equal(t, comment.HTMLURL(db.DefaultContext), action.GetCommentHTMLURL(db.DefaultContext))
|
||||
}
|
||||
|
||||
func TestGetFeeds(t *testing.T) {
|
||||
// test with an individual user
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, actions, 1) {
|
||||
assert.EqualValues(t, 1, actions[0].ID)
|
||||
assert.EqualValues(t, user.ID, actions[0].UserID)
|
||||
}
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestGetFeedsForRepos(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
privRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
pubRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
|
||||
|
||||
// private repo & no login
|
||||
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: privRepo,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
assert.Equal(t, int64(0), count)
|
||||
|
||||
// public repo & no login
|
||||
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: pubRepo,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
// private repo and login
|
||||
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: privRepo,
|
||||
IncludePrivate: true,
|
||||
Actor: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
// public repo & login
|
||||
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: pubRepo,
|
||||
IncludePrivate: true,
|
||||
Actor: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestGetFeeds2(t *testing.T) {
|
||||
// test with an organization user
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
if assert.Len(t, actions, 1) {
|
||||
assert.EqualValues(t, 2, actions[0].ID)
|
||||
assert.EqualValues(t, org.ID, actions[0].UserID)
|
||||
}
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
actions, count, err = activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: org,
|
||||
Actor: user,
|
||||
IncludePrivate: false,
|
||||
OnlyPerformedBy: false,
|
||||
IncludeDeleted: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 0)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestActivityReadable(t *testing.T) {
|
||||
tt := []struct {
|
||||
desc string
|
||||
@ -227,26 +119,6 @@ func TestNotifyWatchers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFeedsCorrupted(t *testing.T) {
|
||||
// Now we will not check for corrupted data in the feeds
|
||||
// users should run doctor to fix their data
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ID: 8,
|
||||
RepoID: 1700,
|
||||
})
|
||||
|
||||
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
Actor: user,
|
||||
IncludePrivate: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestConsistencyUpdateAction(t *testing.T) {
|
||||
if !setting.Database.Type.IsSQLite3() {
|
||||
t.Skip("Test is only for SQLite database.")
|
||||
@ -256,7 +128,7 @@ func TestConsistencyUpdateAction(t *testing.T) {
|
||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||
ID: int64(id),
|
||||
})
|
||||
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id)
|
||||
_, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id)
|
||||
assert.NoError(t, err)
|
||||
actions := make([]*activities_model.Action, 0, 1)
|
||||
//
|
||||
@ -322,24 +194,3 @@ func TestDeleteIssueActions(t *testing.T) {
|
||||
assert.NoError(t, activities_model.DeleteIssueActions(db.DefaultContext, issue.RepoID, issue.ID, issue.Index))
|
||||
unittest.AssertCount(t, &activities_model.Action{}, 0)
|
||||
}
|
||||
|
||||
func TestRepoActions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{})
|
||||
for i := 0; i < 3; i++ {
|
||||
_ = db.Insert(db.DefaultContext, &activities_model.Action{
|
||||
UserID: 2 + int64(i),
|
||||
ActUserID: 2,
|
||||
RepoID: repo.ID,
|
||||
OpType: activities_model.ActionCommentIssue,
|
||||
})
|
||||
}
|
||||
count, _ := db.Count[activities_model.Action](db.DefaultContext, &db.ListOptions{})
|
||||
assert.EqualValues(t, 3, count)
|
||||
actions, _, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: repo,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, actions, 1)
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
|
||||
groupByName = groupBy
|
||||
}
|
||||
|
||||
cond, err := activityQueryCondition(ctx, GetFeedsOptions{
|
||||
cond, err := ActivityQueryCondition(ctx, GetFeedsOptions{
|
||||
RequestedUser: user,
|
||||
RequestedTeam: team,
|
||||
Actor: doer,
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
)
|
||||
|
||||
@ -89,14 +90,33 @@ func (cred *WebAuthnCredential) AfterLoad() {
|
||||
// WebAuthnCredentialList is a list of *WebAuthnCredential
|
||||
type WebAuthnCredentialList []*WebAuthnCredential
|
||||
|
||||
// newCredentialFlagsFromAuthenticatorFlags is copied from https://github.com/go-webauthn/webauthn/pull/337
|
||||
// to convert protocol.AuthenticatorFlags to webauthn.CredentialFlags
|
||||
func newCredentialFlagsFromAuthenticatorFlags(flags protocol.AuthenticatorFlags) webauthn.CredentialFlags {
|
||||
return webauthn.CredentialFlags{
|
||||
UserPresent: flags.HasUserPresent(),
|
||||
UserVerified: flags.HasUserVerified(),
|
||||
BackupEligible: flags.HasBackupEligible(),
|
||||
BackupState: flags.HasBackupState(),
|
||||
}
|
||||
}
|
||||
|
||||
// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials
|
||||
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
|
||||
func (list WebAuthnCredentialList) ToCredentials(defaultAuthFlags ...protocol.AuthenticatorFlags) []webauthn.Credential {
|
||||
// TODO: at the moment, Gitea doesn't store or check the flags
|
||||
// so we need to use the default flags from the authenticator to make the login validation pass
|
||||
// In the future, we should:
|
||||
// 1. store the flags when registering the credential
|
||||
// 2. provide the stored flags when converting the credentials (for login)
|
||||
// 3. for old users, still use this fallback to the default flags
|
||||
defAuthFlags := util.OptionalArg(defaultAuthFlags)
|
||||
creds := make([]webauthn.Credential, 0, len(list))
|
||||
for _, cred := range list {
|
||||
creds = append(creds, webauthn.Credential{
|
||||
ID: cred.CredentialID,
|
||||
PublicKey: cred.PublicKey,
|
||||
AttestationType: cred.AttestationType,
|
||||
Flags: newCredentialFlagsFromAuthenticatorFlags(defAuthFlags),
|
||||
Authenticator: webauthn.Authenticator{
|
||||
AAGUID: cred.AAGUID,
|
||||
SignCount: cred.SignCount,
|
||||
|
@ -134,6 +134,9 @@ func SyncAllTables() error {
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xormEngine, err := newXORMEngine()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "SQLite3 support") {
|
||||
return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
|
||||
}
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
)
|
||||
|
||||
// GetYamlFixturesAccess returns a string containing the contents
|
||||
// for the access table, as recalculated using repo.RecalculateAccesses()
|
||||
func GetYamlFixturesAccess(ctx context.Context) (string, error) {
|
||||
repos := make([]*repo_model.Repository, 0, 50)
|
||||
if err := db.GetEngine(ctx).Find(&repos); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
repo.MustOwner(ctx)
|
||||
if err := access_model.RecalculateAccesses(ctx, repo); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
accesses := make([]*access_model.Access, 0, 200)
|
||||
if err := db.GetEngine(ctx).OrderBy("user_id, repo_id").Find(&accesses); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i, a := range accesses {
|
||||
fmt.Fprintf(&b, "-\n")
|
||||
fmt.Fprintf(&b, " id: %d\n", i+1)
|
||||
fmt.Fprintf(&b, " user_id: %d\n", a.UserID)
|
||||
fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID)
|
||||
fmt.Fprintf(&b, " mode: %d\n", a.Mode)
|
||||
if i < len(accesses)-1 {
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFixtureGeneration(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
test := func(ctx context.Context, gen func(ctx context.Context) (string, error), name string) {
|
||||
expected, err := gen(ctx)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
p := filepath.Join(unittest.FixturesDir(), name+".yml")
|
||||
bytes, err := os.ReadFile(p)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
data := string(util.NormalizeEOL(bytes))
|
||||
assert.EqualValues(t, expected, data, "Differences detected for %s", p)
|
||||
}
|
||||
|
||||
test(db.DefaultContext, GetYamlFixturesAccess, "access")
|
||||
}
|
@ -34,6 +34,7 @@ type ProtectedBranch struct {
|
||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name
|
||||
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
globRule glob.Glob `xorm:"-"`
|
||||
isPlainName bool `xorm:"-"`
|
||||
CanPush bool `xorm:"NOT NULL DEFAULT false"`
|
||||
@ -413,14 +414,27 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
}
|
||||
protectBranch.ApprovalsWhitelistTeamIDs = whitelist
|
||||
|
||||
// Make sure protectBranch.ID is not 0 for whitelists
|
||||
// Looks like it's a new rule
|
||||
if protectBranch.ID == 0 {
|
||||
// as it's a new rule and if priority was not set, we need to calc it.
|
||||
if protectBranch.Priority == 0 {
|
||||
var lowestPrio int64
|
||||
// because of mssql we can not use builder or save xorm syntax, so raw sql it is
|
||||
if _, err := db.GetEngine(ctx).SQL(`SELECT MAX(priority) FROM protected_branch WHERE repo_id = ?`, protectBranch.RepoID).
|
||||
Get(&lowestPrio); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Trace("Create new ProtectedBranch at repo[%d] and detect current lowest priority '%d'", protectBranch.RepoID, lowestPrio)
|
||||
protectBranch.Priority = lowestPrio + 1
|
||||
}
|
||||
|
||||
if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil {
|
||||
return fmt.Errorf("Insert: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// update the rule
|
||||
if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
|
||||
return fmt.Errorf("Update: %v", err)
|
||||
}
|
||||
@ -428,6 +442,24 @@ func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, prote
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProtectBranchPriorities(ctx context.Context, repo *repo_model.Repository, ids []int64) error {
|
||||
prio := int64(1)
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, id := range ids {
|
||||
if _, err := db.GetEngine(ctx).
|
||||
ID(id).Where("repo_id = ?", repo.ID).
|
||||
Cols("priority").
|
||||
Update(&ProtectedBranch{
|
||||
Priority: prio,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
prio++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with
|
||||
// the users from newWhitelist which have explicit read or write access to the repo.
|
||||
func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) {
|
||||
|
@ -28,6 +28,13 @@ func (rules ProtectedBranchRules) sort() {
|
||||
sort.Slice(rules, func(i, j int) bool {
|
||||
rules[i].loadGlob()
|
||||
rules[j].loadGlob()
|
||||
|
||||
// if priority differ, use that to sort
|
||||
if rules[i].Priority != rules[j].Priority {
|
||||
return rules[i].Priority < rules[j].Priority
|
||||
}
|
||||
|
||||
// now we sort the old way
|
||||
if rules[i].isPlainName != rules[j].isPlainName {
|
||||
return rules[i].isPlainName // plain name comes first, so plain name means "less"
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ func TestBranchRuleMatchPriority(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchRuleSort(t *testing.T) {
|
||||
func TestBranchRuleSortLegacy(t *testing.T) {
|
||||
in := []*ProtectedBranch{{
|
||||
RuleName: "b",
|
||||
CreatedUnix: 1,
|
||||
@ -103,3 +103,37 @@ func TestBranchRuleSort(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expect, got)
|
||||
}
|
||||
|
||||
func TestBranchRuleSortPriority(t *testing.T) {
|
||||
in := []*ProtectedBranch{{
|
||||
RuleName: "b",
|
||||
CreatedUnix: 1,
|
||||
Priority: 4,
|
||||
}, {
|
||||
RuleName: "b/*",
|
||||
CreatedUnix: 3,
|
||||
Priority: 2,
|
||||
}, {
|
||||
RuleName: "a/*",
|
||||
CreatedUnix: 2,
|
||||
Priority: 1,
|
||||
}, {
|
||||
RuleName: "c",
|
||||
CreatedUnix: 0,
|
||||
Priority: 0,
|
||||
}, {
|
||||
RuleName: "a",
|
||||
CreatedUnix: 4,
|
||||
Priority: 3,
|
||||
}}
|
||||
expect := []string{"c", "a/*", "b/*", "a", "b"}
|
||||
|
||||
pbr := ProtectedBranchRules(in)
|
||||
pbr.sort()
|
||||
|
||||
var got []string
|
||||
for i := range pbr {
|
||||
got = append(got, pbr[i].RuleName)
|
||||
}
|
||||
assert.Equal(t, expect, got)
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -76,3 +80,77 @@ func TestBranchRuleMatch(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProtectBranchPriorities(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
// Create some test protected branches with initial priorities
|
||||
protectedBranches := []*ProtectedBranch{
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
RuleName: "master",
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
RuleName: "develop",
|
||||
Priority: 2,
|
||||
},
|
||||
{
|
||||
RepoID: repo.ID,
|
||||
RuleName: "feature/*",
|
||||
Priority: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, pb := range protectedBranches {
|
||||
_, err := db.GetEngine(db.DefaultContext).Insert(pb)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test updating priorities
|
||||
newPriorities := []int64{protectedBranches[2].ID, protectedBranches[0].ID, protectedBranches[1].ID}
|
||||
err := UpdateProtectBranchPriorities(db.DefaultContext, repo, newPriorities)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify new priorities
|
||||
pbs, err := FindRepoProtectedBranchRules(db.DefaultContext, repo.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedPriorities := map[string]int64{
|
||||
"feature/*": 1,
|
||||
"master": 2,
|
||||
"develop": 3,
|
||||
}
|
||||
|
||||
for _, pb := range pbs {
|
||||
assert.Equal(t, expectedPriorities[pb.RuleName], pb.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProtectBranchPriority(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
err := UpdateProtectBranch(db.DefaultContext, repo, &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
RuleName: "branch-1",
|
||||
Priority: 1,
|
||||
}, WhitelistOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
newPB := &ProtectedBranch{
|
||||
RepoID: repo.ID,
|
||||
RuleName: "branch-2",
|
||||
// Priority intentionally omitted
|
||||
}
|
||||
|
||||
err = UpdateProtectBranch(db.DefaultContext, repo, newPB, WhitelistOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
savedPB2, err := GetFirstMatchProtectedBranchRule(db.DefaultContext, repo.ID, "branch-2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), savedPB2.Priority)
|
||||
}
|
||||
|
@ -114,6 +114,8 @@ const (
|
||||
|
||||
CommentTypePin // 36 pin Issue
|
||||
CommentTypeUnpin // 37 unpin Issue
|
||||
|
||||
CommentTypeChangeTimeEstimate // 38 Change time estimate
|
||||
)
|
||||
|
||||
var commentStrings = []string{
|
||||
@ -155,6 +157,7 @@ var commentStrings = []string{
|
||||
"pull_cancel_scheduled_merge",
|
||||
"pin",
|
||||
"unpin",
|
||||
"change_time_estimate",
|
||||
}
|
||||
|
||||
func (t CommentType) String() string {
|
||||
@ -1108,7 +1111,7 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList,
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
|
||||
if opts.Page != 0 {
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"xorm.io/builder"
|
||||
@ -112,14 +112,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
}
|
||||
|
||||
var err error
|
||||
if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Repo: issue.Repo,
|
||||
Links: markup.Links{
|
||||
Base: issue.Repo.Link(),
|
||||
},
|
||||
Metas: issue.Repo.ComposeMetas(ctx),
|
||||
}, comment.Content); err != nil {
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
|
||||
if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,9 @@ type Issue struct {
|
||||
|
||||
// For view issue page.
|
||||
ShowRole RoleDescriptor `xorm:"-"`
|
||||
|
||||
// Time estimate
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
var (
|
||||
@ -641,7 +644,7 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
|
||||
Where("issue_id = ?", issue.ID).
|
||||
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
|
||||
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
|
||||
if opts.Page != 0 {
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
err = sess.Find(&issueDeps)
|
||||
@ -934,3 +937,28 @@ func insertIssue(ctx context.Context, issue *Issue) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
|
||||
func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model.User, timeEstimate int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
|
||||
return fmt.Errorf("updateIssueCols: %w", err)
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return fmt.Errorf("loadRepo: %w", err)
|
||||
}
|
||||
|
||||
opts := &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTimeEstimate,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Content: fmt.Sprintf("%d", timeEstimate),
|
||||
}
|
||||
if _, err := CreateComment(ctx, opts); err != nil {
|
||||
return fmt.Errorf("createComment: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOpt
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
watches := make([]*IssueWatch, 0, listOptions.PageSize)
|
||||
return watches, sess.Find(&watches)
|
||||
|
@ -390,7 +390,7 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
@ -462,7 +462,7 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
|
@ -406,7 +406,7 @@ func TestDeleteIssueLabel(t *testing.T) {
|
||||
PosterID: doerID,
|
||||
IssueID: issueID,
|
||||
LabelID: labelID,
|
||||
}, `content=""`)
|
||||
}, `content=''`)
|
||||
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
|
||||
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
|
||||
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
|
||||
|
@ -163,7 +163,7 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
|
||||
Where(opts.toConds()).
|
||||
In("reaction.`type`", setting.UI.Reactions).
|
||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
|
||||
if opts.Page != 0 {
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
|
||||
reactions := make([]*Reaction, 0, opts.PageSize)
|
||||
|
@ -96,7 +96,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
|
||||
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
|
||||
sws := make([]*Stopwatch, 0, 8)
|
||||
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
|
||||
|
||||
sess = sess.Where(opts.ToConds())
|
||||
|
||||
if opts.Page != 0 {
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -33,15 +33,15 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
||||
ourSkip := 2
|
||||
ourSkip += skip
|
||||
deferFn := testlogger.PrintCurrentTest(t, ourSkip)
|
||||
assert.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
||||
require.NoError(t, unittest.SyncDirs(filepath.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
|
||||
|
||||
if err := deleteDB(); err != nil {
|
||||
t.Errorf("unable to reset database: %v", err)
|
||||
t.Fatalf("unable to reset database: %v", err)
|
||||
return nil, deferFn
|
||||
}
|
||||
|
||||
x, err := newXORMEngine()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
if x != nil {
|
||||
oldDefer := deferFn
|
||||
deferFn = func() {
|
||||
|
@ -367,6 +367,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate),
|
||||
newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard),
|
||||
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
|
||||
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
|
||||
newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable),
|
||||
newMigration(310, "Add DeleteBranchAfterMerge to AutoMerge", v1_23.AddDeleteBranchAfterMergeForAutoMerge),
|
||||
}
|
||||
return preparedMigrations
|
||||
|
@ -4,26 +4,13 @@
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type pullAutoMerge struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PullID int64 `xorm:"UNIQUE"`
|
||||
DoerID int64 `xorm:"INDEX NOT NULL"`
|
||||
MergeStyle string `xorm:"varchar(30)"`
|
||||
Message string `xorm:"LONGTEXT"`
|
||||
DeleteBranchAfterMerge bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
func AddPriorityToProtectedBranch(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
Priority int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
// TableName return database table name for xorm
|
||||
func (pullAutoMerge) TableName() string {
|
||||
return "pull_auto_merge"
|
||||
}
|
||||
|
||||
func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error {
|
||||
return x.Sync(new(pullAutoMerge))
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
}
|
||||
|
16
models/migrations/v1_23/v311.go
Normal file
16
models/migrations/v1_23/v311.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
|
||||
type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Issue))
|
||||
}
|
21
models/migrations/v1_23/v312.go
Normal file
21
models/migrations/v1_23/v312.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type pullAutoMerge struct {
|
||||
DeleteBranchAfterMerge bool
|
||||
}
|
||||
|
||||
// TableName return database table name for xorm
|
||||
func (pullAutoMerge) TableName() string {
|
||||
return "pull_auto_merge"
|
||||
}
|
||||
|
||||
func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error {
|
||||
return x.Sync(new(pullAutoMerge))
|
||||
}
|
@ -16,6 +16,31 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type OrgList []*Organization
|
||||
|
||||
func (orgs OrgList) LoadTeams(ctx context.Context) (map[int64]TeamList, error) {
|
||||
if len(orgs) == 0 {
|
||||
return map[int64]TeamList{}, nil
|
||||
}
|
||||
|
||||
orgIDs := make([]int64, len(orgs))
|
||||
for i, org := range orgs {
|
||||
orgIDs[i] = org.ID
|
||||
}
|
||||
|
||||
teams, err := GetTeamsByOrgIDs(ctx, orgIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamMap := make(map[int64]TeamList, len(orgs))
|
||||
for _, team := range teams {
|
||||
teamMap[team.OrgID] = append(teamMap[team.OrgID], team)
|
||||
}
|
||||
|
||||
return teamMap, nil
|
||||
}
|
||||
|
||||
// SearchOrganizationsOptions options to filter organizations
|
||||
type SearchOrganizationsOptions struct {
|
||||
db.ListOptions
|
||||
|
@ -60,3 +60,14 @@ func TestGetUserOrgsList(t *testing.T) {
|
||||
assert.EqualValues(t, 2, orgs[0].NumRepos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOrgListTeams(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, orgs, 1)
|
||||
teamsMap, err := organization.OrgList(orgs).LoadTeams(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teamsMap, 1)
|
||||
assert.Len(t, teamsMap[3], 5)
|
||||
}
|
||||
|
@ -126,3 +126,8 @@ func GetUserRepoTeams(ctx context.Context, orgID, userID, repoID int64) (teams T
|
||||
And("team_repo.repo_id=?", repoID).
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) {
|
||||
teams := make([]*Team, 0, 10)
|
||||
return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@ -83,3 +84,16 @@ func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode per
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Join("INNER", "team_unit", "team_unit.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_unit.type = ?", unitType).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
}
|
||||
|
31
models/organization/team_repo_test.go
Normal file
31
models/organization/team_repo_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
||||
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 2) {
|
||||
assert.EqualValues(t, 21, teams[0].ID)
|
||||
assert.EqualValues(t, 22, teams[1].ID)
|
||||
}
|
||||
}
|
@ -197,3 +197,8 @@ func TestUsersInTeamsCount(t *testing.T) {
|
||||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 5}, 2) // userid 2,4
|
||||
test([]int64{1, 2, 3, 4, 5}, []int64{2, 3, 5}, 3) // userid 2,4,5
|
||||
}
|
||||
|
||||
func TestIsUsableTeamName(t *testing.T) {
|
||||
assert.NoError(t, organization.IsUsableTeamName("usable"))
|
||||
assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new")))
|
||||
}
|
||||
|
38
models/packages/arch/search.go
Normal file
38
models/packages/arch/search.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
arch_module "code.gitea.io/gitea/modules/packages/arch"
|
||||
)
|
||||
|
||||
// GetRepositories gets all available repositories
|
||||
func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) {
|
||||
return packages_model.GetDistinctPropertyValues(
|
||||
ctx,
|
||||
packages_model.TypeArch,
|
||||
ownerID,
|
||||
packages_model.PropertyTypeFile,
|
||||
arch_module.PropertyRepository,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// GetArchitectures gets all available architectures for the given repository
|
||||
func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) {
|
||||
return packages_model.GetDistinctPropertyValues(
|
||||
ctx,
|
||||
packages_model.TypeArch,
|
||||
ownerID,
|
||||
packages_model.PropertyTypeFile,
|
||||
arch_module.PropertyArchitecture,
|
||||
&packages_model.DistinctPropertyDependency{
|
||||
Name: arch_module.PropertyRepository,
|
||||
Value: repository,
|
||||
},
|
||||
)
|
||||
}
|
@ -13,6 +13,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/alpine"
|
||||
"code.gitea.io/gitea/modules/packages/arch"
|
||||
"code.gitea.io/gitea/modules/packages/cargo"
|
||||
"code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/packages/composer"
|
||||
@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
switch p.Type {
|
||||
case TypeAlpine:
|
||||
metadata = &alpine.VersionMetadata{}
|
||||
case TypeArch:
|
||||
metadata = &arch.VersionMetadata{}
|
||||
case TypeCargo:
|
||||
metadata = &cargo.Metadata{}
|
||||
case TypeChef:
|
||||
|
@ -31,6 +31,7 @@ type Type string
|
||||
// List of supported packages
|
||||
const (
|
||||
TypeAlpine Type = "alpine"
|
||||
TypeArch Type = "arch"
|
||||
TypeCargo Type = "cargo"
|
||||
TypeChef Type = "chef"
|
||||
TypeComposer Type = "composer"
|
||||
@ -55,6 +56,7 @@ const (
|
||||
|
||||
var TypeList = []Type{
|
||||
TypeAlpine,
|
||||
TypeArch,
|
||||
TypeCargo,
|
||||
TypeChef,
|
||||
TypeComposer,
|
||||
@ -82,6 +84,8 @@ func (pt Type) Name() string {
|
||||
switch pt {
|
||||
case TypeAlpine:
|
||||
return "Alpine"
|
||||
case TypeArch:
|
||||
return "Arch"
|
||||
case TypeCargo:
|
||||
return "Cargo"
|
||||
case TypeChef:
|
||||
@ -131,6 +135,8 @@ func (pt Type) SVGName() string {
|
||||
switch pt {
|
||||
case TypeAlpine:
|
||||
return "gitea-alpine"
|
||||
case TypeArch:
|
||||
return "gitea-arch"
|
||||
case TypeCargo:
|
||||
return "gitea-cargo"
|
||||
case TypeChef:
|
||||
|
@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag
|
||||
return pfs, count, err
|
||||
}
|
||||
|
||||
// HasFiles tests if there are files of packages matching the search options
|
||||
func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) {
|
||||
return db.Exist[PackageFile](ctx, opts.toConds())
|
||||
}
|
||||
|
||||
// CalculateFileSize sums up all blob sizes matching the search options.
|
||||
// It does NOT respect the deduplication of blobs.
|
||||
func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {
|
||||
|
53
models/renderhelper/commit_checker.go
Normal file
53
models/renderhelper/commit_checker.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
type commitChecker struct {
|
||||
ctx context.Context
|
||||
commitCache map[string]bool
|
||||
gitRepoFacade gitrepo.Repository
|
||||
|
||||
gitRepo *git.Repository
|
||||
gitRepoCloser io.Closer
|
||||
}
|
||||
|
||||
func newCommitChecker(ctx context.Context, gitRepo gitrepo.Repository) *commitChecker {
|
||||
return &commitChecker{ctx: ctx, commitCache: make(map[string]bool), gitRepoFacade: gitRepo}
|
||||
}
|
||||
|
||||
func (c *commitChecker) Close() error {
|
||||
if c != nil && c.gitRepoCloser != nil {
|
||||
return c.gitRepoCloser.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
|
||||
exist, inCache := c.commitCache[commitID]
|
||||
if inCache {
|
||||
return exist
|
||||
}
|
||||
|
||||
if c.gitRepo == nil {
|
||||
r, closer, err := gitrepo.RepositoryFromContextOrOpen(c.ctx, c.gitRepoFacade)
|
||||
if err != nil {
|
||||
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(c.gitRepoFacade), err)
|
||||
return false
|
||||
}
|
||||
c.gitRepo, c.gitRepoCloser = r, closer
|
||||
}
|
||||
|
||||
exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
|
||||
c.commitCache[commitID] = exist
|
||||
return exist
|
||||
}
|
27
models/renderhelper/main_test.go
Normal file
27
models/renderhelper/main_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
FixtureFiles: []string{"repository.yml", "user.yml"},
|
||||
SetUp: func() error {
|
||||
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
|
||||
markup.Init(&markup.RenderHelperFuncs{
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
return username == "user2"
|
||||
},
|
||||
})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
73
models/renderhelper/repo_comment.go
Normal file
73
models/renderhelper/repo_comment.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type RepoComment struct {
|
||||
ctx *markup.RenderContext
|
||||
opts RepoCommentOptions
|
||||
|
||||
commitChecker *commitChecker
|
||||
repoLink string
|
||||
}
|
||||
|
||||
func (r *RepoComment) CleanUp() {
|
||||
_ = r.commitChecker.Close()
|
||||
}
|
||||
|
||||
func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
default:
|
||||
finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
|
||||
}
|
||||
return finalLink
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*RepoComment)(nil)
|
||||
|
||||
type RepoCommentOptions struct {
|
||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
||||
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||
CurrentRefPath string // eg: "branch/main" or "commit/11223344"
|
||||
}
|
||||
|
||||
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
|
||||
helper := &RepoComment{
|
||||
repoLink: repo.Link(),
|
||||
opts: util.OptionalArg(opts),
|
||||
}
|
||||
rctx := markup.NewRenderContext(ctx)
|
||||
helper.ctx = rctx
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||
rctx = rctx.WithMetas(map[string]string{
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "comment",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
return rctx
|
||||
}
|
76
models/renderhelper/repo_comment_test.go
Normal file
76
models/renderhelper/repo_comment_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoComment(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
t.Run("AutoLink", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
@user2
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a><br/>
|
||||
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a><br/>
|
||||
<a href="/user2" rel="nofollow">@user2</a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("AbsoluteAndRelative", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoComment(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
|
||||
// It is Gitea's old behavior, the relative path is resolved to the repo path
|
||||
// It is different from GitHub, GitHub resolves relative links to current page's path
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[/test](/test)
|
||||
[./test](./test)
|
||||
![/image](/image)
|
||||
![./image](./image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
|
||||
<a href="/user2/repo1/test" rel="nofollow">./test</a><br/>
|
||||
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
|
||||
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("WithCurrentRefPath", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoComment(context.Background(), repo1, RepoCommentOptions{CurrentRefPath: "/commit/1234"}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
|
||||
// the ref path is only used to render commit message: a commit message is rendered at the commit page with its commit ID path
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[/test](/test)
|
||||
[./test](./test)
|
||||
![/image](/image)
|
||||
![./image](./image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<p><a href="/user2/repo1/test" rel="nofollow">/test</a><br/>
|
||||
<a href="/user2/repo1/commit/1234/test" rel="nofollow">./test</a><br/>
|
||||
<a href="/user2/repo1/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/image" alt="/image"/></a><br/>
|
||||
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
}
|
77
models/renderhelper/repo_file.go
Normal file
77
models/renderhelper/repo_file.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type RepoFile struct {
|
||||
ctx *markup.RenderContext
|
||||
opts RepoFileOptions
|
||||
|
||||
commitChecker *commitChecker
|
||||
repoLink string
|
||||
}
|
||||
|
||||
func (r *RepoFile) CleanUp() {
|
||||
_ = r.commitChecker.Close()
|
||||
}
|
||||
|
||||
func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
finalLink := link
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
case markup.LinkTypeDefault:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
case markup.LinkTypeRaw:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
case markup.LinkTypeMedia:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
|
||||
}
|
||||
return finalLink
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*RepoFile)(nil)
|
||||
|
||||
type RepoFileOptions struct {
|
||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
||||
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||
|
||||
CurrentRefPath string // eg: "branch/main"
|
||||
CurrentTreePath string // eg: "path/to/file" in the repo
|
||||
}
|
||||
|
||||
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
|
||||
helper := &RepoFile{opts: util.OptionalArg(opts)}
|
||||
rctx := markup.NewRenderContext(ctx)
|
||||
helper.ctx = rctx
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||
rctx = rctx.WithMetas(map[string]string{
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "document",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
return rctx
|
||||
}
|
122
models/renderhelper/repo_file_test.go
Normal file
122
models/renderhelper/repo_file_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
_ "code.gitea.io/gitea/modules/markup/orgmode"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoFile(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
t.Run("AutoLink", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
@user2
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
|
||||
#1
|
||||
<a href="/user2" rel="nofollow">@user2</a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("AbsoluteAndRelative", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "branch/main"}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[/test](/test)
|
||||
[./test](./test)
|
||||
![/image](/image)
|
||||
![./image](./image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
|
||||
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("WithCurrentRefPath", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{CurrentRefPath: "/commit/1234"}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[/test](/test)
|
||||
![/image](/image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("WithCurrentRefPathByTag", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{
|
||||
CurrentRefPath: "/commit/1234",
|
||||
CurrentTreePath: "my-dir",
|
||||
}).
|
||||
WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
<img src="LINK">
|
||||
<video src="LINK">
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
|
||||
<video src="/user2/repo1/media/commit/1234/my-dir/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepoFileOrgMode(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
t.Run("Links", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{
|
||||
CurrentRefPath: "/commit/1234",
|
||||
CurrentTreePath: "my-dir",
|
||||
}).WithRelativePath("my-dir/a.org")
|
||||
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[[https://google.com/]]
|
||||
[[ImageLink.svg][The Image Desc]]
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<p>
|
||||
<a href="https://google.com/" rel="nofollow">https://google.com/</a>
|
||||
<a href="/user2/repo1/media/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("CodeHighlight", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoFile(context.Background(), repo1, RepoFileOptions{}).WithRelativePath("my-dir/a.org")
|
||||
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
#+begin_src c
|
||||
int a = 1;
|
||||
#+end_src
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<div>
|
||||
<pre><code class="chroma language-c"><span class="kt">int</span> <span class="n">a</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span></code></pre>
|
||||
</div>
|
||||
`, rendered)
|
||||
})
|
||||
}
|
80
models/renderhelper/repo_wiki.go
Normal file
80
models/renderhelper/repo_wiki.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type RepoWiki struct {
|
||||
ctx *markup.RenderContext
|
||||
opts RepoWikiOptions
|
||||
|
||||
commitChecker *commitChecker
|
||||
repoLink string
|
||||
}
|
||||
|
||||
func (r *RepoWiki) CleanUp() {
|
||||
_ = r.commitChecker.Close()
|
||||
}
|
||||
|
||||
func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
|
||||
return r.commitChecker.IsCommitIDExisting(commitID)
|
||||
}
|
||||
|
||||
func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
finalLink := link
|
||||
switch likeType {
|
||||
case markup.LinkTypeApp:
|
||||
finalLink = r.ctx.ResolveLinkApp(link)
|
||||
case markup.LinkTypeDefault:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
|
||||
case markup.LinkTypeMedia:
|
||||
finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
|
||||
case markup.LinkTypeRaw: // wiki doesn't use it
|
||||
}
|
||||
|
||||
return finalLink
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*RepoWiki)(nil)
|
||||
|
||||
type RepoWikiOptions struct {
|
||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
||||
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||
|
||||
// these options are not used at the moment because Wiki doesn't support sub-path, nor branch
|
||||
currentRefPath string // eg: "branch/main"
|
||||
currentTreePath string // eg: "path/to/file" in the repo
|
||||
}
|
||||
|
||||
func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, opts ...RepoWikiOptions) *markup.RenderContext {
|
||||
helper := &RepoWiki{opts: util.OptionalArg(opts)}
|
||||
rctx := markup.NewRenderContext(ctx).WithMarkupType(markdown.MarkupName)
|
||||
if repo != nil {
|
||||
helper.repoLink = repo.Link()
|
||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||
rctx = rctx.WithMetas(repo.ComposeWikiMetas(ctx))
|
||||
} else {
|
||||
// this is almost dead code, only to pass the incorrect tests
|
||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||
rctx = rctx.WithMetas(map[string]string{
|
||||
"user": helper.opts.DeprecatedOwnerName,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
|
||||
"markdownLineBreakStyle": "document",
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
})
|
||||
}
|
||||
rctx = rctx.WithHelper(helper)
|
||||
helper.ctx = rctx
|
||||
return rctx
|
||||
}
|
65
models/renderhelper/repo_wiki_test.go
Normal file
65
models/renderhelper/repo_wiki_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoWiki(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
t.Run("AutoLink", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
@user2
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow"><code>65f1bf27bc</code></a>
|
||||
<a href="/user2/repo1/issues/1" class="ref-issue" rel="nofollow">#1</a>
|
||||
<a href="/user2" rel="nofollow">@user2</a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("AbsoluteAndRelative", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
[/test](/test)
|
||||
[./test](./test)
|
||||
![/image](/image)
|
||||
![./image](./image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
|
||||
<a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
|
||||
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
|
||||
<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
})
|
||||
|
||||
t.Run("PathInTag", func(t *testing.T) {
|
||||
rctx := NewRenderContextRepoWiki(context.Background(), repo1).WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
<img src="LINK">
|
||||
<video src="LINK">
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
|
||||
<video src="/user2/repo1/wiki/raw/LINK">
|
||||
</video>`, rendered)
|
||||
})
|
||||
}
|
29
models/renderhelper/simple_document.go
Normal file
29
models/renderhelper/simple_document.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
)
|
||||
|
||||
type SimpleDocument struct {
|
||||
*markup.SimpleRenderHelper
|
||||
ctx *markup.RenderContext
|
||||
baseLink string
|
||||
}
|
||||
|
||||
func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
|
||||
return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
|
||||
}
|
||||
|
||||
var _ markup.RenderHelper = (*SimpleDocument)(nil)
|
||||
|
||||
func NewRenderContextSimpleDocument(ctx context.Context, baseLink string) *markup.RenderContext {
|
||||
helper := &SimpleDocument{baseLink: baseLink}
|
||||
rctx := markup.NewRenderContext(ctx).WithHelper(helper).WithMetas(markup.ComposeSimpleDocumentMetas())
|
||||
helper.ctx = rctx
|
||||
return rctx
|
||||
}
|
40
models/renderhelper/simple_document_test.go
Normal file
40
models/renderhelper/simple_document_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package renderhelper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSimpleDocument(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
rctx := NewRenderContextSimpleDocument(context.Background(), "/base").WithMarkupType(markdown.MarkupName)
|
||||
rendered, err := markup.RenderString(rctx, `
|
||||
65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
@user2
|
||||
|
||||
[/test](/test)
|
||||
[./test](./test)
|
||||
![/image](/image)
|
||||
![./image](./image)
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
|
||||
#1
|
||||
<a href="/base/user2" rel="nofollow">@user2</a></p>
|
||||
<p><a href="/base/test" rel="nofollow">/test</a>
|
||||
<a href="/base/test" rel="nofollow">./test</a>
|
||||
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>
|
||||
<a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="./image"/></a></p>
|
||||
`, rendered)
|
||||
}
|
@ -617,10 +617,7 @@ func (repo *Repository) CanEnableEditor() bool {
|
||||
|
||||
// DescriptionHTML does special handles to description and return HTML string.
|
||||
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
|
||||
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
// Don't use Metas to speedup requests
|
||||
}, repo.Description)
|
||||
desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
|
||||
if err != nil {
|
||||
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
|
||||
return template.HTML(markup.SanitizeDescription(repo.Description))
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@ -146,57 +145,6 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetReviewers get all users can be requested to review:
|
||||
// * for private repositories this returns all users that have read access or higher to the repository.
|
||||
// * for public repositories this returns all users that have read access or higher to the repository,
|
||||
// all repo watchers and all organization members.
|
||||
// TODO: may be we should have a busy choice for users to block review request to them.
|
||||
func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) {
|
||||
// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cond := builder.And(builder.Neq{"`user`.id": posterID}).
|
||||
And(builder.Eq{"`user`.is_active": true})
|
||||
|
||||
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
|
||||
// This a private repository:
|
||||
// Anyone who can read the repository is a requestable reviewer
|
||||
|
||||
cond = cond.And(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("access").Where(
|
||||
builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.Gte{"mode": perm.AccessModeRead}),
|
||||
),
|
||||
))
|
||||
|
||||
if repo.Owner.Type == user_model.UserTypeIndividual && repo.Owner.ID != posterID {
|
||||
// as private *user* repos don't generate an entry in the `access` table,
|
||||
// the owner of a private repo needs to be explicitly added.
|
||||
cond = cond.Or(builder.Eq{"`user`.id": repo.Owner.ID})
|
||||
}
|
||||
} else {
|
||||
// This is a "public" repository:
|
||||
// Any user that has read access, is a watcher or organization member can be requested to review
|
||||
cond = cond.And(builder.And(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("access").
|
||||
Where(builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.Gte{"mode": perm.AccessModeRead})),
|
||||
).Or(builder.In("`user`.id",
|
||||
builder.Select("user_id").From("watch").
|
||||
Where(builder.Eq{"repo_id": repo.ID}.
|
||||
And(builder.In("mode", WatchModeNormal, WatchModeAuto))),
|
||||
).Or(builder.In("`user`.id",
|
||||
builder.Select("uid").From("org_user").
|
||||
Where(builder.Eq{"org_id": repo.OwnerID}),
|
||||
)))))
|
||||
}
|
||||
|
||||
users := make([]*user_model.User, 0, 8)
|
||||
return users, db.GetEngine(ctx).Where(cond).OrderBy(user_model.GetOrderByName()).Find(&users)
|
||||
}
|
||||
|
||||
// GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository
|
||||
// If isShowFullName is set to true, also include full name prefix search
|
||||
func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) {
|
||||
|
@ -38,46 +38,3 @@ func TestRepoAssignees(t *testing.T) {
|
||||
assert.NotContains(t, []int64{users[0].ID, users[1].ID, users[2].ID}, 15)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoGetReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// test public repo
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
ctx := db.DefaultContext
|
||||
reviewers, err := repo_model.GetReviewers(ctx, repo1, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, reviewers, 3) {
|
||||
assert.ElementsMatch(t, []int64{1, 4, 11}, []int64{reviewers[0].ID, reviewers[1].ID, reviewers[2].ID})
|
||||
}
|
||||
|
||||
// should include doer if doer is not PR poster.
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 3)
|
||||
|
||||
// should not include PR poster, if PR poster would be otherwise eligible
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo1, 11, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
// test private user repo
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo2, 2, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
assert.EqualValues(t, reviewers[0].ID, 2)
|
||||
|
||||
// test private org repo
|
||||
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 2)
|
||||
|
||||
reviewers, err = repo_model.GetReviewers(ctx, repo3, 2, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewers, 1)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@ -64,10 +65,10 @@ func BeanExists(t assert.TestingT, bean any, conditions ...any) bool {
|
||||
}
|
||||
|
||||
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
|
||||
func AssertExistsAndLoadBean[T any](t assert.TestingT, bean T, conditions ...any) T {
|
||||
func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
|
||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists,
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists,
|
||||
"Expected to find %+v (of type %T, with conditions %+v), but did not",
|
||||
bean, bean, conditions)
|
||||
return bean
|
||||
|
@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
|
||||
|
||||
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
|
||||
defer sessQuery.Close()
|
||||
if opts.Page != 0 {
|
||||
if opts.Page > 0 {
|
||||
sessQuery = db.SetSessionPagination(sessQuery, opts)
|
||||
}
|
||||
|
||||
|
@ -330,7 +330,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO
|
||||
And("`user`.type=?", UserTypeIndividual).
|
||||
And(isUserVisibleToViewerCond(viewer))
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
users := make([]*User, 0, listOptions.PageSize)
|
||||
@ -352,7 +352,7 @@ func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListO
|
||||
And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).
|
||||
And(isUserVisibleToViewerCond(viewer))
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
if listOptions.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
|
||||
users := make([]*User, 0, listOptions.PageSize)
|
||||
|
@ -4,13 +4,14 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
@ -38,40 +39,42 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
// User represents an implementation of webauthn.User based on User model
|
||||
type User user_model.User
|
||||
// user represents an implementation of webauthn.User based on User model
|
||||
type user struct {
|
||||
ctx context.Context
|
||||
User *user_model.User
|
||||
|
||||
defaultAuthFlags protocol.AuthenticatorFlags
|
||||
}
|
||||
|
||||
var _ webauthn.User = (*user)(nil)
|
||||
|
||||
func NewWebAuthnUser(ctx context.Context, u *user_model.User, defaultAuthFlags ...protocol.AuthenticatorFlags) webauthn.User {
|
||||
return &user{ctx: ctx, User: u, defaultAuthFlags: util.OptionalArg(defaultAuthFlags)}
|
||||
}
|
||||
|
||||
// WebAuthnID implements the webauthn.User interface
|
||||
func (u *User) WebAuthnID() []byte {
|
||||
func (u *user) WebAuthnID() []byte {
|
||||
id := make([]byte, 8)
|
||||
binary.PutVarint(id, u.ID)
|
||||
binary.PutVarint(id, u.User.ID)
|
||||
return id
|
||||
}
|
||||
|
||||
// WebAuthnName implements the webauthn.User interface
|
||||
func (u *User) WebAuthnName() string {
|
||||
if u.LoginName == "" {
|
||||
return u.Name
|
||||
}
|
||||
return u.LoginName
|
||||
func (u *user) WebAuthnName() string {
|
||||
return util.IfZero(u.User.LoginName, u.User.Name)
|
||||
}
|
||||
|
||||
// WebAuthnDisplayName implements the webauthn.User interface
|
||||
func (u *User) WebAuthnDisplayName() string {
|
||||
return (*user_model.User)(u).DisplayName()
|
||||
}
|
||||
|
||||
// WebAuthnIcon implements the webauthn.User interface
|
||||
func (u *User) WebAuthnIcon() string {
|
||||
return (*user_model.User)(u).AvatarLink(db.DefaultContext)
|
||||
func (u *user) WebAuthnDisplayName() string {
|
||||
return u.User.DisplayName()
|
||||
}
|
||||
|
||||
// WebAuthnCredentials implements the webauthn.User interface
|
||||
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||
dbCreds, err := auth.GetWebAuthnCredentialsByUID(db.DefaultContext, u.ID)
|
||||
func (u *user) WebAuthnCredentials() []webauthn.Credential {
|
||||
dbCreds, err := auth.GetWebAuthnCredentialsByUID(u.ctx, u.User.ID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return dbCreds.ToCredentials()
|
||||
return dbCreds.ToCredentials(u.defaultAuthFlags)
|
||||
}
|
||||
|
2
modules/cache/cache_test.go
vendored
2
modules/cache/cache_test.go
vendored
@ -43,7 +43,7 @@ func TestTest(t *testing.T) {
|
||||
elapsed, err := Test()
|
||||
assert.NoError(t, err)
|
||||
// mem cache should take from 300ns up to 1ms on modern hardware ...
|
||||
assert.Less(t, elapsed, SlowCacheThreshold)
|
||||
assert.Less(t, elapsed, time.Millisecond)
|
||||
}
|
||||
|
||||
func TestGetCache(t *testing.T) {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"bytes"
|
||||
stdcsv "encoding/csv"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@ -53,7 +53,7 @@ func CreateReaderAndDetermineDelimiter(ctx *markup.RenderContext, rd io.Reader)
|
||||
func determineDelimiter(ctx *markup.RenderContext, data []byte) rune {
|
||||
extension := ".csv"
|
||||
if ctx != nil {
|
||||
extension = strings.ToLower(filepath.Ext(ctx.RelativePath))
|
||||
extension = strings.ToLower(path.Ext(ctx.RenderOptions.RelativePath))
|
||||
}
|
||||
|
||||
var delimiter rune
|
||||
|
@ -5,13 +5,13 @@ package csv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
@ -231,10 +231,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimiters`,
|
||||
}
|
||||
|
||||
for n, c := range cases {
|
||||
delimiter := determineDelimiter(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: c.filename,
|
||||
}, []byte(decodeSlashes(t, c.csv)))
|
||||
delimiter := determineDelimiter(markup.NewRenderContext(context.Background()).WithRelativePath(c.filename), []byte(decodeSlashes(t, c.csv)))
|
||||
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,16 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||
// GetRefCommitID returns the last commit ID string of given reference.
|
||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
|
||||
ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true)
|
||||
if plumbing.IsHash(name) {
|
||||
return name, nil
|
||||
}
|
||||
refName := plumbing.ReferenceName(name)
|
||||
if err := refName.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ref, err := repo.gogitRepo.Reference(refName, true)
|
||||
if err != nil {
|
||||
if err == plumbing.ErrReferenceNotFound {
|
||||
return "", ErrNotExist{
|
||||
|
@ -101,3 +101,28 @@ func TestRepository_CommitsBetweenIDs(t *testing.T) {
|
||||
assert.Len(t, commits, c.ExpectedCommits, "case %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRefCommitID(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
||||
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
|
||||
assert.NoError(t, err)
|
||||
defer bareRepo1.Close()
|
||||
|
||||
// these test case are specific to the repo1_bare test repo
|
||||
testCases := []struct {
|
||||
Ref string
|
||||
ExpectedCommitID string
|
||||
}{
|
||||
{RefNameFromBranch("master").String(), "ce064814f4a0d337b333e646ece456cd39fab612"},
|
||||
{RefNameFromBranch("branch1").String(), "2839944139e0de9737a044f78b0e4b40d989a9e3"},
|
||||
{RefNameFromTag("test").String(), "3ad28a9149a2864384548f3d17ed7f38014c9e8a"},
|
||||
{"ce064814f4a0d337b333e646ece456cd39fab612", "ce064814f4a0d337b333e646ece456cd39fab612"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
commitID, err := bareRepo1.GetRefCommitID(testCase.Ref)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, testCase.ExpectedCommitID, commitID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,6 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type codeSearchResult struct {
|
||||
|
@ -44,10 +44,10 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error {
|
||||
rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s",
|
||||
setting.AppSubURL,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
ctx.RenderOptions.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RenderOptions.RelativePath),
|
||||
)
|
||||
return ctx.RenderInternal.FormatWithSafeAttrs(output, `<div class="%s" %s="%s"></div>`, playerClassName, playerSrcAttr, rawURL)
|
||||
}
|
||||
|
@ -4,10 +4,10 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -24,8 +24,7 @@ func TestRenderConsole(t *testing.T) {
|
||||
canRender := render.CanRender("test", strings.NewReader(k))
|
||||
assert.True(t, canRender)
|
||||
|
||||
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
|
||||
strings.NewReader(k), &buf)
|
||||
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, v, buf.String())
|
||||
}
|
||||
|
@ -133,10 +133,10 @@ func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.W
|
||||
// Check if maxRows or maxSize is reached, and if true, warn.
|
||||
if (row >= maxRows && maxRows != 0) || (rd.InputOffset() >= maxSize && maxSize != 0) {
|
||||
warn := `<table class="data-table"><tr><td>`
|
||||
rawLink := ` <a href="` + ctx.Links.RawLink() + `/` + util.PathEscapeSegments(ctx.RelativePath) + `">`
|
||||
rawLink := ` <a href="` + ctx.RenderHelper.ResolveLink(util.PathEscapeSegments(ctx.RenderOptions.RelativePath), markup.LinkTypeRaw) + `">`
|
||||
|
||||
// Try to get the user translation
|
||||
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
warn += locale.TrString("repo.file_too_large")
|
||||
rawLink += locale.TrString("repo.file_view_raw")
|
||||
} else {
|
||||
|
@ -4,10 +4,10 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -24,8 +24,7 @@ func TestRenderCSV(t *testing.T) {
|
||||
|
||||
for k, v := range kases {
|
||||
var buf strings.Builder
|
||||
err := render.Render(&markup.RenderContext{Ctx: git.DefaultContext},
|
||||
strings.NewReader(k), &buf)
|
||||
err := render.Render(markup.NewRenderContext(context.Background()), strings.NewReader(k), &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, v, buf.String())
|
||||
}
|
||||
|
19
modules/markup/external/external.go
vendored
19
modules/markup/external/external.go
vendored
@ -12,7 +12,6 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
@ -80,8 +79,8 @@ func envMark(envName string) string {
|
||||
func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
var (
|
||||
command = strings.NewReplacer(
|
||||
envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
|
||||
envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
|
||||
envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
|
||||
envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
|
||||
).Replace(p.Command)
|
||||
commands = strings.Fields(command)
|
||||
args = commands[1:]
|
||||
@ -113,22 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
args = append(args, f.Name())
|
||||
}
|
||||
|
||||
if ctx.Ctx == nil {
|
||||
if !setting.IsProd || setting.IsInTesting {
|
||||
panic("RenderContext did not provide context")
|
||||
}
|
||||
log.Warn("RenderContext did not provide context, defaulting to Shutdown context")
|
||||
ctx.Ctx = graceful.GetManager().ShutdownContext()
|
||||
}
|
||||
|
||||
processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
|
||||
processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)))
|
||||
defer finished()
|
||||
|
||||
cmd := exec.CommandContext(processCtx, commands[0], args...)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
|
||||
"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
|
||||
"GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
|
||||
"GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
|
||||
)
|
||||
if !p.IsInputFile {
|
||||
cmd.Stdin = input
|
||||
|
@ -5,9 +5,9 @@ package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -133,75 +133,49 @@ func CustomLinkURLSchemes(schemes []string) {
|
||||
common.GlobalVars().LinkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|"))
|
||||
}
|
||||
|
||||
type postProcessError struct {
|
||||
context string
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *postProcessError) Error() string {
|
||||
return "PostProcess: " + p.context + ", " + p.err.Error()
|
||||
}
|
||||
|
||||
type processor func(ctx *RenderContext, node *html.Node)
|
||||
|
||||
var defaultProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
codePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emailAddressProcessor,
|
||||
emojiProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
}
|
||||
|
||||
// PostProcess does the final required transformations to the passed raw HTML
|
||||
// PostProcessDefault does the final required transformations to the passed raw HTML
|
||||
// data, and ensures its validity. Transformations include: replacing links and
|
||||
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
|
||||
// MediaWiki, linking issues in the format #ID, and mentions in the format
|
||||
// @user, and others.
|
||||
func PostProcess(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
return postProcess(ctx, defaultProcessors, input, output)
|
||||
func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
codePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emailAddressProcessor,
|
||||
emojiProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
}
|
||||
return postProcess(ctx, procs, input, output)
|
||||
}
|
||||
|
||||
var commitMessageProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emailAddressProcessor,
|
||||
emojiProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
}
|
||||
|
||||
// RenderCommitMessage will use the same logic as PostProcess, but will disable
|
||||
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
|
||||
// set, which changes every text node into a link to the passed default link.
|
||||
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) {
|
||||
procs := commitMessageProcessors
|
||||
return renderProcessString(ctx, procs, content)
|
||||
}
|
||||
|
||||
var commitMessageSubjectProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
emojiProcessor,
|
||||
// PostProcessCommitMessage will use the same logic as PostProcess, but will disable
|
||||
// the shortLinkProcessor.
|
||||
func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emailAddressProcessor,
|
||||
emojiProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
}
|
||||
return postProcessString(ctx, procs, content)
|
||||
}
|
||||
|
||||
var emojiProcessors = []processor{
|
||||
@ -209,12 +183,23 @@ var emojiProcessors = []processor{
|
||||
emojiProcessor,
|
||||
}
|
||||
|
||||
// RenderCommitMessageSubject will use the same logic as PostProcess and
|
||||
// RenderCommitMessage, but will disable the shortLinkProcessor and
|
||||
// PostProcessCommitMessageSubject will use the same logic as PostProcess and
|
||||
// PostProcessCommitMessage, but will disable the shortLinkProcessor and
|
||||
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
|
||||
// which changes every text node into a link to the passed default link.
|
||||
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
|
||||
procs := slices.Clone(commitMessageSubjectProcessors)
|
||||
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
|
||||
procs := []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
linkProcessor,
|
||||
mentionProcessor,
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
emojiProcessor,
|
||||
}
|
||||
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
|
||||
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
|
||||
node.Type = html.ElementNode
|
||||
@ -223,19 +208,37 @@ func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string)
|
||||
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}}
|
||||
node.FirstChild, node.LastChild = ch, ch
|
||||
})
|
||||
return renderProcessString(ctx, procs, content)
|
||||
return postProcessString(ctx, procs, content)
|
||||
}
|
||||
|
||||
// RenderIssueTitle to process title on individual issue/pull page
|
||||
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) {
|
||||
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
|
||||
return renderProcessString(ctx, []processor{
|
||||
// PostProcessIssueTitle to process title on individual issue/pull page
|
||||
func PostProcessIssueTitle(ctx *RenderContext, title string) (string, error) {
|
||||
return postProcessString(ctx, []processor{
|
||||
issueIndexPatternProcessor,
|
||||
commitCrossReferencePatternProcessor,
|
||||
hashCurrentPatternProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
emojiProcessor,
|
||||
}, title)
|
||||
}
|
||||
|
||||
func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
|
||||
// PostProcessDescriptionHTML will use similar logic as PostProcess, but will
|
||||
// use a single special linkProcessor.
|
||||
func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, error) {
|
||||
return postProcessString(ctx, []processor{
|
||||
descriptionLinkProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
emojiProcessor,
|
||||
}, content)
|
||||
}
|
||||
|
||||
// PostProcessEmoji for when we want to just process emoji and shortcodes
|
||||
// in various places it isn't already run through the normal markdown processor
|
||||
func PostProcessEmoji(ctx *RenderContext, content string) (string, error) {
|
||||
return postProcessString(ctx, emojiProcessors, content)
|
||||
}
|
||||
|
||||
func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
|
||||
var buf strings.Builder
|
||||
if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
|
||||
return "", err
|
||||
@ -243,24 +246,10 @@ func renderProcessString(ctx *RenderContext, procs []processor, content string)
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// RenderDescriptionHTML will use similar logic as PostProcess, but will
|
||||
// use a single special linkProcessor.
|
||||
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
|
||||
return renderProcessString(ctx, []processor{
|
||||
descriptionLinkProcessor,
|
||||
emojiShortCodeProcessor,
|
||||
emojiProcessor,
|
||||
}, content)
|
||||
}
|
||||
|
||||
// RenderEmoji for when we want to just process emoji and shortcodes
|
||||
// in various places it isn't already run through the normal markdown processor
|
||||
func RenderEmoji(ctx *RenderContext, content string) (string, error) {
|
||||
return renderProcessString(ctx, emojiProcessors, content)
|
||||
}
|
||||
|
||||
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
|
||||
defer ctx.Cancel()
|
||||
if !ctx.usedByRender && ctx.RenderHelper != nil {
|
||||
defer ctx.RenderHelper.CleanUp()
|
||||
}
|
||||
// FIXME: don't read all content to memory
|
||||
rawHTML, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
@ -277,7 +266,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||
strings.NewReader("</body></html>"),
|
||||
))
|
||||
if err != nil {
|
||||
return &postProcessError{"invalid HTML", err}
|
||||
return fmt.Errorf("markup.postProcess: invalid HTML: %w", err)
|
||||
}
|
||||
|
||||
if node.Type == html.DocumentNode {
|
||||
@ -309,7 +298,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||
// Render everything to buf.
|
||||
for _, node := range newNodes {
|
||||
if err := html.Render(output, node); err != nil {
|
||||
return &postProcessError{"error rendering processed HTML", err}
|
||||
return fmt.Errorf("markup.postProcess: html.Render: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -396,7 +385,7 @@ func createLink(ctx *RenderContext, href, content, class string) *html.Node {
|
||||
Data: atom.A.String(),
|
||||
Attr: []html.Attribute{{Key: "href", Val: href}},
|
||||
}
|
||||
if !RenderBehaviorForTesting.DisableInternalAttributes {
|
||||
if !RenderBehaviorForTesting.DisableAdditionalAttributes {
|
||||
a.Attr = append(a.Attr, html.Attribute{Key: "data-markdown-generated-content"})
|
||||
}
|
||||
if class != "" {
|
||||
|
@ -38,7 +38,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
|
||||
CommitID: node.Data[m[6]:m[7]],
|
||||
FilePath: node.Data[m[8]:m[9]],
|
||||
}
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, opts.FullURL) {
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, opts.FullURL) {
|
||||
return 0, 0, "", nil
|
||||
}
|
||||
u, err := url.Parse(opts.FilePath)
|
||||
@ -51,7 +51,7 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt
|
||||
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
|
||||
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
|
||||
opts.LineStart, opts.LineStop = lineStart, lineStop
|
||||
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
|
||||
h, err := DefaultRenderHelperFuncs.RenderRepoFileCodePreview(ctx, opts)
|
||||
return m[0], m[1], h, err
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
@ -17,19 +16,16 @@ import (
|
||||
)
|
||||
|
||||
func TestRenderCodePreview(t *testing.T) {
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
|
||||
markup.Init(&markup.RenderHelperFuncs{
|
||||
RenderRepoFileCodePreview: func(ctx context.Context, options markup.RenderCodePreviewOptions) (template.HTML, error) {
|
||||
return "<div>code preview</div>", nil
|
||||
},
|
||||
})
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
MarkupType: markdown.MarkupName,
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithMarkupType(markdown.MarkupName), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
|
||||
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" data-markdown-generated-content="" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
||||
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
|
||||
}
|
||||
|
@ -4,13 +4,10 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
@ -84,7 +81,7 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
||||
|
||||
// fullHashPatternProcessor renders SHA containing URLs
|
||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
@ -111,7 +108,7 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
nodeStop := node.NextSibling
|
||||
@ -163,15 +160,12 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
|
||||
// are assumed to be in the same repository.
|
||||
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || (ctx.Repo == nil && ctx.GitRepo == nil) {
|
||||
if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || ctx.RenderHelper == nil {
|
||||
return
|
||||
}
|
||||
|
||||
start := 0
|
||||
next := node.NextSibling
|
||||
if ctx.ShaExistCache == nil {
|
||||
ctx.ShaExistCache = make(map[string]bool)
|
||||
}
|
||||
for node != nil && node != next && start < len(node.Data) {
|
||||
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
|
||||
if m == nil {
|
||||
@ -189,35 +183,12 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// as used by git and github for linking and thus we have to do similar.
|
||||
// Because of this, we check to make sure that a matched hash is actually
|
||||
// a commit in the repository before making it a link.
|
||||
|
||||
// check cache first
|
||||
exist, inCache := ctx.ShaExistCache[hash]
|
||||
if !inCache {
|
||||
if ctx.GitRepo == nil {
|
||||
var err error
|
||||
var closer io.Closer
|
||||
ctx.GitRepo, closer, err = gitrepo.RepositoryFromContextOrOpen(ctx.Ctx, ctx.Repo)
|
||||
if err != nil {
|
||||
log.Error("unable to open repository: %s Error: %v", gitrepo.RepoGitURL(ctx.Repo), err)
|
||||
return
|
||||
}
|
||||
ctx.AddCancel(func() {
|
||||
_ = closer.Close()
|
||||
ctx.GitRepo = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
|
||||
exist = ctx.GitRepo.IsReferenceExist(hash)
|
||||
ctx.ShaExistCache[hash] = exist
|
||||
}
|
||||
|
||||
if !exist {
|
||||
if !ctx.RenderHelper.IsCommitIDExisting(hash) {
|
||||
start = m[3]
|
||||
continue
|
||||
}
|
||||
|
||||
link := util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], "commit", hash)
|
||||
link := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash), LinkTypeApp)
|
||||
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
|
||||
start = 0
|
||||
node = node.NextSibling.NextSibling
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
testModule "code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -34,8 +33,7 @@ func numericIssueLink(baseURL, class string, index int, marker string) string {
|
||||
|
||||
// link an HTML link
|
||||
func link(href, class, contents string) string {
|
||||
extra := ` data-markdown-generated-content=""`
|
||||
extra += util.Iif(class != "", ` class="`+class+`"`, "")
|
||||
extra := util.Iif(class != "", ` class="`+class+`"`, "")
|
||||
return fmt.Sprintf(`<a href="%s"%s>%s</a>`, href, extra, contents)
|
||||
}
|
||||
|
||||
@ -69,22 +67,11 @@ var localMetas = map[string]string{
|
||||
"markupAllowShortIssuePattern": "true",
|
||||
}
|
||||
|
||||
var localWikiMetas = map[string]string{
|
||||
"user": "test-owner",
|
||||
"repo": "test-repo",
|
||||
"markupContentMode": "wiki",
|
||||
}
|
||||
|
||||
func TestRender_IssueIndexPattern(t *testing.T) {
|
||||
// numeric: render inputs without valid mentions
|
||||
test := func(s string) {
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: numericMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, s, NewTestRenderContext())
|
||||
testRenderIssueIndexPattern(t, s, s, NewTestRenderContext(numericMetas))
|
||||
}
|
||||
|
||||
// should not render anything when there are no mentions
|
||||
@ -132,10 +119,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||
links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker)
|
||||
}
|
||||
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas))
|
||||
|
||||
class := "ref-issue"
|
||||
if isExternal {
|
||||
@ -146,10 +130,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||
links[i] = numericIssueLink(prefix, class, index, marker)
|
||||
}
|
||||
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: numericMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, expectedNum, NewTestRenderContext(TestAppURL, numericMetas))
|
||||
}
|
||||
|
||||
// should render freestanding mentions
|
||||
@ -183,10 +164,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
|
||||
|
||||
// alphanumeric: render inputs without valid mentions
|
||||
test := func(s string) {
|
||||
testRenderIssueIndexPattern(t, s, s, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: alphanumericMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, s, NewTestRenderContext(alphanumericMetas))
|
||||
}
|
||||
test("")
|
||||
test("this is a test")
|
||||
@ -216,10 +194,7 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
|
||||
links[i] = externalIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue ref-external-issue", name)
|
||||
}
|
||||
expected := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: alphanumericMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, expected, NewTestRenderContext(alphanumericMetas))
|
||||
}
|
||||
test("OTT-1234 test", "%s test", "OTT-1234")
|
||||
test("test T-12 issue", "test %s issue", "T-12")
|
||||
@ -239,10 +214,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf(expectedFmt, links...)
|
||||
testRenderIssueIndexPattern(t, s, expected, &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, s, expected, NewTestRenderContext(metas))
|
||||
}
|
||||
|
||||
test("abc ISSUE-123 def", "abc %s def",
|
||||
@ -263,10 +235,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) {
|
||||
[]string{"ISSUE-123"},
|
||||
)
|
||||
|
||||
testRenderIssueIndexPattern(t, "will not match", "will not match", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: regexpMetas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "will not match", "will not match", NewTestRenderContext(regexpMetas))
|
||||
}
|
||||
|
||||
func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
|
||||
@ -278,21 +247,12 @@ func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
|
||||
"style": IssueNameStyleNumeric,
|
||||
}
|
||||
|
||||
testRenderIssueIndexPattern(t, "#1", "#1", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "#1312", "#1312", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "!1", "!1", &RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
})
|
||||
testRenderIssueIndexPattern(t, "#1", "#1", NewTestRenderContext(metas))
|
||||
testRenderIssueIndexPattern(t, "#1312", "#1312", NewTestRenderContext(metas))
|
||||
testRenderIssueIndexPattern(t, "!1", "!1", NewTestRenderContext(metas))
|
||||
}
|
||||
|
||||
func TestRender_RenderIssueTitle(t *testing.T) {
|
||||
func TestRender_PostProcessIssueTitle(t *testing.T) {
|
||||
setting.AppURL = TestAppURL
|
||||
metas := map[string]string{
|
||||
"format": "https://someurl.com/{user}/{repo}/{index}",
|
||||
@ -300,20 +260,12 @@ func TestRender_RenderIssueTitle(t *testing.T) {
|
||||
"repo": "someRepo",
|
||||
"style": IssueNameStyleNumeric,
|
||||
}
|
||||
actual, err := RenderIssueTitle(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: metas,
|
||||
}, "#1")
|
||||
actual, err := PostProcessIssueTitle(NewTestRenderContext(metas), "#1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "#1", actual)
|
||||
}
|
||||
|
||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
|
||||
ctx.Links.AbsolutePrefix = true
|
||||
if ctx.Links.Base == "" {
|
||||
ctx.Links.Base = TestRepoURL
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf)
|
||||
assert.NoError(t, err)
|
||||
@ -325,24 +277,12 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
|
||||
test := func(input, expected string) {
|
||||
var buffer strings.Builder
|
||||
err := PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(input), &buffer)
|
||||
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||
|
||||
buffer.Reset()
|
||||
err = PostProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, strings.NewReader(input), &buffer)
|
||||
err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||
}
|
||||
@ -364,16 +304,10 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
|
||||
func TestRender_FullIssueURLs(t *testing.T) {
|
||||
setting.AppURL = TestAppURL
|
||||
defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
defer testModule.MockVariableValue(&RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
var result strings.Builder
|
||||
err := postProcess(&RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: Links{
|
||||
Base: TestRepoURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
||||
err := postProcess(NewTestRenderContext(localMetas), []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, result.String())
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
next := node.NextSibling
|
||||
@ -36,14 +36,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
link := node.Data[m[0]:m[1]]
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx.Ctx, link) {
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, link) {
|
||||
return
|
||||
}
|
||||
text := "#" + node.Data[m[2]:m[3]]
|
||||
// if m[4] and m[5] is not -1, then link is to a comment
|
||||
// indicate that in the text by appending (comment)
|
||||
if m[4] != -1 && m[5] != -1 {
|
||||
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
text += " " + locale.TrString("repo.from_comment")
|
||||
} else {
|
||||
text += " (comment)"
|
||||
@ -56,7 +56,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
matchOrg := linkParts[len(linkParts)-4]
|
||||
matchRepo := linkParts[len(linkParts)-3]
|
||||
|
||||
if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] {
|
||||
if matchOrg == ctx.RenderOptions.Metas["user"] && matchRepo == ctx.RenderOptions.Metas["repo"] {
|
||||
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
||||
} else {
|
||||
text = matchOrg + "/" + matchRepo + text
|
||||
@ -67,14 +67,14 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
if ctx.RenderOptions.Metas == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// crossLinkOnly: do not parse "#123", only parse "owner/repo#123"
|
||||
// if there is no repo in the context, then the "#123" format can't be parsed
|
||||
// old logic: crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
crossLinkOnly := ctx.Metas["markupAllowShortIssuePattern"] != "true"
|
||||
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
|
||||
|
||||
var (
|
||||
found bool
|
||||
@ -84,20 +84,20 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
|
||||
for node != nil && node != next {
|
||||
_, hasExtTrackFormat := ctx.Metas["format"]
|
||||
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
|
||||
|
||||
// Repos with external issue trackers might still need to reference local PRs
|
||||
// We need to concern with the first one that shows up in the text, whichever it is
|
||||
isNumericStyle := ctx.Metas["style"] == "" || ctx.Metas["style"] == IssueNameStyleNumeric
|
||||
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
|
||||
foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
|
||||
|
||||
switch ctx.Metas["style"] {
|
||||
switch ctx.RenderOptions.Metas["style"] {
|
||||
case "", IssueNameStyleNumeric:
|
||||
found, ref = foundNumeric, refNumeric
|
||||
case IssueNameStyleAlphanumeric:
|
||||
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
||||
case IssueNameStyleRegexp:
|
||||
pattern, err := regexplru.GetCompiled(ctx.Metas["regexp"])
|
||||
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -121,9 +121,9 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
var link *html.Node
|
||||
reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
|
||||
if hasExtTrackFormat && !ref.IsPull {
|
||||
ctx.Metas["index"] = ref.Issue
|
||||
ctx.RenderOptions.Metas["index"] = ref.Issue
|
||||
|
||||
res, err := vars.Expand(ctx.Metas["format"], ctx.Metas)
|
||||
res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
|
||||
if err != nil {
|
||||
// here we could just log the error and continue the rendering
|
||||
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
|
||||
@ -136,9 +136,11 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
// Gitea will redirect on click as appropriate.
|
||||
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
|
||||
if ref.Owner == "" {
|
||||
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ctx.Metas["user"], ctx.Metas["repo"], issuePath, ref.Issue), reftext, "ref-issue")
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp)
|
||||
link = createLink(ctx, linkHref, reftext, "ref-issue")
|
||||
} else {
|
||||
link = createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, issuePath, ref.Issue), reftext, "ref-issue")
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp)
|
||||
link = createLink(ctx, linkHref, reftext, "ref-issue")
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +179,8 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
|
||||
reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
|
||||
link := createLink(ctx, util.URLJoin(ctx.Links.Prefix(), ref.Owner, ref.Name, "commit", ref.CommitSha), reftext, "commit")
|
||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
|
||||
link := createLink(ctx, linkHref, reftext, "commit")
|
||||
|
||||
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
||||
node = node.NextSibling.NextSibling
|
||||
|
@ -6,37 +6,14 @@ package markup
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup/common"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
|
||||
isAnchorFragment := link != "" && link[0] == '#'
|
||||
if !isAnchorFragment && !IsFullURLString(link) {
|
||||
linkBase := ctx.Links.Base
|
||||
if ctx.IsMarkupContentWiki() {
|
||||
// no need to check if the link should be resolved as a wiki link or a wiki raw link
|
||||
// just use wiki link here, and it will be redirected to a wiki raw link if necessary
|
||||
linkBase = ctx.Links.WikiLink()
|
||||
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
||||
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
||||
// and then this link will be handled by the "legacy-ref" code and be redirected to the default branch like "/owner/repo/src/branch/main/{the-file-path}"
|
||||
linkBase = ctx.Links.SrcLink()
|
||||
}
|
||||
link, resolved = util.URLJoin(linkBase, link), true
|
||||
}
|
||||
if isAnchorFragment && userContentAnchorPrefix != "" {
|
||||
link, resolved = userContentAnchorPrefix+link[1:], true
|
||||
}
|
||||
return link, resolved
|
||||
}
|
||||
|
||||
func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
@ -116,7 +93,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
|
||||
name += tail
|
||||
image := false
|
||||
ext := filepath.Ext(link)
|
||||
ext := path.Ext(link)
|
||||
switch ext {
|
||||
// fast path: empty string, ignore
|
||||
case "":
|
||||
@ -139,6 +116,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if image {
|
||||
link = strings.ReplaceAll(link, " ", "+")
|
||||
} else {
|
||||
// the hacky wiki name encoding: space to "-"
|
||||
link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
|
||||
}
|
||||
if !strings.Contains(link, "/") {
|
||||
@ -146,9 +124,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
}
|
||||
if image {
|
||||
if !absoluteLink {
|
||||
link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), link)
|
||||
}
|
||||
link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia)
|
||||
title := props["title"]
|
||||
if title == "" {
|
||||
title = props["alt"]
|
||||
@ -174,7 +150,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
childNode.Attr = childNode.Attr[:2]
|
||||
}
|
||||
} else {
|
||||
link, _ = ResolveLink(ctx, link, "")
|
||||
link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault)
|
||||
childNode.Type = html.TextNode
|
||||
childNode.Data = name
|
||||
}
|
||||
|
@ -25,15 +25,16 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||
loc.Start += start
|
||||
loc.End += start
|
||||
mention := node.Data[loc.Start:loc.End]
|
||||
teams, ok := ctx.Metas["teams"]
|
||||
teams, ok := ctx.RenderOptions.Metas["teams"]
|
||||
// FIXME: util.URLJoin may not be necessary here:
|
||||
// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
|
||||
// is an AppSubURL link we can probably fallback to concatenation.
|
||||
// team mention should follow @orgName/teamName style
|
||||
if ok && strings.Contains(mention, "/") {
|
||||
mentionOrgAndTeam := strings.Split(mention, "/")
|
||||
if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "" /*mention*/))
|
||||
if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
|
||||
link := ctx.RenderHelper.ResolveLink(util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), LinkTypeApp)
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
continue
|
||||
@ -43,8 +44,9 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
mentionedUsername := mention[1:]
|
||||
|
||||
if DefaultProcessorHelper.IsUsernameMentionable != nil && DefaultProcessorHelper.IsUsernameMentionable(ctx.Ctx, mentionedUsername) {
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, util.URLJoin(ctx.Links.Prefix(), mentionedUsername), mention, "" /*mention*/))
|
||||
if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) {
|
||||
link := ctx.RenderHelper.ResolveLink(mentionedUsername, LinkTypeApp)
|
||||
replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
|
||||
node = node.NextSibling.NextSibling
|
||||
start = 0
|
||||
} else {
|
||||
|
@ -4,8 +4,6 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@ -17,7 +15,7 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
}
|
||||
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
|
||||
|
||||
// By default, the "<img>" tag should also be clickable,
|
||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||
@ -53,7 +51,7 @@ func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||
continue
|
||||
}
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()), attr.Val)
|
||||
attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
node.Attr[i] = attr
|
||||
|
@ -9,8 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -23,50 +21,13 @@ import (
|
||||
var (
|
||||
testRepoOwnerName = "user13"
|
||||
testRepoName = "repo11"
|
||||
localMetas = map[string]string{
|
||||
"user": testRepoOwnerName,
|
||||
"repo": testRepoName,
|
||||
}
|
||||
localWikiMetas = map[string]string{
|
||||
"user": testRepoOwnerName,
|
||||
"repo": testRepoName,
|
||||
"markupContentMode": "wiki",
|
||||
}
|
||||
localMetas = map[string]string{"user": testRepoOwnerName, "repo": testRepoName}
|
||||
)
|
||||
|
||||
type mockRepo struct {
|
||||
OwnerName string
|
||||
RepoName string
|
||||
}
|
||||
|
||||
func (m *mockRepo) GetOwnerName() string {
|
||||
return m.OwnerName
|
||||
}
|
||||
|
||||
func (m *mockRepo) GetName() string {
|
||||
return m.RepoName
|
||||
}
|
||||
|
||||
func newMockRepo(ownerName, repoName string) gitrepo.Repository {
|
||||
return &mockRepo{
|
||||
OwnerName: ownerName,
|
||||
RepoName: repoName,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_Commits(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
|
||||
buffer, err := markup.RenderString(rctx, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -109,18 +70,10 @@ func TestRender_Commits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_CrossReferences(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: setting.AppSubURL,
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
|
||||
buffer, err := markup.RenderString(rctx, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -152,15 +105,9 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
|
||||
func TestRender_links(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -263,15 +210,9 @@ func TestRender_links(t *testing.T) {
|
||||
|
||||
func TestRender_email(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
res, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
res, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
|
||||
}
|
||||
@ -338,13 +279,7 @@ func TestRender_emoji(t *testing.T) {
|
||||
|
||||
test := func(input, expected string) {
|
||||
expected = strings.ReplaceAll(expected, "&", "&")
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: "a.md",
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markup.RenderString(markup.NewTestRenderContext().WithRelativePath("a.md"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -403,217 +338,133 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
tree := util.URLJoin(markup.TestRepoURL, "src", "master")
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
}, input)
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(tree), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
buffer, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
|
||||
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
|
||||
url := util.URLJoin(tree, "Link")
|
||||
otherURL := util.URLJoin(tree, "Other-Link")
|
||||
encodedURL := util.URLJoin(tree, "Link%3F")
|
||||
imgurl := util.URLJoin(mediatree, "Link.jpg")
|
||||
otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg")
|
||||
encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg")
|
||||
notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg")
|
||||
urlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link")
|
||||
otherURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Other-Link")
|
||||
encodedURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "Link%3F")
|
||||
imgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link.jpg")
|
||||
otherImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+Other.jpg")
|
||||
encodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "Link+%23.jpg")
|
||||
notencodedImgurlWiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw", "some", "path", "Link+#.jpg")
|
||||
imgurl := util.URLJoin(tree, "Link.jpg")
|
||||
otherImgurl := util.URLJoin(tree, "Link+Other.jpg")
|
||||
encodedImgurl := util.URLJoin(tree, "Link+%23.jpg")
|
||||
notencodedImgurl := util.URLJoin(tree, "some", "path", "Link+#.jpg")
|
||||
renderableFileURL := util.URLJoin(tree, "markdown_file.md")
|
||||
renderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "markdown_file.md")
|
||||
unrenderableFileURL := util.URLJoin(tree, "file.zip")
|
||||
unrenderableFileURLWiki := util.URLJoin(markup.TestRepoURL, "wiki", "file.zip")
|
||||
favicon := "http://google.com/favicon.ico"
|
||||
|
||||
test(
|
||||
"[[Link]]",
|
||||
`<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
|
||||
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link.-]]",
|
||||
`<p><a href="http://localhost:3000/test-owner/test-repo/src/master/Link.-" rel="nofollow">Link.-</a></p>`,
|
||||
`<p><a href="http://localhost:3000/test-owner/test-repo/wiki/Link.-" rel="nofollow">Link.-</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link.jpg]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Link.jpg" alt="Link.jpg"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Link.jpg" alt="Link.jpg"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[["+favicon+"]]",
|
||||
`<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico" alt="`+favicon+`"/></a></p>`,
|
||||
`<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico" alt="`+favicon+`"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link]]",
|
||||
`<p><a href="`+url+`" rel="nofollow">Name</a></p>`,
|
||||
`<p><a href="`+urlWiki+`" rel="nofollow">Name</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link.jpg]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Name" alt="Name"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Name" alt="Name"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link.jpg|alt=AltName]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="AltName" alt="AltName"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="AltName" alt="AltName"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link.jpg|title=Title]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="Title"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="Title"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link.jpg|alt=AltName|title=Title]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="AltName"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link.jpg|alt=\"AltName\"|title='Title']]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Title" alt="AltName"/></a></p>`,
|
||||
`<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" title="Title" alt="AltName"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link Other.jpg|alt=\"AltName\"|title='Title']]",
|
||||
`<p><a href="`+otherImgurl+`" rel="nofollow"><img src="`+otherImgurl+`" title="Title" alt="AltName"/></a></p>`,
|
||||
`<p><a href="`+otherImgurlWiki+`" rel="nofollow"><img src="`+otherImgurlWiki+`" title="Title" alt="AltName"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link]] [[Other Link]]",
|
||||
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a></p>`,
|
||||
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link?]]",
|
||||
`<p><a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
|
||||
`<p><a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link]] [[Other Link]] [[Link?]]",
|
||||
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
|
||||
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[markdown_file.md]]",
|
||||
`<p><a href="`+renderableFileURL+`" rel="nofollow">markdown_file.md</a></p>`,
|
||||
`<p><a href="`+renderableFileURLWiki+`" rel="nofollow">markdown_file.md</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[file.zip]]",
|
||||
`<p><a href="`+unrenderableFileURL+`" rel="nofollow">file.zip</a></p>`,
|
||||
`<p><a href="`+unrenderableFileURLWiki+`" rel="nofollow">file.zip</a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Link #.jpg]]",
|
||||
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`,
|
||||
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`" title="Link #.jpg" alt="Link #.jpg"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]",
|
||||
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Title" alt="AltName"/></a></p>`,
|
||||
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`" title="Title" alt="AltName"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"[[some/path/Link #.jpg]]",
|
||||
`<p><a href="`+notencodedImgurl+`" rel="nofollow"><img src="`+notencodedImgurl+`" title="Link #.jpg" alt="some/path/Link #.jpg"/></a></p>`,
|
||||
`<p><a href="`+notencodedImgurlWiki+`" rel="nofollow"><img src="`+notencodedImgurlWiki+`" title="Link #.jpg" alt="some/path/Link #.jpg"/></a></p>`)
|
||||
)
|
||||
test(
|
||||
"<p><a href=\"https://example.org\">[[foobar]]</a></p>",
|
||||
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`,
|
||||
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_RelativeMedias(t *testing.T) {
|
||||
render := func(input string, isWiki bool, links markup.Links) string {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: links,
|
||||
Metas: util.Iif(isWiki, localWikiMetas, localMetas),
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
return strings.TrimSpace(string(buffer))
|
||||
}
|
||||
|
||||
out := render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/media/test-branch/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/media/test-branch/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<img src="/LINK"/>`, out)
|
||||
|
||||
out = render(`<video src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/test-owner/test-repo/LINK"></video>`, out)
|
||||
|
||||
out = render(`<video src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/test-owner/test-repo/wiki/raw/LINK"></video>`, out)
|
||||
|
||||
out = render(`<video src="/LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/LINK"></video>`, out)
|
||||
)
|
||||
}
|
||||
|
||||
func Test_ParseClusterFuzz(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
|
||||
localMetas := map[string]string{
|
||||
"user": "go-gitea",
|
||||
"repo": "gitea",
|
||||
}
|
||||
localMetas := map[string]string{"user": "go-gitea", "repo": "gitea"}
|
||||
|
||||
data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, res.String(), "<html")
|
||||
|
||||
data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY "
|
||||
|
||||
res.Reset()
|
||||
err = markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err = markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, res.String(), "<html")
|
||||
}
|
||||
|
||||
func TestPostProcess_RenderDocument(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
setting.StaticURLPrefix = markup.TestAppURL // can't run standalone
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
|
||||
test := func(input, expected string) {
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: "https://example.com",
|
||||
},
|
||||
Metas: map[string]string{"user": "go-gitea", "repo": "gitea"},
|
||||
}, strings.NewReader(input), &res)
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(markup.TestAppURL, map[string]string{"user": "go-gitea", "repo": "gitea"}), strings.NewReader(input), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
|
||||
}
|
||||
@ -650,10 +501,7 @@ func TestIssue16020(t *testing.T) {
|
||||
data := `<img src="data:image/png;base64,i//V"/>`
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, data, res.String())
|
||||
}
|
||||
@ -666,29 +514,15 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
assert.NoError(b, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzz(t *testing.T) {
|
||||
s := "t/l/issues/8#/../../a"
|
||||
renderContext := markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "https://example.com/go-gitea/gitea",
|
||||
},
|
||||
Metas: map[string]string{
|
||||
"user": "go-gitea",
|
||||
"repo": "gitea",
|
||||
},
|
||||
}
|
||||
|
||||
err := markup.PostProcess(&renderContext, strings.NewReader(s), io.Discard)
|
||||
|
||||
renderContext := markup.NewTestRenderContext()
|
||||
err := markup.PostProcessDefault(renderContext, strings.NewReader(s), io.Discard)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@ -696,10 +530,7 @@ func TestIssue18471(t *testing.T) {
|
||||
data := `http://domain/org/repo/compare/783b039...da951ce`
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcess(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Metas: localMetas,
|
||||
}, strings.NewReader(data), &res)
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
|
||||
|
@ -4,11 +4,15 @@
|
||||
package markup_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
setting.IsInTesting = true
|
||||
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ func NewASTTransformer(renderInternal *internal.RenderInternal) *ASTTransformer
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) applyElementDir(n ast.Node) {
|
||||
if markup.DefaultProcessorHelper.ElementDir != "" {
|
||||
n.SetAttributeString("dir", []byte(markup.DefaultProcessorHelper.ElementDir))
|
||||
if !markup.RenderBehaviorForTesting.DisableAdditionalAttributes {
|
||||
n.SetAttributeString("dir", "auto")
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,10 +79,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
|
||||
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
|
||||
// especially in many tests.
|
||||
markdownLineBreakStyle := ctx.Metas["markdownLineBreakStyle"]
|
||||
if markup.RenderBehaviorForTesting.ForceHardLineBreak {
|
||||
v.SetHardLineBreak(true)
|
||||
} else if markdownLineBreakStyle == "comment" {
|
||||
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
|
||||
if markdownLineBreakStyle == "comment" {
|
||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
||||
} else if markdownLineBreakStyle == "document" {
|
||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
||||
|
@ -4,18 +4,15 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
return username == "r-lyeh"
|
||||
},
|
||||
})
|
||||
unittest.MainTest(m)
|
||||
setting.IsInTesting = true
|
||||
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
bufWithMetadataLength := len(buf)
|
||||
|
||||
rc := &RenderConfig{
|
||||
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
|
||||
Meta: markup.RenderMetaAsDetails,
|
||||
Icon: "table",
|
||||
Lang: "",
|
||||
}
|
||||
@ -241,7 +241,7 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
|
||||
|
||||
// Render renders Markdown to HTML with all specific handling stuff.
|
||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
ctx.MarkupType = MarkupName
|
||||
ctx.RenderOptions.MarkupType = MarkupName
|
||||
return markup.Render(ctx, input, output)
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
@ -37,76 +35,23 @@ var localMetas = map[string]string{
|
||||
"repo": testRepoName,
|
||||
}
|
||||
|
||||
var localWikiMetas = map[string]string{
|
||||
"user": testRepoOwnerName,
|
||||
"repo": testRepoName,
|
||||
"markupContentMode": "wiki",
|
||||
}
|
||||
|
||||
type mockRepo struct {
|
||||
OwnerName string
|
||||
RepoName string
|
||||
}
|
||||
|
||||
func (m *mockRepo) GetOwnerName() string {
|
||||
return m.OwnerName
|
||||
}
|
||||
|
||||
func (m *mockRepo) GetName() string {
|
||||
return m.RepoName
|
||||
}
|
||||
|
||||
func newMockRepo(ownerName, repoName string) gitrepo.Repository {
|
||||
return &mockRepo{
|
||||
OwnerName: ownerName,
|
||||
RepoName: repoName,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_StandardLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, input)
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
|
||||
buffer, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
|
||||
googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
|
||||
test("<https://google.com/>", googleRendered, googleRendered)
|
||||
|
||||
lnk := util.URLJoin(FullURL, "WikiPage")
|
||||
lnkWiki := util.URLJoin(FullURL, "wiki", "WikiPage")
|
||||
test("[WikiPage](WikiPage)",
|
||||
`<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
|
||||
`<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
|
||||
test("<https://google.com/>", googleRendered)
|
||||
test("[Link](Link)", `<p><a href="/Link" rel="nofollow">Link</a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_Images(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, input)
|
||||
buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
}
|
||||
@ -140,94 +85,13 @@ func TestRender_Images(t *testing.T) {
|
||||
`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
}
|
||||
|
||||
func testAnswers(baseURLContent, baseURLImages string) []string {
|
||||
return []string{
|
||||
`<p>Wiki! Enjoy :)</p>
|
||||
<ul>
|
||||
<li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
|
||||
</ul>
|
||||
<p>See commit <a href="/` + testRepoOwnerName + `/` + testRepoName + `/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
|
||||
<p>Ideas and codes</p>
|
||||
<ul>
|
||||
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
|
||||
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="` + FullURL + `issues/786" class="ref-issue" rel="nofollow">#786</a></li>
|
||||
<li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
|
||||
<li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
|
||||
<li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
|
||||
</ul>
|
||||
`,
|
||||
`<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2>
|
||||
<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>
|
||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><a href="` + baseURLImages + `/images/icon-install.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-install.png" title="icon-install.png" alt="images/icon-install.png"/></a></th>
|
||||
<th><a href="` + baseURLContent + `/Installation" rel="nofollow">Installation</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="` + baseURLImages + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-usage.png" title="icon-usage.png" alt="images/icon-usage.png"/></a></td>
|
||||
<td><a href="` + baseURLContent + `/Usage" rel="nofollow">Usage</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
`<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
|
||||
<ol>
|
||||
<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a><br/>
|
||||
<a href="` + baseURLImages + `/images/1.png" rel="nofollow"><img src="` + baseURLImages + `/images/1.png" title="1.png" alt="images/1.png"/></a></li>
|
||||
<li>Perform a test run by hitting the Run! button.<br/>
|
||||
<a href="` + baseURLImages + `/images/2.png" rel="nofollow"><img src="` + baseURLImages + `/images/2.png" title="2.png" alt="images/2.png"/></a></li>
|
||||
</ol>
|
||||
<h2 id="user-content-custom-id">More tests</h2>
|
||||
<p>(from <a href="https://www.markdownguide.org/extended-syntax/" rel="nofollow">https://www.markdownguide.org/extended-syntax/</a>)</p>
|
||||
<h3 id="user-content-checkboxes">Checkboxes</h3>
|
||||
<ul>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="434"/>unchecked</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li>
|
||||
</ul>
|
||||
<h3 id="user-content-definition-list">Definition list</h3>
|
||||
<dl>
|
||||
<dt>First Term</dt>
|
||||
<dd>This is the definition of the first term.</dd>
|
||||
<dt>Second Term</dt>
|
||||
<dd>This is one definition of the second term.</dd>
|
||||
<dd>This is another definition of the second term.</dd>
|
||||
</dl>
|
||||
<h3 id="user-content-footnotes">Footnotes</h3>
|
||||
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p>
|
||||
<div>
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:user-content-1">
|
||||
<p>This is the first footnote. <a href="#fnref:user-content-1" rel="nofollow">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:user-content-bignote">
|
||||
<p>Here is one with multiple paragraphs and code.</p>
|
||||
<p>Indent paragraphs to include them in the footnote.</p>
|
||||
<p><code>{ my code }</code></p>
|
||||
<p>Add as many paragraphs as you like. <a href="#fnref:user-content-bignote" rel="nofollow">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`, `<ul>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="3"/> If you want to rebase/retry this PR, click this checkbox.</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p>This PR has been generated by <a href="https://github.com/renovatebot/renovate" rel="nofollow">Renovate Bot</a>.</p>
|
||||
`,
|
||||
}
|
||||
}
|
||||
func TestTotal_RenderString(t *testing.T) {
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
|
||||
// Test cases without ambiguous links
|
||||
var sameCases = []string{
|
||||
// dear imgui wiki markdown extract: special wiki syntax
|
||||
`Wiki! Enjoy :)
|
||||
// Test cases without ambiguous links (It is not right to copy a whole file here, instead it should clearly test what is being tested)
|
||||
sameCases := []string{
|
||||
// dear imgui wiki markdown extract: special wiki syntax
|
||||
`Wiki! Enjoy :)
|
||||
- [[Links, Language bindings, Engine bindings|Links]]
|
||||
- [[Tips]]
|
||||
|
||||
@ -240,8 +104,8 @@ Ideas and codes
|
||||
- Node graph editors https://github.com/ocornut/imgui/issues/306
|
||||
- [[Memory Editor|memory_editor_example]]
|
||||
- [[Plot var helper|plot_var_example]]`,
|
||||
// wine-staging wiki home extract: tables, special wiki syntax, images
|
||||
`## What is Wine Staging?
|
||||
// wine-staging wiki home extract: tables, special wiki syntax, images
|
||||
`## What is Wine Staging?
|
||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
|
||||
|
||||
## Quick Links
|
||||
@ -251,8 +115,8 @@ Here are some links to the most important topics. You can find the full list of
|
||||
|--------------------------------|----------------------------------------------------------|
|
||||
| [[images/icon-usage.png]] | [[Usage]] |
|
||||
`,
|
||||
// libgdx wiki page: inline images with special syntax
|
||||
`[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X.
|
||||
// libgdx wiki page: inline images with special syntax
|
||||
`[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X.
|
||||
|
||||
1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop)
|
||||
[[images/1.png]]
|
||||
@ -292,7 +156,7 @@ Here is a simple footnote,[^1] and here is a longer one.[^bignote]
|
||||
|
||||
Add as many paragraphs as you like.
|
||||
`,
|
||||
`
|
||||
`
|
||||
- [ ] <!-- rebase-check --> If you want to rebase/retry this PR, click this checkbox.
|
||||
|
||||
---
|
||||
@ -300,98 +164,117 @@ Here is a simple footnote,[^1] and here is a longer one.[^bignote]
|
||||
This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
|
||||
|
||||
<!-- test-comment -->`,
|
||||
}
|
||||
|
||||
func TestTotal_RenderWiki(t *testing.T) {
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
setting.AppURL = AppURL
|
||||
answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw"))
|
||||
for i := 0; i < len(sameCases); i++ {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localWikiMetas,
|
||||
}, sameCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, answers[i], string(line))
|
||||
}
|
||||
|
||||
testCases := []string{
|
||||
// Guard wiki sidebar: special syntax
|
||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||
// rendered
|
||||
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
baseURL := ""
|
||||
testAnswers := []string{
|
||||
`<p>Wiki! Enjoy :)</p>
|
||||
<ul>
|
||||
<li><a href="` + baseURL + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="` + baseURL + `/Tips" rel="nofollow">Tips</a></li>
|
||||
</ul>
|
||||
<p>See commit <a href="/` + testRepoOwnerName + `/` + testRepoName + `/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p>
|
||||
<p>Ideas and codes</p>
|
||||
<ul>
|
||||
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li>
|
||||
<li>Bezier widget (by <a href="/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="` + FullURL + `issues/786" class="ref-issue" rel="nofollow">#786</a></li>
|
||||
<li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li>
|
||||
<li><a href="` + baseURL + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
|
||||
<li><a href="` + baseURL + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
|
||||
</ul>
|
||||
`,
|
||||
// special syntax
|
||||
`[[Name|Link]]`,
|
||||
// rendered
|
||||
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2>
|
||||
<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>
|
||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><a href="` + baseURL + `/images/icon-install.png" rel="nofollow"><img src="` + baseURL + `/images/icon-install.png" title="icon-install.png" alt="images/icon-install.png"/></a></th>
|
||||
<th><a href="` + baseURL + `/Installation" rel="nofollow">Installation</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="` + baseURL + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURL + `/images/icon-usage.png" title="icon-usage.png" alt="images/icon-usage.png"/></a></td>
|
||||
<td><a href="` + baseURL + `/Usage" rel="nofollow">Usage</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
`<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
|
||||
<ol>
|
||||
<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a>
|
||||
<a href="` + baseURL + `/images/1.png" rel="nofollow"><img src="` + baseURL + `/images/1.png" title="1.png" alt="images/1.png"/></a></li>
|
||||
<li>Perform a test run by hitting the Run! button.
|
||||
<a href="` + baseURL + `/images/2.png" rel="nofollow"><img src="` + baseURL + `/images/2.png" title="2.png" alt="images/2.png"/></a></li>
|
||||
</ol>
|
||||
<h2 id="user-content-custom-id">More tests</h2>
|
||||
<p>(from <a href="https://www.markdownguide.org/extended-syntax/" rel="nofollow">https://www.markdownguide.org/extended-syntax/</a>)</p>
|
||||
<h3 id="user-content-checkboxes">Checkboxes</h3>
|
||||
<ul>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="434"/>unchecked</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li>
|
||||
</ul>
|
||||
<h3 id="user-content-definition-list">Definition list</h3>
|
||||
<dl>
|
||||
<dt>First Term</dt>
|
||||
<dd>This is the definition of the first term.</dd>
|
||||
<dt>Second Term</dt>
|
||||
<dd>This is one definition of the second term.</dd>
|
||||
<dd>This is another definition of the second term.</dd>
|
||||
</dl>
|
||||
<h3 id="user-content-footnotes">Footnotes</h3>
|
||||
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p>
|
||||
<div>
|
||||
<hr/>
|
||||
<ol>
|
||||
<li id="fn:user-content-1">
|
||||
<p>This is the first footnote. <a href="#fnref:user-content-1" rel="nofollow">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:user-content-bignote">
|
||||
<p>Here is one with multiple paragraphs and code.</p>
|
||||
<p>Indent paragraphs to include them in the footnote.</p>
|
||||
<p><code>{ my code }</code></p>
|
||||
<p>Add as many paragraphs as you like. <a href="#fnref:user-content-bignote" rel="nofollow">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
`<ul>
|
||||
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="3"/> If you want to rebase/retry this PR, click this checkbox.</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p>This PR has been generated by <a href="https://github.com/renovatebot/renovate" rel="nofollow">Renovate Bot</a>.</p>
|
||||
`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
Metas: localWikiMetas,
|
||||
}, testCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, testCases[i+1], string(line))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotal_RenderString(t *testing.T) {
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
setting.AppURL = AppURL
|
||||
answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master"))
|
||||
markup.Init(&markup.RenderHelperFuncs{
|
||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||
return username == "r-lyeh"
|
||||
},
|
||||
})
|
||||
for i := 0; i < len(sameCases); i++ {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
Repo: newMockRepo(testRepoOwnerName, testRepoName),
|
||||
Metas: localMetas,
|
||||
}, sameCases[i])
|
||||
line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), sameCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, answers[i], string(line))
|
||||
}
|
||||
|
||||
testCases := []string{}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
line, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: FullURL,
|
||||
},
|
||||
}, testCases[i])
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, template.HTML(testCases[i+1]), line)
|
||||
assert.Equal(t, testAnswers[i], string(line))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRender_RenderParagraphs(t *testing.T) {
|
||||
test := func(t *testing.T, str string, cnt int) {
|
||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, str)
|
||||
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), str)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
|
||||
mac := strings.ReplaceAll(str, "\n", "\r")
|
||||
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, mac)
|
||||
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), mac)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
|
||||
dos := strings.ReplaceAll(str, "\n", "\r\n")
|
||||
res, err = markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, dos)
|
||||
res, err = markdown.RenderRawString(markup.NewTestRenderContext(), dos)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res)
|
||||
}
|
||||
@ -419,7 +302,7 @@ func TestMarkdownRenderRaw(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
log.Info("Test markdown render error with fuzzy data: %x, the following errors can be recovered", testcase)
|
||||
_, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, string(testcase))
|
||||
_, err := markdown.RenderRawString(markup.NewTestRenderContext(), string(testcase))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@ -428,11 +311,10 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
|
||||
testcase := `![image1](/image1)
|
||||
![image2](/image2)
|
||||
`
|
||||
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br>
|
||||
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a>
|
||||
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
|
||||
`
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||
res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, res)
|
||||
}
|
||||
@ -441,7 +323,7 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
|
||||
testcase := `[Link with emoji :moon: in text](https://gitea.io)`
|
||||
expected := `<p><a href="https://gitea.io" rel="nofollow">Link with emoji <span class="emoji" aria-label="waxing gibbous moon">🌔</span> in text</a></p>
|
||||
`
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, template.HTML(expected), res)
|
||||
}
|
||||
@ -479,7 +361,7 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range positiveTests {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -498,7 +380,7 @@ func TestColorPreview(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range negativeTests {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test)
|
||||
assert.NotContains(t, res, `<span class="color-preview" style="background-color: `, "Unexpected result in testcase %q", test)
|
||||
}
|
||||
@ -573,7 +455,7 @@ func TestMathBlock(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -610,7 +492,7 @@ foo: bar
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||
res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||
assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
|
||||
}
|
||||
@ -641,376 +523,33 @@ mail@domain.com
|
||||
space${SPACE}${SPACE}
|
||||
`
|
||||
input = strings.ReplaceAll(input, "${SPACE}", " ") // replace ${SPACE} with " ", to avoid some editor's auto-trimming
|
||||
cases := []struct {
|
||||
Links markup.Links
|
||||
IsWiki bool
|
||||
Expected string
|
||||
}{
|
||||
{ // 0
|
||||
Links: markup.Links{},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
|
||||
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
expected := `<p>space @mention-user<br/>
|
||||
/just/a/path.bin
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
|
||||
<a href="/file.bin" rel="nofollow">local link</a>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a>
|
||||
<a href="/file.bin" rel="nofollow">local link</a>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a>
|
||||
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
|
||||
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a>
|
||||
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
|
||||
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||
<span class="emoji" aria-label="thumbs up">👍</span>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
|
||||
@mention-user test
|
||||
#123
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 1
|
||||
Links: markup.Links{},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 2
|
||||
Links: markup.Links{
|
||||
Base: "https://gitea.io/",
|
||||
},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 3
|
||||
Links: markup.Links{
|
||||
Base: "https://gitea.io/",
|
||||
},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 4
|
||||
Links: markup.Links{
|
||||
Base: "/relative/path",
|
||||
},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 5
|
||||
Links: markup.Links{
|
||||
Base: "/relative/path",
|
||||
},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 6
|
||||
Links: markup.Links{
|
||||
Base: "/user/repo",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 7
|
||||
Links: markup.Links{
|
||||
Base: "/relative/path",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 8
|
||||
Links: markup.Links{
|
||||
Base: "/user/repo",
|
||||
TreePath: "sub/folder",
|
||||
},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 9
|
||||
Links: markup.Links{
|
||||
Base: "/relative/path",
|
||||
TreePath: "sub/folder",
|
||||
},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 10
|
||||
Links: markup.Links{
|
||||
Base: "/user/repo",
|
||||
BranchPath: "branch/main",
|
||||
TreePath: "sub/folder",
|
||||
},
|
||||
IsWiki: false,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
{ // 11
|
||||
Links: markup.Links{
|
||||
Base: "/relative/path",
|
||||
BranchPath: "branch/main",
|
||||
TreePath: "sub/folder",
|
||||
},
|
||||
IsWiki: true,
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
|
||||
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a><br/>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
|
||||
<span class="emoji" aria-label="thumbs up">👍</span><br/>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
|
||||
@mention-user test<br/>
|
||||
#123<br/>
|
||||
space</p>
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.ForceHardLineBreak, true)()
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableInternalAttributes, true)()
|
||||
for i, c := range cases {
|
||||
result, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: context.Background(),
|
||||
Links: c.Links,
|
||||
Metas: util.Iif(c.IsWiki, map[string]string{"markupContentMode": "wiki"}, map[string]string{}),
|
||||
}, input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
||||
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
|
||||
}
|
||||
`
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(result))
|
||||
}
|
||||
|
||||
func TestAttention(t *testing.T) {
|
||||
@ -1029,7 +568,7 @@ func TestAttention(t *testing.T) {
|
||||
}
|
||||
|
||||
test := func(input, expected string) {
|
||||
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, input)
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
|
||||
}
|
||||
@ -1062,6 +601,6 @@ func BenchmarkSpecializedMarkdown(b *testing.B) {
|
||||
func BenchmarkMarkdownRender(b *testing.B) {
|
||||
// 23202 50840 ns/op
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = markdown.RenderString(&markup.RenderContext{Ctx: context.Background()}, "https://example.com\n- a\n- b\n")
|
||||
_, _ = markdown.RenderString(markup.NewTestRenderContext(), "https://example.com\n- a\n- b\n")
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,7 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
giteautil "code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
@ -20,10 +17,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
|
||||
|
||||
// Check if the destination is a real link
|
||||
if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
|
||||
v.Destination = []byte(giteautil.URLJoin(
|
||||
ctx.Links.ResolveMediaLink(ctx.IsMarkupContentWiki()),
|
||||
strings.TrimLeft(string(v.Destination), "/"),
|
||||
))
|
||||
v.Destination = []byte(ctx.RenderHelper.ResolveLink(string(v.Destination), markup.LinkTypeMedia))
|
||||
}
|
||||
|
||||
parent := v.Parent()
|
||||
|
@ -9,8 +9,19 @@ import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
func resolveLink(ctx *markup.RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
|
||||
isAnchorFragment := link != "" && link[0] == '#'
|
||||
if !isAnchorFragment && !markup.IsFullURLString(link) {
|
||||
link, resolved = ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault), true
|
||||
}
|
||||
if isAnchorFragment && userContentAnchorPrefix != "" {
|
||||
link, resolved = userContentAnchorPrefix+link[1:], true
|
||||
}
|
||||
return link, resolved
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
|
||||
if link, resolved := markup.ResolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
|
||||
if link, resolved := resolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
|
||||
v.Destination = []byte(link)
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,15 @@ package markup
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/lexers"
|
||||
@ -21,33 +22,36 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
markup.RegisterRenderer(Renderer{})
|
||||
markup.RegisterRenderer(renderer{})
|
||||
}
|
||||
|
||||
// Renderer implements markup.Renderer for orgmode
|
||||
type Renderer struct{}
|
||||
type renderer struct{}
|
||||
|
||||
var _ markup.PostProcessRenderer = (*Renderer)(nil)
|
||||
var (
|
||||
_ markup.Renderer = (*renderer)(nil)
|
||||
_ markup.PostProcessRenderer = (*renderer)(nil)
|
||||
)
|
||||
|
||||
// Name implements markup.Renderer
|
||||
func (Renderer) Name() string {
|
||||
func (renderer) Name() string {
|
||||
return "orgmode"
|
||||
}
|
||||
|
||||
// NeedPostProcess implements markup.PostProcessRenderer
|
||||
func (Renderer) NeedPostProcess() bool { return true }
|
||||
func (renderer) NeedPostProcess() bool { return true }
|
||||
|
||||
// Extensions implements markup.Renderer
|
||||
func (Renderer) Extensions() []string {
|
||||
func (renderer) Extensions() []string {
|
||||
return []string{".org"}
|
||||
}
|
||||
|
||||
// SanitizerRules implements markup.Renderer
|
||||
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
func (renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return []setting.MarkupSanitizerRule{}
|
||||
}
|
||||
|
||||
// Render renders orgmode rawbytes to HTML
|
||||
// Render renders orgmode raw bytes to HTML
|
||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
htmlWriter := org.NewHTMLWriter()
|
||||
htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool, params map[string]string) string {
|
||||
@ -57,10 +61,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
var w strings.Builder
|
||||
if _, err := w.WriteString(`<pre>`); err != nil {
|
||||
return ""
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
|
||||
lexer := lexers.Get(lang)
|
||||
if lexer == nil && lang == "" {
|
||||
@ -71,26 +72,20 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
lang = strings.ToLower(lexer.Config().Name)
|
||||
}
|
||||
|
||||
// include language-x class as part of commonmark spec
|
||||
if err := ctx.RenderInternal.FormatWithSafeAttrs(w, `<pre><code class="chroma language-%s">`, lang); err != nil {
|
||||
return ""
|
||||
}
|
||||
if lexer == nil {
|
||||
// include language-x class as part of commonmark spec
|
||||
if _, err := w.WriteString(`<code class="chroma language-` + lang + `">`); err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, err := w.WriteString(html.EscapeString(source)); err != nil {
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
// include language-x class as part of commonmark spec
|
||||
if _, err := w.WriteString(`<code class="chroma language-` + lang + `">`); err != nil {
|
||||
return ""
|
||||
}
|
||||
lexer = chroma.Coalesce(lexer)
|
||||
|
||||
if _, err := w.WriteString(string(highlight.CodeFromLexer(lexer, source))); err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := w.WriteString("</code></pre>"); err != nil {
|
||||
return ""
|
||||
}
|
||||
@ -98,11 +93,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
|
||||
return w.String()
|
||||
}
|
||||
|
||||
w := &Writer{
|
||||
HTMLWriter: htmlWriter,
|
||||
Ctx: ctx,
|
||||
}
|
||||
|
||||
w := &orgWriter{rctx: ctx, HTMLWriter: htmlWriter}
|
||||
htmlWriter.ExtendingWriter = w
|
||||
|
||||
res, err := org.New().Silent().Parse(input, "").Write(w)
|
||||
@ -123,17 +114,18 @@ func RenderString(ctx *markup.RenderContext, content string) (string, error) {
|
||||
}
|
||||
|
||||
// Render renders orgmode string to HTML string
|
||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
func (renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
return Render(ctx, input, output)
|
||||
}
|
||||
|
||||
// Writer implements org.Writer
|
||||
type Writer struct {
|
||||
type orgWriter struct {
|
||||
*org.HTMLWriter
|
||||
Ctx *markup.RenderContext
|
||||
rctx *markup.RenderContext
|
||||
}
|
||||
|
||||
func (r *Writer) resolveLink(kind, link string) string {
|
||||
var _ org.Writer = (*orgWriter)(nil)
|
||||
|
||||
func (r *orgWriter) resolveLink(kind, link string) string {
|
||||
link = strings.TrimPrefix(link, "file:")
|
||||
if !strings.HasPrefix(link, "#") && // not a URL fragment
|
||||
!markup.IsFullURLString(link) {
|
||||
@ -142,48 +134,43 @@ func (r *Writer) resolveLink(kind, link string) string {
|
||||
// so we need to try to guess the link kind again here
|
||||
kind = org.RegularLink{URL: link}.Kind()
|
||||
}
|
||||
|
||||
base := r.Ctx.Links.Base
|
||||
if r.Ctx.IsMarkupContentWiki() {
|
||||
base = r.Ctx.Links.WikiLink()
|
||||
} else if r.Ctx.Links.HasBranchInfo() {
|
||||
base = r.Ctx.Links.SrcLink()
|
||||
}
|
||||
|
||||
if kind == "image" || kind == "video" {
|
||||
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsMarkupContentWiki())
|
||||
link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia)
|
||||
} else {
|
||||
link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault)
|
||||
}
|
||||
|
||||
link = util.URLJoin(base, link)
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
// WriteRegularLink renders images, links or videos
|
||||
func (r *Writer) WriteRegularLink(l org.RegularLink) {
|
||||
func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
|
||||
link := r.resolveLink(l.Kind(), l.URL)
|
||||
|
||||
printHTML := func(html string, a ...any) {
|
||||
_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...))
|
||||
}
|
||||
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
|
||||
switch l.Kind() {
|
||||
case "image":
|
||||
if l.Description == nil {
|
||||
_, _ = fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
|
||||
printHTML(`<img src="%s" alt="%s">`, link, link)
|
||||
} else {
|
||||
imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
|
||||
_, _ = fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
|
||||
printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc)
|
||||
}
|
||||
case "video":
|
||||
if l.Description == nil {
|
||||
_, _ = fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
|
||||
printHTML(`<video src="%s">%s</video>`, link, link)
|
||||
} else {
|
||||
videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
|
||||
_, _ = fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
|
||||
printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
|
||||
}
|
||||
default:
|
||||
description := link
|
||||
var description any = link
|
||||
if l.Description != nil {
|
||||
description = r.WriteNodesAsString(l.Description...)
|
||||
description = template.HTML(r.WriteNodesAsString(l.Description...)) // orgmode HTMLWriter outputs HTML content
|
||||
}
|
||||
_, _ = fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description)
|
||||
printHTML(`<a href="%s">%s</a>`, link, description)
|
||||
}
|
||||
}
|
||||
|
@ -4,54 +4,38 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const AppURL = "http://localhost:3000/"
|
||||
func TestMain(m *testing.M) {
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
setting.IsInTesting = true
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRender_StandardLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string, isWiki bool) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "/relative-path",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
Metas: map[string]string{"markupContentMode": util.Iif(isWiki, "wiki", "")},
|
||||
}, input)
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/media/branch/main/"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
test("[[https://google.com/]]",
|
||||
`<p><a href="https://google.com/">https://google.com/</a></p>`, false)
|
||||
test("[[WikiPage][The WikiPage Desc]]",
|
||||
`<p><a href="/relative-path/wiki/WikiPage">The WikiPage Desc</a></p>`, true)
|
||||
`<p><a href="https://google.com/">https://google.com/</a></p>`)
|
||||
test("[[ImageLink.svg][The Image Desc]]",
|
||||
`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`, false)
|
||||
`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_InternalLinks(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "/relative-path",
|
||||
BranchPath: "branch/main",
|
||||
},
|
||||
}, input)
|
||||
buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/src/branch/main"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
@ -67,29 +51,22 @@ func TestRender_InternalLinks(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_Media(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: "./relative-path",
|
||||
},
|
||||
}, input)
|
||||
buffer, err := RenderString(markup.NewTestRenderContext("./relative-path"), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
test("[[file:../../.images/src/02/train.jpg]]",
|
||||
`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg" /></p>`)
|
||||
`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg"></p>`)
|
||||
test("[[file:train.jpg]]",
|
||||
`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg" /></p>`)
|
||||
`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg"></p>`)
|
||||
|
||||
// With description.
|
||||
test("[[https://example.com][https://example.com/example.svg]]",
|
||||
`<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></a></p>`)
|
||||
`<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg"></a></p>`)
|
||||
test("[[https://example.com][pre https://example.com/example.svg post]]",
|
||||
`<p><a href="https://example.com">pre <img src="https://example.com/example.svg" alt="https://example.com/example.svg" /> post</a></p>`)
|
||||
`<p><a href="https://example.com">pre <img src="https://example.com/example.svg" alt="https://example.com/example.svg"> post</a></p>`)
|
||||
test("[[https://example.com][https://example.com/example.mp4]]",
|
||||
`<p><a href="https://example.com"><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></a></p>`)
|
||||
test("[[https://example.com][pre https://example.com/example.mp4 post]]",
|
||||
@ -97,28 +74,24 @@ func TestRender_Media(t *testing.T) {
|
||||
|
||||
// Without description.
|
||||
test("[[https://example.com/example.svg]]",
|
||||
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
|
||||
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg"></p>`)
|
||||
test("[[https://example.com/example.mp4]]",
|
||||
`<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
|
||||
|
||||
// test [[LINK][DESCRIPTION]] syntax with "file:" prefix
|
||||
test(`[[https://example.com/][file:https://example.com/foo%20bar.svg]]`,
|
||||
`<p><a href="https://example.com/"><img src="https://example.com/foo%20bar.svg" alt="https://example.com/foo%20bar.svg" /></a></p>`)
|
||||
`<p><a href="https://example.com/"><img src="https://example.com/foo%20bar.svg" alt="https://example.com/foo%20bar.svg"></a></p>`)
|
||||
test(`[[file:https://example.com/foo%20bar.svg][Goto Image]]`,
|
||||
`<p><a href="https://example.com/foo%20bar.svg">Goto Image</a></p>`)
|
||||
test(`[[file:https://example.com/link][https://example.com/image.jpg]]`,
|
||||
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
|
||||
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg"></a></p>`)
|
||||
test(`[[file:https://example.com/link][file:https://example.com/image.jpg]]`,
|
||||
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
|
||||
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg"></a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_Source(t *testing.T) {
|
||||
setting.AppURL = AppURL
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
}, input)
|
||||
buffer, err := RenderString(markup.NewTestRenderContext(), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
@ -9,9 +9,8 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -29,96 +28,114 @@ const (
|
||||
)
|
||||
|
||||
var RenderBehaviorForTesting struct {
|
||||
// Markdown line break rendering has 2 default behaviors:
|
||||
// * Use hard: replace "\n" with "<br>" for comments, setting.Markdown.EnableHardLineBreakInComments=true
|
||||
// * Keep soft: "\n" for non-comments (a.k.a. documents), setting.Markdown.EnableHardLineBreakInDocuments=false
|
||||
// In history, there was a mess:
|
||||
// * The behavior was controlled by `Metas["mode"] != "document",
|
||||
// * However, many places render the content without setting "mode" in Metas, all these places used comment line break setting incorrectly
|
||||
ForceHardLineBreak bool
|
||||
|
||||
// Gitea will emit some internal attributes for various purposes, these attributes don't affect rendering.
|
||||
// Gitea will emit some additional attributes for various purposes, these attributes don't affect rendering.
|
||||
// But there are too many hard-coded test cases, to avoid changing all of them again and again, we can disable emitting these internal attributes.
|
||||
DisableInternalAttributes bool
|
||||
DisableAdditionalAttributes bool
|
||||
}
|
||||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
Ctx context.Context
|
||||
RelativePath string // relative path from tree root of the branch
|
||||
type RenderOptions struct {
|
||||
UseAbsoluteLink bool
|
||||
|
||||
// relative path from tree root of the branch
|
||||
RelativePath string
|
||||
|
||||
// eg: "orgmode", "asciicast", "console"
|
||||
// for file mode, it could be left as empty, and will be detected by file extension in RelativePath
|
||||
MarkupType string
|
||||
|
||||
Links Links // special link references for rendering, especially when there is a branch/tree path
|
||||
|
||||
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
|
||||
// BranchNameSubURL (for iframe&asciicast)
|
||||
// markupAllowShortIssuePattern, markupContentMode (wiki)
|
||||
// markupAllowShortIssuePattern
|
||||
// markdownLineBreakStyle (comment, document)
|
||||
Metas map[string]string
|
||||
|
||||
GitRepo *git.Repository
|
||||
Repo gitrepo.Repository
|
||||
ShaExistCache map[string]bool
|
||||
cancelFn func()
|
||||
SidebarTocNode ast.Node
|
||||
RenderMetaAs RenderMetaMode
|
||||
InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||
InStandalonePage bool
|
||||
}
|
||||
|
||||
// RenderContext represents a render context
|
||||
type RenderContext struct {
|
||||
ctx context.Context
|
||||
|
||||
// the context might be used by the "render" function, but it might also be used by "postProcess" function
|
||||
usedByRender bool
|
||||
|
||||
SidebarTocNode ast.Node
|
||||
|
||||
RenderHelper RenderHelper
|
||||
RenderOptions RenderOptions
|
||||
RenderInternal internal.RenderInternal
|
||||
}
|
||||
|
||||
// Cancel runs any cleanup functions that have been registered for this Ctx
|
||||
func (ctx *RenderContext) Cancel() {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
ctx.ShaExistCache = map[string]bool{}
|
||||
if ctx.cancelFn == nil {
|
||||
return
|
||||
}
|
||||
ctx.cancelFn()
|
||||
func (ctx *RenderContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return ctx.ctx.Deadline()
|
||||
}
|
||||
|
||||
// AddCancel adds the provided fn as a Cleanup for this Ctx
|
||||
func (ctx *RenderContext) AddCancel(fn func()) {
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
oldCancelFn := ctx.cancelFn
|
||||
if oldCancelFn == nil {
|
||||
ctx.cancelFn = fn
|
||||
return
|
||||
}
|
||||
ctx.cancelFn = func() {
|
||||
defer oldCancelFn()
|
||||
fn()
|
||||
}
|
||||
func (ctx *RenderContext) Done() <-chan struct{} {
|
||||
return ctx.ctx.Done()
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) IsMarkupContentWiki() bool {
|
||||
return ctx.Metas != nil && ctx.Metas["markupContentMode"] == "wiki"
|
||||
func (ctx *RenderContext) Err() error {
|
||||
return ctx.ctx.Err()
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) Value(key any) any {
|
||||
return ctx.ctx.Value(key)
|
||||
}
|
||||
|
||||
var _ context.Context = (*RenderContext)(nil)
|
||||
|
||||
func NewRenderContext(ctx context.Context) *RenderContext {
|
||||
return &RenderContext{ctx: ctx, RenderHelper: &SimpleRenderHelper{}}
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithMarkupType(typ string) *RenderContext {
|
||||
ctx.RenderOptions.MarkupType = typ
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithRelativePath(path string) *RenderContext {
|
||||
ctx.RenderOptions.RelativePath = path
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext {
|
||||
ctx.RenderOptions.Metas = metas
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext {
|
||||
ctx.RenderOptions.InStandalonePage = v
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithUseAbsoluteLink(v bool) *RenderContext {
|
||||
ctx.RenderOptions.UseAbsoluteLink = v
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
|
||||
ctx.RenderHelper = helper
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.MarkupType == "" && ctx.RelativePath != "" {
|
||||
ctx.MarkupType = DetectMarkupTypeByFileName(ctx.RelativePath)
|
||||
if ctx.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
|
||||
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
renderer := renderers[ctx.MarkupType]
|
||||
renderer := renderers[ctx.RenderOptions.MarkupType]
|
||||
if renderer == nil {
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.MarkupType)
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
}
|
||||
|
||||
if ctx.RelativePath != "" {
|
||||
if ctx.RenderOptions.RelativePath != "" {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||
if !ctx.InStandalonePage {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||
return renderIFrame(ctx, output)
|
||||
@ -151,10 +168,10 @@ width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
|
||||
sandbox="allow-scripts"
|
||||
></iframe>`,
|
||||
setting.AppSubURL,
|
||||
url.PathEscape(ctx.Metas["user"]),
|
||||
url.PathEscape(ctx.Metas["repo"]),
|
||||
ctx.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RelativePath),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
ctx.RenderOptions.Metas["BranchNameSubURL"],
|
||||
url.PathEscape(ctx.RenderOptions.RelativePath),
|
||||
))
|
||||
return err
|
||||
}
|
||||
@ -168,6 +185,11 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
ctx.usedByRender = true
|
||||
if ctx.RenderHelper != nil {
|
||||
defer ctx.RenderHelper.CleanUp()
|
||||
}
|
||||
|
||||
finalProcessor := ctx.RenderInternal.Init(output)
|
||||
defer finalProcessor.Close()
|
||||
|
||||
@ -176,7 +198,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
pr1, pw1, close1 := pipes()
|
||||
defer close1()
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx.Ctx)
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
|
||||
|
||||
if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
|
||||
@ -192,7 +214,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
|
||||
eg.Go(func() (err error) {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
err = PostProcess(ctx, pr1, pw2)
|
||||
err = PostProcessDefault(ctx, pr1, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr1)
|
||||
}
|
||||
@ -209,11 +231,8 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
}
|
||||
|
||||
// Init initializes the render global variables
|
||||
func Init(ph *ProcessorHelper) {
|
||||
if ph != nil {
|
||||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
func Init(renderHelpFuncs *RenderHelperFuncs) {
|
||||
DefaultRenderHelperFuncs = renderHelpFuncs
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
@ -230,3 +249,42 @@ func Init(ph *ProcessorHelper) {
|
||||
func ComposeSimpleDocumentMetas() map[string]string {
|
||||
return map[string]string{"markdownLineBreakStyle": "document"}
|
||||
}
|
||||
|
||||
type TestRenderHelper struct {
|
||||
ctx *RenderContext
|
||||
BaseLink string
|
||||
}
|
||||
|
||||
func (r *TestRenderHelper) CleanUp() {}
|
||||
|
||||
func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool {
|
||||
return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a")
|
||||
}
|
||||
|
||||
func (r *TestRenderHelper) ResolveLink(link string, likeType LinkType) string {
|
||||
return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
|
||||
}
|
||||
|
||||
var _ RenderHelper = (*TestRenderHelper)(nil)
|
||||
|
||||
// NewTestRenderContext is a helper function to create a RenderContext for testing purpose
|
||||
// It accepts string (BaseLink), map[string]string (Metas)
|
||||
func NewTestRenderContext(baseLinkOrMetas ...any) *RenderContext {
|
||||
if !setting.IsInTesting {
|
||||
panic("NewTestRenderContext should only be used in testing")
|
||||
}
|
||||
helper := &TestRenderHelper{}
|
||||
ctx := NewRenderContext(context.Background()).WithHelper(helper)
|
||||
helper.ctx = ctx
|
||||
for _, v := range baseLinkOrMetas {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
helper.BaseLink = v
|
||||
case map[string]string:
|
||||
ctx = ctx.WithMetas(v)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %T", v))
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
@ -6,16 +6,52 @@ package markup
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// ProcessorHelper is a helper for the rendering processors (it could be renamed to RenderHelper in the future).
|
||||
// The main purpose of this helper is to decouple some functions which are not directly available in this package.
|
||||
type ProcessorHelper struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
type LinkType string
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
const (
|
||||
LinkTypeApp LinkType = "app" // the link is relative to the AppSubURL
|
||||
LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path)
|
||||
LinkTypeMedia LinkType = "media" // the link should be used to access media files (images, videos)
|
||||
LinkTypeRaw LinkType = "raw" // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
|
||||
)
|
||||
|
||||
type RenderHelper interface {
|
||||
CleanUp()
|
||||
|
||||
// TODO: such dependency is not ideal. We should decouple the processors step by step.
|
||||
// It should make the render choose different processors for different purposes,
|
||||
// but not make processors to guess "is it rendering a comment or a wiki?" or "does it need to check commit ID?"
|
||||
|
||||
IsCommitIDExisting(commitID string) bool
|
||||
ResolveLink(link string, likeType LinkType) string
|
||||
}
|
||||
|
||||
// RenderHelperFuncs is used to decouple cycle-import
|
||||
// At the moment there are different packages:
|
||||
// modules/markup: basic markup rendering
|
||||
// models/renderhelper: need to access models and git repo, and models/issues needs it
|
||||
// services/markup: some real helper functions could only be provided here because it needs to access various services & templates
|
||||
type RenderHelperFuncs struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||
}
|
||||
|
||||
var DefaultProcessorHelper ProcessorHelper
|
||||
var DefaultRenderHelperFuncs *RenderHelperFuncs
|
||||
|
||||
type SimpleRenderHelper struct{}
|
||||
|
||||
func (r *SimpleRenderHelper) CleanUp() {}
|
||||
|
||||
func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *SimpleRenderHelper) ResolveLink(link string, likeType LinkType) string {
|
||||
return resolveLinkRelative(context.Background(), setting.AppSubURL+"/", "", link, false)
|
||||
}
|
||||
|
||||
var _ RenderHelper = (*SimpleRenderHelper)(nil)
|
||||
|
42
modules/markup/render_link.go
Normal file
42
modules/markup/render_link.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute bool) (finalLink string) {
|
||||
if IsFullURLString(link) {
|
||||
return link
|
||||
}
|
||||
if strings.HasPrefix(link, "/") {
|
||||
if strings.HasPrefix(link, base) && strings.Count(base, "/") >= 4 {
|
||||
// a trick to tolerate that some users were using absolut paths (the old gitea's behavior)
|
||||
finalLink = link
|
||||
} else {
|
||||
finalLink = util.URLJoin(base, "./", link)
|
||||
}
|
||||
} else {
|
||||
finalLink = util.URLJoin(base, "./", cur, link)
|
||||
}
|
||||
finalLink = strings.TrimSuffix(finalLink, "/")
|
||||
if absolute {
|
||||
finalLink = httplib.MakeAbsoluteURL(ctx, finalLink)
|
||||
}
|
||||
return finalLink
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) (finalLink string) {
|
||||
return resolveLinkRelative(ctx, base, cur, link, ctx.RenderOptions.UseAbsoluteLink)
|
||||
}
|
||||
|
||||
func (ctx *RenderContext) ResolveLinkApp(link string) string {
|
||||
return ctx.ResolveLinkRelative(setting.AppSubURL+"/", "", link)
|
||||
}
|
27
modules/markup/render_link_test.go
Normal file
27
modules/markup/render_link_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResolveLinkRelative(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
setting.AppURL = "http://localhost:3000"
|
||||
assert.Equal(t, "/a", resolveLinkRelative(ctx, "/a", "", "", false))
|
||||
assert.Equal(t, "/a/b", resolveLinkRelative(ctx, "/a", "b", "", false))
|
||||
assert.Equal(t, "/a/b/c", resolveLinkRelative(ctx, "/a", "b", "c", false))
|
||||
assert.Equal(t, "/a/c", resolveLinkRelative(ctx, "/a", "b", "/c", false))
|
||||
assert.Equal(t, "http://localhost:3000/a", resolveLinkRelative(ctx, "/a", "", "", true))
|
||||
|
||||
// some users might have used absolute paths a lot, so if the prefix overlaps and has enough slashes, we should tolerate it
|
||||
assert.Equal(t, "/owner/repo/foo/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo", "", "/owner/repo/foo/bar/xxx", false))
|
||||
assert.Equal(t, "/owner/repo/foo/bar/xxx", resolveLinkRelative(ctx, "/owner/repo/foo/bar", "", "/owner/repo/foo/bar/xxx", false))
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type Links struct {
|
||||
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), usually it is the path to the repo
|
||||
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 {
|
||||
if l.AbsolutePrefix {
|
||||
return setting.AppURL
|
||||
}
|
||||
return setting.AppSubURL
|
||||
}
|
||||
|
||||
func (l *Links) HasBranchInfo() bool {
|
||||
return l.BranchPath != ""
|
||||
}
|
||||
|
||||
func (l *Links) SrcLink() string {
|
||||
return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) MediaLink() string {
|
||||
return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) RawLink() string {
|
||||
return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
|
||||
}
|
||||
|
||||
func (l *Links) WikiLink() string {
|
||||
return util.URLJoin(l.Base, "wiki")
|
||||
}
|
||||
|
||||
func (l *Links) WikiRawLink() string {
|
||||
return util.URLJoin(l.Base, "wiki/raw")
|
||||
}
|
||||
|
||||
func (l *Links) ResolveMediaLink(isWiki bool) string {
|
||||
if isWiki {
|
||||
return l.WikiRawLink()
|
||||
} else if l.HasBranchInfo() {
|
||||
return l.MediaLink()
|
||||
}
|
||||
return l.Base
|
||||
}
|
@ -6,7 +6,7 @@ package markup
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -55,7 +55,7 @@ func RegisterRenderer(renderer Renderer) {
|
||||
|
||||
// GetRendererByFileName get renderer by filename
|
||||
func GetRendererByFileName(filename string) Renderer {
|
||||
extension := strings.ToLower(filepath.Ext(filename))
|
||||
extension := strings.ToLower(path.Ext(filename))
|
||||
return extRenderers[extension]
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Chroma always uses 1-2 letters for style names, we could tolerate it at the moment
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^\w{0,2}$`)).OnElements("span")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
|
@ -19,6 +19,7 @@ func TestSanitizer(t *testing.T) {
|
||||
// Code highlighting class
|
||||
`<code class="random string"></code>`, `<code></code>`,
|
||||
`<code class="language-random ui tab active menu attached animating sidebar following bar center"></code>`, `<code></code>`,
|
||||
`<span class="k"></span><span class="nb"></span>`, `<span class="k"></span><span class="nb"></span>`,
|
||||
|
||||
// Input checkbox
|
||||
`<input type="hidden">`, ``,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user