mirror of
https://github.com/go-gitea/gitea.git
synced 2024-06-26 01:15:32 +00:00
Compare commits
86 Commits
90a1f89951
...
8a4f24eee7
Author | SHA1 | Date | |
---|---|---|---|
|
8a4f24eee7 | ||
|
363c123598 | ||
|
4b6eb46e69 | ||
|
0f09c22663 | ||
|
25f3ec5b65 | ||
|
597d1da96b | ||
|
f5dfd7d73c | ||
|
129206da45 | ||
|
f446e3b4ab | ||
|
c70d4f03c9 | ||
|
78e8296e11 | ||
|
e37ecd1732 | ||
|
42718d32af | ||
|
84cbb6c4d2 | ||
|
23147494a7 | ||
|
d4e4226c3c | ||
|
4e7b067a7f | ||
|
fa82a8af12 | ||
|
e4abaff7ff | ||
|
1761459ebc | ||
|
5b56d13e0d | ||
|
e61e9a36b7 | ||
|
bb04311b0b | ||
|
fede3cbada | ||
|
47ca61d8ba | ||
|
7115dce773 | ||
|
90bcdf9829 | ||
|
21ba5ca03b | ||
|
45dbeb5600 | ||
|
130ea31d6d | ||
|
1968c2222d | ||
|
a975ce8d9d | ||
|
e25d6960b5 | ||
|
fc2d75f86d | ||
|
4bf848a06b | ||
|
61c97fdef1 | ||
|
e6ab6e637f | ||
|
397930d8c1 | ||
|
5342a61124 | ||
|
1844dc6c1d | ||
|
507fbf4c3c | ||
|
a2304cb163 | ||
|
4f7d6feab7 | ||
|
6106a61eff | ||
|
0188d82e49 | ||
|
291a00dc57 | ||
|
15debbbe4e | ||
|
ab1948d4a3 | ||
|
8e33746746 | ||
|
da4bbc4247 | ||
|
f7125ab61a | ||
|
24dace8f76 | ||
|
6a3c487d07 | ||
|
e728fd741b | ||
|
06ebae7472 | ||
|
8de8972baf | ||
|
816222243a | ||
|
bd80225ec3 | ||
|
fcc061ae44 | ||
|
138e946c3d | ||
|
9000811118 | ||
|
4ca65fabda | ||
|
1f8ac27b31 | ||
|
c888c933a9 | ||
|
4f9b8b397c | ||
|
a7557494ca | ||
|
93570de496 | ||
|
433963e52c | ||
|
aace3bccc3 | ||
|
8c68c5e436 | ||
|
0f0db6a14f | ||
|
cb27c438a8 | ||
|
fc641b3a28 | ||
|
4b20b51f82 | ||
|
c6854202be | ||
|
9b05bfb173 | ||
|
2788a7ca27 | ||
|
98a61040b1 | ||
|
3cc7f763c3 | ||
|
ab458ce10b | ||
|
a4275951ba | ||
|
352a2cae24 | ||
|
972f807ee7 | ||
|
572fa55fbc | ||
|
1137a0357e | ||
|
e2c43f085f |
|
@ -10,7 +10,8 @@
|
|||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {
|
||||
"version": "3.12"
|
||||
}
|
||||
},
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
|
@ -25,7 +26,7 @@
|
|||
"Vue.volar",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"vitest.explorer",
|
||||
"qwtel.sqlite-viewer",
|
||||
"cweijan.vscode-database-client2",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"Azurite.azurite"
|
||||
]
|
||||
|
|
|
@ -324,7 +324,7 @@ rules:
|
|||
jquery/no-sizzle: [2]
|
||||
jquery/no-slide: [2]
|
||||
jquery/no-submit: [2]
|
||||
jquery/no-text: [0]
|
||||
jquery/no-text: [2]
|
||||
jquery/no-toggle: [2]
|
||||
jquery/no-trigger: [0]
|
||||
jquery/no-trim: [2]
|
||||
|
@ -477,7 +477,7 @@ rules:
|
|||
no-jquery/no-slide: [2]
|
||||
no-jquery/no-sub: [2]
|
||||
no-jquery/no-support: [2]
|
||||
no-jquery/no-text: [0]
|
||||
no-jquery/no-text: [2]
|
||||
no-jquery/no-trigger: [0]
|
||||
no-jquery/no-trim: [2]
|
||||
no-jquery/no-type: [2]
|
||||
|
@ -798,7 +798,7 @@ rules:
|
|||
unicorn/prefer-object-has-own: [0]
|
||||
unicorn/prefer-optional-catch-binding: [2]
|
||||
unicorn/prefer-prototype-methods: [0]
|
||||
unicorn/prefer-query-selector: [0]
|
||||
unicorn/prefer-query-selector: [2]
|
||||
unicorn/prefer-reflect-apply: [0]
|
||||
unicorn/prefer-regexp-test: [2]
|
||||
unicorn/prefer-set-has: [0]
|
||||
|
|
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
|
@ -1,9 +1,10 @@
|
|||
<!-- start tips -->
|
||||
<!-- start tips -->
|
||||
Please check the following:
|
||||
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports.
|
||||
2. Make sure you have read contributing guidelines: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md .
|
||||
3. Describe what your pull request does and which issue you're targeting (if any).
|
||||
4. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
|
||||
5. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
|
||||
6. Delete all these tips before posting.
|
||||
3. For documentations contribution, please go to https://gitea.com/gitea/docs
|
||||
4. Describe what your pull request does and which issue you're targeting (if any).
|
||||
5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
|
||||
6. Your input here will be included in the commit message when this PR has been merged. If you don't want some content to be included, please separate them with a line like `---`.
|
||||
7. Delete all these tips before posting.
|
||||
<!-- end tips -->
|
||||
|
|
|
@ -43,7 +43,7 @@ vscode:
|
|||
- Vue.volar
|
||||
- ms-azuretools.vscode-docker
|
||||
- vitest.explorer
|
||||
- qwtel.sqlite-viewer
|
||||
- cweijan.vscode-database-client2
|
||||
- GitHub.vscode-pull-request-github
|
||||
|
||||
ports:
|
||||
|
|
|
@ -22,6 +22,7 @@ linters:
|
|||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- unparam
|
||||
- wastedassign
|
||||
|
||||
run:
|
||||
|
|
|
@ -358,7 +358,8 @@ $REWRITTEN_PR_SUMMARY
|
|||
|
||||
## Documentation
|
||||
|
||||
If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in the same PR.
|
||||
If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs).
|
||||
**The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.**
|
||||
|
||||
## API v1
|
||||
|
||||
|
|
18
Makefile
18
Makefile
|
@ -25,10 +25,10 @@ COMMA := ,
|
|||
|
||||
XGO_VERSION := go-1.22.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/cosmtrek/air@v1
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.0
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
|
||||
|
@ -36,6 +36,7 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
|||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3
|
||||
|
||||
DOCKER_IMAGE ?= gitea/gitea
|
||||
DOCKER_TAG ?= latest
|
||||
|
@ -213,6 +214,7 @@ help:
|
|||
@echo " - lint-go lint go files"
|
||||
@echo " - lint-go-fix lint go files and fix issues"
|
||||
@echo " - lint-go-vet lint go files with vet"
|
||||
@echo " - lint-go-gopls lint go files with gopls"
|
||||
@echo " - lint-js lint js files"
|
||||
@echo " - lint-js-fix lint js files and fix issues"
|
||||
@echo " - lint-css lint css files"
|
||||
|
@ -366,7 +368,7 @@ lint-frontend: lint-js lint-css
|
|||
lint-frontend-fix: lint-js-fix lint-css-fix
|
||||
|
||||
.PHONY: lint-backend
|
||||
lint-backend: lint-go lint-go-vet lint-editorconfig
|
||||
lint-backend: lint-go lint-go-vet lint-go-gopls lint-editorconfig
|
||||
|
||||
.PHONY: lint-backend-fix
|
||||
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
|
||||
|
@ -424,6 +426,11 @@ lint-go-vet:
|
|||
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
|
||||
@$(GO) vet -vettool=gitea-vet ./...
|
||||
|
||||
.PHONY: lint-go-gopls
|
||||
lint-go-gopls:
|
||||
@echo "Running gopls check..."
|
||||
@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
|
||||
|
||||
.PHONY: lint-editorconfig
|
||||
lint-editorconfig:
|
||||
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
|
||||
|
@ -864,13 +871,14 @@ deps-tools:
|
|||
$(GO) install $(GO_LICENSES_PACKAGE)
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE)
|
||||
$(GO) install $(ACTIONLINT_PACKAGE)
|
||||
$(GO) install $(GOPLS_PACKAGE)
|
||||
|
||||
node_modules: package-lock.json
|
||||
npm install --no-save
|
||||
@touch node_modules
|
||||
|
||||
.venv: poetry.lock
|
||||
poetry install --no-root
|
||||
poetry install
|
||||
@touch .venv
|
||||
|
||||
.PHONY: update
|
||||
|
@ -887,7 +895,7 @@ update-js: node-check | node_modules
|
|||
update-py: node-check | node_modules
|
||||
npx updates -u -f pyproject.toml
|
||||
rm -rf .venv poetry.lock
|
||||
poetry install --no-root
|
||||
poetry install
|
||||
@touch .venv
|
||||
|
||||
.PHONY: fomantic
|
||||
|
|
|
@ -5,7 +5,9 @@ package cmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
|
@ -194,8 +196,20 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
|
|||
|
||||
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
||||
_, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath)
|
||||
return err
|
||||
if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := storage.Copy(dstStorage, artifact.StoragePath, storage.ActionsArtifacts, artifact.StoragePath)
|
||||
if err != nil {
|
||||
// ignore files that do not exist
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,10 @@ RUN_USER = ; git
|
|||
;; Overwrite the automatically generated public URL. Necessary for proxies and docker.
|
||||
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
|
||||
;;
|
||||
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
|
||||
;; DO NOT USE IT IN PRODUCTION!!!
|
||||
;USE_SUB_URL_PATH = false
|
||||
;;
|
||||
;; when STATIC_URL_PREFIX is empty it will follow ROOT_URL
|
||||
;STATIC_URL_PREFIX =
|
||||
;;
|
||||
|
@ -1334,6 +1338,9 @@ LEVEL = Info
|
|||
;;
|
||||
;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
|
||||
;MAX_FILE_SIZE = 524288
|
||||
;;
|
||||
;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
|
||||
;MAX_ROWS = 2500
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -1687,6 +1694,16 @@ LEVEL = Info
|
|||
;; convert \r\n to \n for Sendmail
|
||||
;SENDMAIL_CONVERT_CRLF = true
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[mailer.override_header]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; This is empty by default, use it only if you know what you need it for.
|
||||
;Reply-To = test@example.com, test2@example.com
|
||||
;Content-Type = text/html; charset=utf-8
|
||||
;In-Reply-To =
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[email.incoming]
|
||||
|
|
|
@ -724,11 +724,13 @@ Define allowed algorithms and their minimum key length (use -1 to disable a type
|
|||
|
||||
## Mailer (`mailer`)
|
||||
|
||||
⚠️ This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
|
||||
:::warning
|
||||
This section is for Gitea 1.18 and later. If you are using Gitea 1.17 or older,
|
||||
please refer to
|
||||
[Gitea 1.17 app.ini example](https://github.com/go-gitea/gitea/blob/release/v1.17/custom/conf/app.example.ini)
|
||||
and
|
||||
[Gitea 1.17 configuration document](https://github.com/go-gitea/gitea/blob/release/v1.17/docs/content/doc/advanced/config-cheat-sheet.en-us.md)
|
||||
:::
|
||||
|
||||
- `ENABLED`: **false**: Enable to use a mail service.
|
||||
- `PROTOCOL`: **_empty_**: Mail server protocol. One of "smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy". _Before 1.18, this was inferred from a combination of `MAILER_TYPE` and `IS_TLS_ENABLED`._
|
||||
|
@ -761,6 +763,21 @@ and
|
|||
- `SEND_BUFFER_LEN`: **100**: Buffer length of mailing queue. **DEPRECATED** use `LENGTH` in `[queue.mailer]`
|
||||
- `SEND_AS_PLAIN_TEXT`: **false**: Send mails only in plain text, without HTML alternative.
|
||||
|
||||
## Override Email Headers (`mailer.override_header`)
|
||||
|
||||
:::warning
|
||||
This is empty by default, use it only if you know what you need it for.
|
||||
:::
|
||||
|
||||
examples would be:
|
||||
|
||||
```ini
|
||||
[mailer.override_header]
|
||||
Reply-To = test@example.com, test2@example.com
|
||||
Content-Type = text/html; charset=utf-8
|
||||
In-Reply-To =
|
||||
```
|
||||
|
||||
## Incoming Email (`email.incoming`)
|
||||
|
||||
- `ENABLED`: **false**: Enable handling of incoming emails.
|
||||
|
|
|
@ -47,7 +47,7 @@ We recommend [Google HTML/CSS Style Guide](https://google.github.io/styleguide/h
|
|||
9. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
|
||||
10. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
|
||||
11. Custom event names are recommended to use `ce-` prefix.
|
||||
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-word-break`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
|
||||
12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-ellipsis`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
|
||||
13. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
|
||||
|
||||
### Accessibility / ARIA
|
||||
|
|
|
@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
|
|||
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
|
||||
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
|
||||
11. 推荐使用自定义事件名称前缀`ce-`。
|
||||
12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-word-break`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
|
||||
12. 建议使用 Tailwind CSS,它可以通过 `tw-` 前缀获得,例如 `tw-relative`. Gitea 自身的助手类 CSS 使用 `gt-` 前缀(`gt-ellipsis`),Gitea 自身的私有框架级 CSS 类使用 `g-` 前缀(`g-modal-confirm`)。
|
||||
13. 尽量避免内联脚本和样式,建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免,请解释无法避免的原因。
|
||||
|
||||
### 可访问性 / ARIA
|
||||
|
|
|
@ -25,7 +25,7 @@ It is designed to be compatible with [GitHub Actions workflow badge](https://doc
|
|||
You can use the following URL to get the badge:
|
||||
|
||||
```
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}/badge.svg?branch={branch}&event={event}
|
||||
```
|
||||
|
||||
- `{owner}`: The owner of the repository.
|
||||
|
|
|
@ -44,6 +44,8 @@ You can use the following variables enclosed in `${}` inside these templates whi
|
|||
- PullRequestIndex: Pull request's index number
|
||||
- PullRequestReference: Pull request's reference char with index number. i.e. #1, !2
|
||||
- ClosingIssues: return a string contains all issues which will be closed by this pull request i.e. `close #1, close #2`
|
||||
- ReviewedOn: Which pull request this commit belongs to. For example `Reviewed-on: https://gitea.com/foo/bar/pulls/1`
|
||||
- ReviewedBy: Who approved the pull request before the merge. For example `Reviewed-by: Jane Doe <jane.doe@example.com>`
|
||||
|
||||
## Rebase
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1715534503,
|
||||
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
|
||||
"lastModified": 1717974879,
|
||||
"narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
|
||||
"rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
19
go.mod
19
go.mod
|
@ -20,7 +20,7 @@ require (
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||
github.com/ProtonMail/go-crypto v1.0.0
|
||||
github.com/PuerkitoBio/goquery v1.9.1
|
||||
github.com/alecthomas/chroma/v2 v2.13.0
|
||||
github.com/alecthomas/chroma/v2 v2.14.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.3.10
|
||||
github.com/buildkite/terminal-to-html/v3 v3.11.0
|
||||
|
@ -108,13 +108,13 @@ require (
|
|||
github.com/yuin/goldmark v1.7.0
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/tools v0.19.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
|
||||
google.golang.org/grpc v1.62.1
|
||||
google.golang.org/protobuf v1.33.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
|
@ -293,8 +293,8 @@ require (
|
|||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||
|
@ -311,6 +311,9 @@ replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
|
|||
|
||||
replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5
|
||||
|
||||
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
|
||||
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||
|
|
43
go.sum
43
go.sum
|
@ -82,16 +82,18 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
|
|||
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
|
||||
github.com/RoaringBitmap/roaring v1.9.0 h1:lwKhr90/j0jVXJyh5X+vQN1VVn77rQFfYnh6RDRGCcE=
|
||||
github.com/RoaringBitmap/roaring v1.9.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
|
||||
github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA=
|
||||
github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
|
@ -564,8 +566,6 @@ github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyq
|
|||
github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
|
||||
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
|
||||
|
@ -866,8 +866,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
|||
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.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
|
@ -878,8 +878,8 @@ 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.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
@ -900,8 +900,8 @@ 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -912,8 +912,8 @@ 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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -951,8 +951,8 @@ 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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
||||
|
@ -962,8 +962,8 @@ 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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -975,8 +975,9 @@ 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -991,8 +992,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.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -54,7 +54,6 @@ type FindTaskOptions struct {
|
|||
UpdatedBefore timeutil.TimeStamp
|
||||
StartedBefore timeutil.TimeStamp
|
||||
RunnerID int64
|
||||
IDOrderDesc bool
|
||||
}
|
||||
|
||||
func (opts FindTaskOptions) ToConds() builder.Cond {
|
||||
|
@ -84,8 +83,5 @@ func (opts FindTaskOptions) ToConds() builder.Cond {
|
|||
}
|
||||
|
||||
func (opts FindTaskOptions) ToOrders() string {
|
||||
if opts.IDOrderDesc {
|
||||
return "`id` DESC"
|
||||
}
|
||||
return ""
|
||||
return "`id` DESC"
|
||||
}
|
||||
|
|
|
@ -18,12 +18,6 @@ const (
|
|||
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
|
||||
SearchOrderByOldest SearchOrderBy = "created_unix ASC"
|
||||
SearchOrderByNewest SearchOrderBy = "created_unix DESC"
|
||||
SearchOrderBySize SearchOrderBy = "size ASC"
|
||||
SearchOrderBySizeReverse SearchOrderBy = "size DESC"
|
||||
SearchOrderByGitSize SearchOrderBy = "git_size ASC"
|
||||
SearchOrderByGitSizeReverse SearchOrderBy = "git_size DESC"
|
||||
SearchOrderByLFSSize SearchOrderBy = "lfs_size ASC"
|
||||
SearchOrderByLFSSizeReverse SearchOrderBy = "lfs_size DESC"
|
||||
SearchOrderByID SearchOrderBy = "id ASC"
|
||||
SearchOrderByIDReverse SearchOrderBy = "id DESC"
|
||||
SearchOrderByStars SearchOrderBy = "num_stars ASC"
|
||||
|
|
|
@ -215,16 +215,15 @@ func fileTimestampToTime(timestamp int64) time.Time {
|
|||
return time.UnixMicro(timestamp)
|
||||
}
|
||||
|
||||
func (f *file) loadMetaByPath() (*dbfsMeta, error) {
|
||||
func (f *file) loadMetaByPath() error {
|
||||
var fileMeta dbfsMeta
|
||||
if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
} else if ok {
|
||||
f.metaID = fileMeta.ID
|
||||
f.blockSize = fileMeta.BlockSize
|
||||
return &fileMeta, nil
|
||||
}
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *file) open(flag int) (err error) {
|
||||
|
@ -288,10 +287,7 @@ func (f *file) createEmpty() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = f.loadMetaByPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return f.loadMetaByPath()
|
||||
}
|
||||
|
||||
func (f *file) truncate() error {
|
||||
|
@ -368,8 +364,5 @@ func buildPath(path string) string {
|
|||
func newDbFile(ctx context.Context, path string) (*file, error) {
|
||||
path = buildPath(path)
|
||||
f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize}
|
||||
if _, err := f.loadMetaByPath(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
return f, f.loadMetaByPath()
|
||||
}
|
||||
|
|
|
@ -107,17 +107,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
|||
|
||||
func (opts FindBranchOptions) ToOrders() string {
|
||||
orderBy := opts.OrderBy
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
|
||||
if orderBy != "" {
|
||||
orderBy += ", "
|
||||
}
|
||||
orderBy += "is_deleted ASC"
|
||||
}
|
||||
if orderBy == "" {
|
||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
||||
return "commit_time DESC, name ASC"
|
||||
orderBy = "commit_time DESC, name ASC"
|
||||
}
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the beginning
|
||||
orderBy = "is_deleted ASC, " + orderBy
|
||||
}
|
||||
|
||||
return orderBy
|
||||
}
|
||||
|
||||
|
|
|
@ -110,6 +110,19 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
|
|||
return tag, nil
|
||||
}
|
||||
|
||||
// GetProtectedTagByNamePattern gets protected tag by name_pattern
|
||||
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
|
||||
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
|
||||
has, err := db.GetEngine(ctx).Get(tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
// IsUserAllowedToControlTag checks if a user can control the specific tag.
|
||||
// It returns true if the tag name is not protected or the user is allowed to control it.
|
||||
func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) {
|
||||
|
|
|
@ -27,23 +27,27 @@ func init() {
|
|||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||
if issue.isAssigneeLoaded || len(issue.Assignees) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset maybe preexisting assignees
|
||||
issue.Assignees = []*user_model.User{}
|
||||
issue.Assignee = nil
|
||||
|
||||
err = db.GetEngine(ctx).Table("`user`").
|
||||
if err = db.GetEngine(ctx).Table("`user`").
|
||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||
Find(&issue.Assignees)
|
||||
if err != nil {
|
||||
Find(&issue.Assignees); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.isAssigneeLoaded = true
|
||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
|
|
|
@ -16,25 +16,25 @@ import (
|
|||
// CommentList defines a list of comments
|
||||
type CommentList []*Comment
|
||||
|
||||
func (comments CommentList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.PosterID, c.PosterID > 0
|
||||
})
|
||||
}
|
||||
|
||||
// LoadPosters loads posters
|
||||
func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterMaps, err := getPosters(ctx, comments.getPosterIDs())
|
||||
posterIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
|
||||
return c.PosterID, c.Poster == nil && c.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
if comment.Poster == nil {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -98,32 +98,35 @@ var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already
|
|||
|
||||
// Issue represents an issue or pull request of repository.
|
||||
type Issue struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||
PosterID int64 `xorm:"INDEX"`
|
||||
Poster *user_model.User `xorm:"-"`
|
||||
OriginalAuthor string
|
||||
OriginalAuthorID int64 `xorm:"index"`
|
||||
Title string `xorm:"name"`
|
||||
Content string `xorm:"LONGTEXT"`
|
||||
RenderedContent template.HTML `xorm:"-"`
|
||||
ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
|
||||
Labels []*Label `xorm:"-"`
|
||||
isLabelsLoaded bool `xorm:"-"`
|
||||
MilestoneID int64 `xorm:"INDEX"`
|
||||
Milestone *Milestone `xorm:"-"`
|
||||
isMilestoneLoaded bool `xorm:"-"`
|
||||
Project *project_model.Project `xorm:"-"`
|
||||
Priority int
|
||||
AssigneeID int64 `xorm:"-"`
|
||||
Assignee *user_model.User `xorm:"-"`
|
||||
isAssigneeLoaded bool `xorm:"-"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
IsRead bool `xorm:"-"`
|
||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
||||
PullRequest *PullRequest `xorm:"-"`
|
||||
NumComments int
|
||||
Ref string
|
||||
PinOrder int `xorm:"DEFAULT 0"`
|
||||
|
||||
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
|
@ -131,11 +134,12 @@ type Issue struct {
|
|||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
isAttachmentsLoaded bool `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
TotalTrackedTime int64 `xorm:"-"`
|
||||
Assignees []*user_model.User `xorm:"-"`
|
||||
|
||||
// IsLocked limits commenting abilities to users on an issue
|
||||
// with write access
|
||||
|
@ -187,6 +191,19 @@ func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) LoadAttachments(ctx context.Context) (err error) {
|
||||
if issue.isAttachmentsLoaded || issue.Attachments != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isAttachmentsLoaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTimetrackerEnabled returns true if the repo enables timetracking
|
||||
func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
|
@ -287,11 +304,12 @@ func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
|||
|
||||
// LoadMilestone load milestone of this issue.
|
||||
func (issue *Issue) LoadMilestone(ctx context.Context) (err error) {
|
||||
if (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
if !issue.isMilestoneLoaded && (issue.Milestone == nil || issue.Milestone.ID != issue.MilestoneID) && issue.MilestoneID > 0 {
|
||||
issue.Milestone, err = GetMilestoneByRepoID(ctx, issue.RepoID, issue.MilestoneID)
|
||||
if err != nil && !IsErrMilestoneNotExist(err) {
|
||||
return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %w", issue.RepoID, issue.MilestoneID, err)
|
||||
}
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -327,11 +345,8 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if issue.Attachments == nil {
|
||||
issue.Attachments, err = repo_model.GetAttachmentsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAttachmentsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
if err = issue.LoadAttachments(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.loadComments(ctx); err != nil {
|
||||
|
@ -350,6 +365,13 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||
return issue.loadReactions(ctx)
|
||||
}
|
||||
|
||||
func (issue *Issue) ResetAttributesLoaded() {
|
||||
issue.isLabelsLoaded = false
|
||||
issue.isMilestoneLoaded = false
|
||||
issue.isAttachmentsLoaded = false
|
||||
issue.isAssigneeLoaded = false
|
||||
}
|
||||
|
||||
// GetIsRead load the `IsRead` field of the issue
|
||||
func (issue *Issue) GetIsRead(ctx context.Context, userID int64) error {
|
||||
issueUser := &IssueUser{IssueID: issue.ID, UID: userID}
|
||||
|
|
|
@ -111,6 +111,7 @@ func NewIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
|
|||
return err
|
||||
}
|
||||
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -160,6 +161,8 @@ func NewIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *us
|
|||
return err
|
||||
}
|
||||
|
||||
// reload all labels
|
||||
issue.isLabelsLoaded = false
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
|
@ -325,11 +328,12 @@ func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
|||
|
||||
// LoadLabels loads labels
|
||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||
if issue.Labels == nil && issue.ID != 0 {
|
||||
if !issue.isLabelsLoaded && issue.Labels == nil && issue.ID != 0 {
|
||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||
}
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -72,29 +72,29 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi
|
|||
return repo_model.ValuesRepository(repoMaps), nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
return container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, true
|
||||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
||||
func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterMaps, err := getPosters(ctx, issues.getPosterIDs())
|
||||
posterIDs := container.FilterSlice(issues, func(issue *Issue) (int64, bool) {
|
||||
return issue.PosterID, issue.Poster == nil && issue.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
if issue.Poster == nil {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPosters(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||
func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
|
@ -136,7 +136,7 @@ func (issues IssueList) getIssueIDs() []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
func (issues IssueList) LoadLabels(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -177,7 +177,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
// When there are no rows left and we try to close it.
|
||||
// Since that is not relevant for us, we can safely ignore it.
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %w", err1)
|
||||
return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
|
@ -185,6 +185,7 @@ func (issues IssueList) loadLabels(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
issue.isLabelsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -195,7 +196,7 @@ func (issues IssueList) getMilestoneIDs() []int64 {
|
|||
})
|
||||
}
|
||||
|
||||
func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
func (issues IssueList) LoadMilestones(ctx context.Context) error {
|
||||
milestoneIDs := issues.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
|
@ -220,6 +221,7 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
issue.isMilestoneLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -263,7 +265,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
func (issues IssueList) LoadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -310,6 +312,10 @@ func (issues IssueList) loadAssignees(ctx context.Context) error {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Assignees = assignees[issue.ID]
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
issue.isAssigneeLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -413,6 +419,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) {
|
|||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
issue.isAttachmentsLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -538,23 +545,23 @@ func (issues IssueList) LoadAttributes(ctx context.Context) error {
|
|||
return fmt.Errorf("issue.loadAttributes: LoadRepositories: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %w", err)
|
||||
if err := issues.LoadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadPosters: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %w", err)
|
||||
if err := issues.LoadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadLabels: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %w", err)
|
||||
if err := issues.LoadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: LoadMilestones: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.LoadProjects(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.loadAssignees(ctx); err != nil {
|
||||
if err := issues.LoadAssignees(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -99,9 +99,9 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
|||
}
|
||||
}
|
||||
|
||||
func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyLimit(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if opts.Paginator == nil || opts.Paginator.IsListAll() {
|
||||
return sess
|
||||
return
|
||||
}
|
||||
|
||||
start := 0
|
||||
|
@ -109,11 +109,9 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
|||
start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
|
||||
}
|
||||
sess.Limit(opts.Paginator.PageSize, start)
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if len(opts.LabelIDs) > 0 {
|
||||
if opts.LabelIDs[0] == 0 {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
|
||||
|
@ -136,11 +134,9 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
|
|||
if len(opts.ExcludedLabelNames) > 0 {
|
||||
sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames)))
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID {
|
||||
sess.And("issue.milestone_id = 0")
|
||||
} else if len(opts.MilestoneIDs) > 0 {
|
||||
|
@ -153,11 +149,9 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
|
|||
From("milestone").
|
||||
Where(builder.In("name", opts.IncludeMilestones)))
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if opts.ProjectID > 0 { // specific project
|
||||
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
|
||||
And("project_issue.project_id=?", opts.ProjectID)
|
||||
|
@ -166,10 +160,9 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
|
|||
}
|
||||
// opts.ProjectID == 0 means all projects,
|
||||
// do not need to apply any condition
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||
// opts.ProjectColumnID == 0 means all project columns,
|
||||
// do not need to apply any condition
|
||||
if opts.ProjectColumnID > 0 {
|
||||
|
@ -177,10 +170,9 @@ func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.
|
|||
} else if opts.ProjectColumnID == db.NoConditionID {
|
||||
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if len(opts.RepoIDs) == 1 {
|
||||
opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
|
||||
} else if len(opts.RepoIDs) > 1 {
|
||||
|
@ -195,10 +187,9 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
|
|||
if opts.RepoCond != nil {
|
||||
sess.And(opts.RepoCond)
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
||||
func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
|
||||
if len(opts.IssueIDs) > 0 {
|
||||
sess.In("issue.id", opts.IssueIDs)
|
||||
}
|
||||
|
@ -261,8 +252,6 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
|
|||
if opts.User != nil {
|
||||
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
|
||||
|
@ -339,22 +328,22 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
|
|||
return cond
|
||||
}
|
||||
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session {
|
||||
return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", assigneeID)
|
||||
}
|
||||
|
||||
func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session {
|
||||
return sess.And("issue.poster_id=?", posterID)
|
||||
func applyPosterCondition(sess *xorm.Session, posterID int64) {
|
||||
sess.And("issue.poster_id=?", posterID)
|
||||
}
|
||||
|
||||
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session {
|
||||
return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
|
||||
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
|
||||
sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
|
||||
And("issue_user.is_mentioned = ?", true).
|
||||
And("issue_user.uid = ?", mentionedID)
|
||||
}
|
||||
|
||||
func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session {
|
||||
func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) {
|
||||
existInTeamQuery := builder.Select("team_user.team_id").
|
||||
From("team_user").
|
||||
Where(builder.Eq{"team_user.uid": reviewRequestedID})
|
||||
|
@ -375,11 +364,11 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
|
|||
),
|
||||
builder.In("review.id", maxReview),
|
||||
))
|
||||
return sess.Where("issue.poster_id <> ?", reviewRequestedID).
|
||||
sess.Where("issue.poster_id <> ?", reviewRequestedID).
|
||||
And(builder.In("issue.id", subQuery))
|
||||
}
|
||||
|
||||
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session {
|
||||
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) {
|
||||
// Query for pull requests where you are a reviewer or commenter, excluding
|
||||
// any pull requests already returned by the review requested filter.
|
||||
notPoster := builder.Neq{"issue.poster_id": reviewedID}
|
||||
|
@ -406,11 +395,11 @@ func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session
|
|||
builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview),
|
||||
)),
|
||||
)
|
||||
return sess.And(notPoster, builder.Or(reviewed, commented))
|
||||
sess.And(notPoster, builder.Or(reviewed, commented))
|
||||
}
|
||||
|
||||
func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session {
|
||||
return sess.And(
|
||||
func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
|
||||
sess.And(
|
||||
builder.
|
||||
NotIn("issue.id",
|
||||
builder.Select("issue_id").
|
||||
|
|
|
@ -159,10 +159,11 @@ type PullRequest struct {
|
|||
|
||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
RequestedReviewers []*user_model.User `xorm:"-"`
|
||||
isRequestedReviewersLoaded bool `xorm:"-"`
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||
|
@ -289,7 +290,7 @@ func (pr *PullRequest) LoadHeadRepo(ctx context.Context) (err error) {
|
|||
|
||||
// LoadRequestedReviewers loads the requested reviewers.
|
||||
func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
if len(pr.RequestedReviewers) > 0 {
|
||||
if pr.isRequestedReviewersLoaded || len(pr.RequestedReviewers) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -297,10 +298,10 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pr.isRequestedReviewersLoaded = true
|
||||
for _, review := range reviews {
|
||||
pr.RequestedReviewers = append(pr.RequestedReviewers, review.Reviewer)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
|
@ -26,7 +28,7 @@ type PullRequestsOptions struct {
|
|||
MilestoneID int64
|
||||
}
|
||||
|
||||
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) {
|
||||
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
|
||||
sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
|
||||
|
||||
sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
|
||||
|
@ -44,7 +46,7 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
|
|||
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
return sess
|
||||
}
|
||||
|
||||
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
|
||||
|
@ -123,28 +125,20 @@ func GetPullRequestIDsByCheckStatus(ctx context.Context, status PullRequestStatu
|
|||
}
|
||||
|
||||
// PullRequests returns all pull requests for a base Repo by the given conditions
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
|
||||
func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (PullRequestList, int64, error) {
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
countSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
|
||||
if err != nil {
|
||||
log.Error("listPullRequestStatement: %v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
countSession := listPullRequestStatement(ctx, baseRepoID, opts)
|
||||
maxResults, err := countSession.Count(new(PullRequest))
|
||||
if err != nil {
|
||||
log.Error("Count PRs: %v", err)
|
||||
return nil, maxResults, err
|
||||
}
|
||||
|
||||
findSession, err := listPullRequestStatement(ctx, baseRepoID, opts)
|
||||
findSession := listPullRequestStatement(ctx, baseRepoID, opts)
|
||||
applySorts(findSession, opts.SortType, 0)
|
||||
if err != nil {
|
||||
log.Error("listPullRequestStatement: %v", err)
|
||||
return nil, maxResults, err
|
||||
}
|
||||
findSession = db.SetSessionPagination(findSession, opts)
|
||||
prs := make([]*PullRequest, 0, opts.PageSize)
|
||||
return prs, maxResults, findSession.Find(&prs)
|
||||
|
@ -153,50 +147,92 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||
// PullRequestList defines a list of pull requests
|
||||
type PullRequestList []*PullRequest
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||
repoIDs := make(container.Set[int64])
|
||||
for _, pr := range prs {
|
||||
if pr.BaseRepo == nil && pr.BaseRepoID > 0 {
|
||||
repoIDs.Add(pr.BaseRepoID)
|
||||
}
|
||||
if pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
||||
repoIDs.Add(pr.HeadRepoID)
|
||||
}
|
||||
}
|
||||
return repoIDs.Values()
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
issues := make([]*Issue, 0, len(issueIDs))
|
||||
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||
repoIDs := prs.getRepositoryIDs()
|
||||
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
set := make(map[int64]*Issue)
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
In("id", repoIDs).
|
||||
Find(&reposMap); err != nil {
|
||||
return fmt.Errorf("find repos: %w", err)
|
||||
}
|
||||
for _, pr := range prs {
|
||||
pr.Issue = set[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
if pr.BaseRepo == nil {
|
||||
pr.BaseRepo = reposMap[pr.BaseRepoID]
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
pr.HeadRepo = reposMap[pr.HeadRepoID]
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadAttributes(ctx context.Context) error {
|
||||
if _, err := prs.LoadIssues(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
|
||||
if len(prs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Load issues which are not loaded
|
||||
issueIDs := container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||
return pr.IssueID, pr.Issue == nil && pr.IssueID > 0
|
||||
})
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return nil, fmt.Errorf("find issues: %w", err)
|
||||
}
|
||||
|
||||
issueList := make(IssueList, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
if pr.Issue == nil {
|
||||
pr.Issue = issues[pr.IssueID]
|
||||
/*
|
||||
Old code:
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
|
||||
It's worth panic because it's almost impossible to happen under normal use.
|
||||
But in integration testing, an asynchronous task could read a database that has been reset.
|
||||
So returning an error would make more sense, let the caller has a choice to ignore it.
|
||||
*/
|
||||
if pr.Issue == nil {
|
||||
return nil, fmt.Errorf("issues and prs may be not in sync: cannot find issue %v for pr %v: %w", pr.IssueID, pr.ID, util.ErrNotExist)
|
||||
}
|
||||
}
|
||||
pr.Issue.PullRequest = pr
|
||||
if pr.Issue.Repo == nil {
|
||||
pr.Issue.Repo = pr.BaseRepo
|
||||
}
|
||||
issueList = append(issueList, pr.Issue)
|
||||
}
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// GetIssueIDs returns all issue ids
|
||||
func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
}
|
||||
return issueIDs
|
||||
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
|
||||
return pr.IssueID, pr.IssueID > 0
|
||||
})
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
|
|
|
@ -92,7 +92,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
|||
|
||||
// Here to catch weird edge-cases where column constraints above are
|
||||
// not applied by the DB backend
|
||||
_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL")
|
||||
_, err := x.Exec("UPDATE `repository` set `object_format_name` = 'sha1' WHERE `object_format_name` = '' or `object_format_name` IS NULL")
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,13 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
|
|||
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
|
||||
}
|
||||
|
||||
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
// AvatarLink returns the full avatar url with http host or the empty string if the repo doesn't have an avatar.
|
||||
//
|
||||
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
func (repo *Repository) AvatarLink(ctx context.Context) string {
|
||||
return httplib.MakeAbsoluteURL(ctx, repo.relAvatarLink(ctx))
|
||||
relLink := repo.relAvatarLink(ctx)
|
||||
if relLink != "" {
|
||||
return httplib.MakeAbsoluteURL(ctx, relLink)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
28
models/repo/avatar_test.go
Normal file
28
models/repo/avatar_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoAvatarLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||
|
||||
repo := &Repository{ID: 1, Avatar: "avatar.png"}
|
||||
link := repo.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link)
|
||||
|
||||
setting.AppURL = "https://localhost/sub-path/"
|
||||
setting.AppSubURL = "/sub-path"
|
||||
link = repo.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link)
|
||||
}
|
|
@ -207,31 +207,6 @@ type SearchRepoOptions struct {
|
|||
OnlyShowRelevant bool
|
||||
}
|
||||
|
||||
// SearchOrderBy is used to sort the result
|
||||
type SearchOrderBy string
|
||||
|
||||
func (s SearchOrderBy) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Strings for sorting result
|
||||
const (
|
||||
SearchOrderByAlphabetically SearchOrderBy = "name ASC"
|
||||
SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC"
|
||||
SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC"
|
||||
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
|
||||
SearchOrderByOldest SearchOrderBy = "created_unix ASC"
|
||||
SearchOrderByNewest SearchOrderBy = "created_unix DESC"
|
||||
SearchOrderBySize SearchOrderBy = "size ASC"
|
||||
SearchOrderBySizeReverse SearchOrderBy = "size DESC"
|
||||
SearchOrderByID SearchOrderBy = "id ASC"
|
||||
SearchOrderByIDReverse SearchOrderBy = "id DESC"
|
||||
SearchOrderByStars SearchOrderBy = "num_stars ASC"
|
||||
SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC"
|
||||
SearchOrderByForks SearchOrderBy = "num_forks ASC"
|
||||
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
||||
)
|
||||
|
||||
// UserOwnedRepoCond returns user ownered repositories
|
||||
func UserOwnedRepoCond(userID int64) builder.Cond {
|
||||
return builder.Eq{
|
||||
|
|
|
@ -5,20 +5,48 @@ package repo
|
|||
|
||||
import "code.gitea.io/gitea/models/db"
|
||||
|
||||
// SearchOrderByMap represents all possible search order
|
||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||
// OrderByMap represents all possible search order
|
||||
var OrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||
"asc": {
|
||||
"alpha": "owner_name ASC, name ASC",
|
||||
"created": db.SearchOrderByOldest,
|
||||
"updated": db.SearchOrderByLeastUpdated,
|
||||
"size": db.SearchOrderBySize,
|
||||
"id": db.SearchOrderByID,
|
||||
"alpha": "owner_name ASC, name ASC",
|
||||
"created": db.SearchOrderByOldest,
|
||||
"updated": db.SearchOrderByLeastUpdated,
|
||||
"size": "size ASC",
|
||||
"git_size": "git_size ASC",
|
||||
"lfs_size": "lfs_size ASC",
|
||||
"id": db.SearchOrderByID,
|
||||
"stars": db.SearchOrderByStars,
|
||||
"forks": db.SearchOrderByForks,
|
||||
},
|
||||
"desc": {
|
||||
"alpha": "owner_name DESC, name DESC",
|
||||
"created": db.SearchOrderByNewest,
|
||||
"updated": db.SearchOrderByRecentUpdated,
|
||||
"size": db.SearchOrderBySizeReverse,
|
||||
"id": db.SearchOrderByIDReverse,
|
||||
"alpha": "owner_name DESC, name DESC",
|
||||
"created": db.SearchOrderByNewest,
|
||||
"updated": db.SearchOrderByRecentUpdated,
|
||||
"size": "size DESC",
|
||||
"git_size": "git_size DESC",
|
||||
"lfs_size": "lfs_size DESC",
|
||||
"id": db.SearchOrderByIDReverse,
|
||||
"stars": db.SearchOrderByStarsReverse,
|
||||
"forks": db.SearchOrderByForksReverse,
|
||||
},
|
||||
}
|
||||
|
||||
// OrderByFlatMap is similar to OrderByMap but use human language keywords
|
||||
// to decide between asc and desc
|
||||
var OrderByFlatMap = map[string]db.SearchOrderBy{
|
||||
"newest": OrderByMap["desc"]["created"],
|
||||
"oldest": OrderByMap["asc"]["created"],
|
||||
"leastupdate": OrderByMap["asc"]["updated"],
|
||||
"reversealphabetically": OrderByMap["desc"]["alpha"],
|
||||
"alphabetically": OrderByMap["asc"]["alpha"],
|
||||
"reversesize": OrderByMap["desc"]["size"],
|
||||
"size": OrderByMap["asc"]["size"],
|
||||
"reversegitsize": OrderByMap["desc"]["git_size"],
|
||||
"gitsize": OrderByMap["asc"]["git_size"],
|
||||
"reverselfssize": OrderByMap["desc"]["lfs_size"],
|
||||
"lfssize": OrderByMap["asc"]["lfs_size"],
|
||||
"moststars": OrderByMap["desc"]["stars"],
|
||||
"feweststars": OrderByMap["asc"]["stars"],
|
||||
"mostforks": OrderByMap["desc"]["forks"],
|
||||
"fewestforks": OrderByMap["asc"]["forks"],
|
||||
}
|
||||
|
|
|
@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
|||
return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
|
||||
}
|
||||
|
||||
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
// AvatarLink returns the full avatar url with http host.
|
||||
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
func (u *User) AvatarLink(ctx context.Context) string {
|
||||
return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0))
|
||||
relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty
|
||||
return httplib.MakeAbsoluteURL(ctx, relLink)
|
||||
}
|
||||
|
||||
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data
|
||||
|
|
28
models/user/avatar_test.go
Normal file
28
models/user/avatar_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserAvatarLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||
|
||||
u := &User{ID: 1, Avatar: "avatar.png"}
|
||||
link := u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/avatars/avatar.png", link)
|
||||
|
||||
setting.AppURL = "https://localhost/sub-path/"
|
||||
setting.AppSubURL = "/sub-path"
|
||||
link = u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
|
||||
}
|
|
@ -856,6 +856,10 @@ func GetUserByID(ctx context.Context, id int64) (*User, error) {
|
|||
|
||||
// GetUserByIDs returns the user objects by given IDs if exists.
|
||||
func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
users := make([]*User, 0, len(ids))
|
||||
err := db.GetEngine(ctx).In("id", ids).
|
||||
Table("user").
|
||||
|
|
|
@ -18,7 +18,7 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error
|
|||
return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
|
||||
}
|
||||
|
||||
func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) {
|
||||
func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam
|
||||
parsed, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)
|
||||
|
|
|
@ -4,12 +4,67 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) {
|
||||
if pos >= len(str) {
|
||||
return 0, 0, false
|
||||
}
|
||||
r, size = utf8.DecodeRuneInString(str[pos:])
|
||||
if r == utf8.RuneError {
|
||||
r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii
|
||||
}
|
||||
return r, size, true
|
||||
}
|
||||
|
||||
func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
|
||||
end = pos
|
||||
for {
|
||||
r, size, has := naturalSortGetRune(str, end)
|
||||
if !has {
|
||||
break
|
||||
}
|
||||
isCurRuneNum := '0' <= r && r <= '9'
|
||||
if end == pos {
|
||||
isNumber = isCurRuneNum
|
||||
end += size
|
||||
} else if isCurRuneNum == isNumber {
|
||||
end += size
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return end, isNumber
|
||||
}
|
||||
|
||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortLess(s1, s2 string) bool {
|
||||
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
|
||||
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
|
||||
// So we need to handle the number parts by ourselves
|
||||
c := collate.New(language.English, collate.Numeric)
|
||||
return c.CompareString(s1, s2) < 0
|
||||
pos1, pos2 := 0, 0
|
||||
for pos1 < len(s1) && pos2 < len(s2) {
|
||||
end1, isNum1 := naturalSortAdvance(s1, pos1)
|
||||
end2, isNum2 := naturalSortAdvance(s2, pos2)
|
||||
part1, part2 := s1[pos1:end1], s2[pos2:end2]
|
||||
if isNum1 && isNum2 {
|
||||
if part1 != part2 {
|
||||
if len(part1) != len(part2) {
|
||||
return len(part1) < len(part2)
|
||||
}
|
||||
return part1 < part2
|
||||
}
|
||||
} else {
|
||||
if cmp := c.CompareString(part1, part2); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
}
|
||||
pos1, pos2 = end1, end2
|
||||
}
|
||||
return len(s1) < len(s2)
|
||||
}
|
||||
|
|
|
@ -10,21 +10,36 @@ import (
|
|||
)
|
||||
|
||||
func TestNaturalSortLess(t *testing.T) {
|
||||
test := func(s1, s2 string, less bool) {
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
|
||||
testLess := func(s1, s2 string) {
|
||||
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
testEqual := func(s1, s2 string) {
|
||||
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
test("v1.20.0", "v1.2.0", false)
|
||||
test("v1.20.0", "v1.29.0", true)
|
||||
test("v1.20.0", "v1.20.0", false)
|
||||
test("abc", "bcd", true)
|
||||
test("a-1-a", "a-1-b", true)
|
||||
test("2", "12", true)
|
||||
test("a", "ab", true)
|
||||
|
||||
test("A", "b", true)
|
||||
test("a", "B", true)
|
||||
testEqual("", "")
|
||||
testLess("", "a")
|
||||
testLess("", "1")
|
||||
|
||||
test("cafe", "café", true)
|
||||
test("café", "cafe", false)
|
||||
test("caff", "café", false)
|
||||
testLess("v1.2", "v1.2.0")
|
||||
testLess("v1.2.0", "v1.10.0")
|
||||
testLess("v1.20.0", "v1.29.0")
|
||||
testEqual("v1.20.0", "v1.20.0")
|
||||
|
||||
testLess("a", "A")
|
||||
testLess("a", "B")
|
||||
testLess("A", "b")
|
||||
testLess("A", "ab")
|
||||
|
||||
testLess("abc", "bcd")
|
||||
testLess("a-1-a", "a-1-b")
|
||||
testLess("2", "12")
|
||||
|
||||
testLess("cafe", "café")
|
||||
testLess("café", "caff")
|
||||
|
||||
testLess("A-2", "A-11")
|
||||
testLess("0.txt", "1.txt")
|
||||
}
|
||||
|
|
32
modules/cache/cache.go
vendored
32
modules/cache/cache.go
vendored
|
@ -4,6 +4,7 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -35,6 +36,37 @@ func Init() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
testCacheKey = "DefaultCache.TestKey"
|
||||
SlowCacheThreshold = 100 * time.Microsecond
|
||||
)
|
||||
|
||||
func Test() (time.Duration, error) {
|
||||
if defaultCache == nil {
|
||||
return 0, fmt.Errorf("default cache not initialized")
|
||||
}
|
||||
|
||||
testData := fmt.Sprintf("%x", make([]byte, 500))
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if err := defaultCache.Delete(testCacheKey); err != nil {
|
||||
return 0, fmt.Errorf("expect cache to delete data based on key if exist but got: %w", err)
|
||||
}
|
||||
if err := defaultCache.Put(testCacheKey, testData, 10); err != nil {
|
||||
return 0, fmt.Errorf("expect cache to store data but got: %w", err)
|
||||
}
|
||||
testVal, hit := defaultCache.Get(testCacheKey)
|
||||
if !hit {
|
||||
return 0, fmt.Errorf("expect cache hit but got none")
|
||||
}
|
||||
if testVal != testData {
|
||||
return 0, fmt.Errorf("expect cache to return same value as stored but got other")
|
||||
}
|
||||
|
||||
return time.Since(start), nil
|
||||
}
|
||||
|
||||
// GetCache returns the currently configured cache
|
||||
func GetCache() StringCache {
|
||||
return defaultCache
|
||||
|
|
12
modules/cache/cache_test.go
vendored
12
modules/cache/cache_test.go
vendored
|
@ -34,6 +34,18 @@ func TestNewContext(t *testing.T) {
|
|||
assert.Nil(t, con)
|
||||
}
|
||||
|
||||
func TestTest(t *testing.T) {
|
||||
defaultCache = nil
|
||||
_, err := Test()
|
||||
assert.Error(t, err)
|
||||
|
||||
createTestCache()
|
||||
elapsed, err := Test()
|
||||
assert.NoError(t, err)
|
||||
// mem cache should take from 300ns up to 1ms on modern hardware ...
|
||||
assert.Less(t, elapsed, SlowCacheThreshold)
|
||||
}
|
||||
|
||||
func TestGetCache(t *testing.T) {
|
||||
createTestCache()
|
||||
|
||||
|
|
|
@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string {
|
|||
return req.Header.Get("X-Forwarded-Host")
|
||||
}
|
||||
|
||||
// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
|
||||
// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
|
||||
func GuessCurrentAppURL(ctx context.Context) string {
|
||||
return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
|
||||
func GuessCurrentHostURL(ctx context.Context) string {
|
||||
req, ok := ctx.Value(RequestContextKey).(*http.Request)
|
||||
if !ok {
|
||||
return setting.AppURL
|
||||
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||
}
|
||||
// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
|
||||
// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
|
||||
|
@ -74,20 +79,27 @@ func GuessCurrentAppURL(ctx context.Context) string {
|
|||
// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
|
||||
reqScheme := getRequestScheme(req)
|
||||
if reqScheme == "" {
|
||||
return setting.AppURL
|
||||
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||
}
|
||||
reqHost := getForwardedHost(req)
|
||||
if reqHost == "" {
|
||||
reqHost = req.Host
|
||||
}
|
||||
return reqScheme + "://" + reqHost + setting.AppSubURL + "/"
|
||||
return reqScheme + "://" + reqHost
|
||||
}
|
||||
|
||||
func MakeAbsoluteURL(ctx context.Context, s string) string {
|
||||
if IsRelativeURL(s) {
|
||||
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/")
|
||||
// MakeAbsoluteURL tries to make a link to an absolute URL:
|
||||
// * If link is empty, it returns the current app URL.
|
||||
// * If link is absolute, it returns the link.
|
||||
// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
|
||||
func MakeAbsoluteURL(ctx context.Context, link string) string {
|
||||
if link == "" {
|
||||
return GuessCurrentAppURL(ctx)
|
||||
}
|
||||
return s
|
||||
if !IsRelativeURL(link) {
|
||||
return link
|
||||
}
|
||||
return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
|
||||
}
|
||||
|
||||
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
|
||||
|
|
|
@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||
|
||||
ctx := context.Background()
|
||||
assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo"))
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
})
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
|
@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||
"X-Forwarded-Host": {"forwarded-host"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
|
@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
|||
"X-Forwarded-Proto": {"https"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
}
|
||||
|
||||
func TestIsCurrentGiteaSiteURL(t *testing.T) {
|
||||
|
|
|
@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||
searchOpt.MilestoneIDs = opts.MilestoneIDs
|
||||
}
|
||||
|
||||
if opts.ProjectID > 0 {
|
||||
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
||||
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
|
||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id int64) optional.Option[int64] {
|
||||
if id > 0 {
|
||||
|
@ -49,7 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||
return nil
|
||||
}
|
||||
|
||||
searchOpt.ProjectID = convertID(opts.ProjectID)
|
||||
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||
searchOpt.PosterID = convertID(opts.PosterID)
|
||||
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
||||
|
|
|
@ -211,7 +211,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
|
|||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req.Header.Set("Accept", MediaType)
|
||||
req.Header.Set("Accept", AcceptHeader)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
@ -251,6 +251,6 @@ func handleErrorResponse(resp *http.Response) error {
|
|||
return err
|
||||
}
|
||||
|
||||
log.Trace("ErrorResponse: %v", er)
|
||||
log.Trace("ErrorResponse(%v): %v", resp.Status, er)
|
||||
return errors.New(er.Message)
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
|||
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, MediaType, req.Header.Get("Content-type"))
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
|
||||
var batchRequest BatchRequest
|
||||
err := json.NewDecoder(req.Body).Decode(&batchRequest)
|
||||
|
@ -263,7 +263,7 @@ func TestHTTPClientUpload(t *testing.T) {
|
|||
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, MediaType, req.Header.Get("Content-type"))
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
|
||||
var batchRequest BatchRequest
|
||||
err := json.NewDecoder(req.Body).Decode(&batchRequest)
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
const (
|
||||
// MediaType contains the media type for LFS server requests
|
||||
MediaType = "application/vnd.git-lfs+json"
|
||||
// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
|
||||
AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
|
||||
)
|
||||
|
||||
// BatchRequest contains multiple requests processed in one batch operation.
|
||||
|
|
|
@ -37,6 +37,7 @@ func (a *BasicTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCl
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("Download Request: %+v", req)
|
||||
resp, err := performRequest(ctx, a.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -26,7 +26,7 @@ func TestBasicTransferAdapter(t *testing.T) {
|
|||
p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5}
|
||||
|
||||
roundTripHandler := func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
assert.Equal(t, "test-value", req.Header.Get("test-header"))
|
||||
|
||||
url := req.URL.String()
|
||||
|
|
|
@ -5,8 +5,6 @@ package markup
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"regexp"
|
||||
|
@ -15,6 +13,8 @@ import (
|
|||
"code.gitea.io/gitea/modules/csv"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -81,86 +81,38 @@ func writeField(w io.Writer, element, class, field string) error {
|
|||
func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
tmpBlock := bufio.NewWriter(output)
|
||||
maxSize := setting.UI.CSV.MaxFileSize
|
||||
maxRows := setting.UI.CSV.MaxRows
|
||||
|
||||
if maxSize == 0 {
|
||||
return r.tableRender(ctx, input, tmpBlock)
|
||||
if maxSize != 0 {
|
||||
input = io.LimitReader(input, maxSize+1)
|
||||
}
|
||||
|
||||
rawBytes, err := io.ReadAll(io.LimitReader(input, maxSize+1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if int64(len(rawBytes)) <= maxSize {
|
||||
return r.tableRender(ctx, bytes.NewReader(rawBytes), tmpBlock)
|
||||
}
|
||||
return r.fallbackRender(io.MultiReader(bytes.NewReader(rawBytes), input), tmpBlock)
|
||||
}
|
||||
|
||||
func (Renderer) fallbackRender(input io.Reader, tmpBlock *bufio.Writer) error {
|
||||
_, err := tmpBlock.WriteString("<pre>")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scan := bufio.NewScanner(input)
|
||||
scan.Split(bufio.ScanRunes)
|
||||
for scan.Scan() {
|
||||
switch scan.Text() {
|
||||
case `&`:
|
||||
_, err = tmpBlock.WriteString("&")
|
||||
case `'`:
|
||||
_, err = tmpBlock.WriteString("'") // "'" is shorter than "'" and apos was not in HTML until HTML5.
|
||||
case `<`:
|
||||
_, err = tmpBlock.WriteString("<")
|
||||
case `>`:
|
||||
_, err = tmpBlock.WriteString(">")
|
||||
case `"`:
|
||||
_, err = tmpBlock.WriteString(""") // """ is shorter than """.
|
||||
default:
|
||||
_, err = tmpBlock.Write(scan.Bytes())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = scan.Err(); err != nil {
|
||||
return fmt.Errorf("fallbackRender scan: %w", err)
|
||||
}
|
||||
|
||||
_, err = tmpBlock.WriteString("</pre>")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tmpBlock.Flush()
|
||||
}
|
||||
|
||||
func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock *bufio.Writer) error {
|
||||
rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
|
||||
return err
|
||||
}
|
||||
row := 1
|
||||
|
||||
row := 0
|
||||
for {
|
||||
fields, err := rd.Read()
|
||||
if err == io.EOF {
|
||||
if err == io.EOF || (row >= maxRows && maxRows != 0) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := tmpBlock.WriteString("<tr>"); err != nil {
|
||||
return err
|
||||
}
|
||||
element := "td"
|
||||
if row == 1 {
|
||||
if row == 0 {
|
||||
element = "th"
|
||||
}
|
||||
if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
|
||||
if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row+1)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, field := range fields {
|
||||
|
@ -174,8 +126,32 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock
|
|||
|
||||
row++
|
||||
}
|
||||
|
||||
if _, err = tmpBlock.WriteString("</table>"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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) + `">`
|
||||
|
||||
// Try to get the user translation
|
||||
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
|
||||
warn += locale.TrString("repo.file_too_large")
|
||||
rawLink += locale.TrString("repo.file_view_raw")
|
||||
} else {
|
||||
warn += "The file is too large to be shown."
|
||||
rawLink += "View Raw"
|
||||
}
|
||||
|
||||
warn += rawLink + `</a></td></tr></table>`
|
||||
|
||||
// Write the HTML string to the output
|
||||
if _, err := tmpBlock.WriteString(warn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tmpBlock.Flush()
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
package markup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -31,12 +29,4 @@ func TestRenderCSV(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, v, buf.String())
|
||||
}
|
||||
|
||||
t.Run("fallbackRender", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
err := render.fallbackRender(strings.NewReader("1,<a>\n2,<b>"), bufio.NewWriter(&buf))
|
||||
assert.NoError(t, err)
|
||||
want := "<pre>1,<a>\n2,<b></pre>"
|
||||
assert.Equal(t, want, buf.String())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ var (
|
|||
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
|
||||
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
|
||||
// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
|
||||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,](\s|$))`)
|
||||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
|
||||
|
||||
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
||||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||
|
@ -372,7 +372,42 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
|||
return nil
|
||||
}
|
||||
|
||||
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
||||
func handleNodeImg(ctx *RenderContext, img *html.Node) {
|
||||
for i, attr := range img.Attr {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
|
||||
if attr.Val != "" && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "/") {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
||||
|
||||
// By default, the "<img>" tag should also be clickable,
|
||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||
// so it must match the default markdown image behavior.
|
||||
hasParentAnchor := false
|
||||
for p := img.Parent; p != nil; p = p.Parent {
|
||||
if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasParentAnchor {
|
||||
imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
|
||||
{Key: "href", Val: attr.Val},
|
||||
{Key: "target", Val: "_blank"},
|
||||
}}
|
||||
parent := img.Parent
|
||||
imgNext := img.NextSibling
|
||||
parent.RemoveChild(img)
|
||||
parent.InsertBefore(imgA, imgNext)
|
||||
imgA.AppendChild(img)
|
||||
}
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
img.Attr[i] = attr
|
||||
}
|
||||
}
|
||||
|
||||
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
|
||||
// Add user-content- to IDs and "#" links if they don't already have them
|
||||
for idx, attr := range node.Attr {
|
||||
val := strings.TrimPrefix(attr.Val, "#")
|
||||
|
@ -397,21 +432,14 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
|||
textNode(ctx, procs, node)
|
||||
case html.ElementNode:
|
||||
if node.Data == "img" {
|
||||
for i, attr := range node.Attr {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
if len(attr.Val) > 0 && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
node.Attr[i] = attr
|
||||
}
|
||||
next := node.NextSibling
|
||||
handleNodeImg(ctx, node)
|
||||
return next
|
||||
} else if node.Data == "a" {
|
||||
// Restrict text in links to emojis
|
||||
procs = emojiProcessors
|
||||
} else if node.Data == "code" || node.Data == "pre" {
|
||||
return
|
||||
return node.NextSibling
|
||||
} else if node.Data == "i" {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key != "class" {
|
||||
|
@ -434,11 +462,11 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
|||
}
|
||||
}
|
||||
}
|
||||
for n := node.FirstChild; n != nil; n = n.NextSibling {
|
||||
visitNode(ctx, procs, n)
|
||||
for n := node.FirstChild; n != nil; {
|
||||
n = visitNode(ctx, procs, n)
|
||||
}
|
||||
}
|
||||
// ignore everything else
|
||||
return node.NextSibling
|
||||
}
|
||||
|
||||
// textNode runs the passed node through various processors, in order to handle
|
||||
|
@ -851,7 +879,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
|
||||
// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
|
||||
// The "mode" approach should be refactored to some other more clear&reliable way.
|
||||
crossLinkOnly := (ctx.Metas["mode"] == "document" && !ctx.IsWiki)
|
||||
crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
|
||||
var (
|
||||
found bool
|
||||
|
|
|
@ -18,8 +18,7 @@ import (
|
|||
|
||||
const (
|
||||
TestAppURL = "http://localhost:3000/"
|
||||
TestOrgRepo = "gogits/gogs"
|
||||
TestRepoURL = TestAppURL + TestOrgRepo + "/"
|
||||
TestRepoURL = TestAppURL + "test-owner/test-repo/"
|
||||
)
|
||||
|
||||
// externalIssueLink an HTML link to an alphanumeric-style issue
|
||||
|
@ -64,8 +63,8 @@ var regexpMetas = map[string]string{
|
|||
|
||||
// these values should match the TestOrgRepo const above
|
||||
var localMetas = map[string]string{
|
||||
"user": "gogits",
|
||||
"repo": "gogs",
|
||||
"user": "test-owner",
|
||||
"repo": "test-repo",
|
||||
}
|
||||
|
||||
func TestRender_IssueIndexPattern(t *testing.T) {
|
||||
|
@ -362,12 +361,12 @@ func TestRender_FullIssueURLs(t *testing.T) {
|
|||
`Look here <a href="http://localhost:3000/person/repo/issues/4" class="ref-issue">person/repo#4</a>`)
|
||||
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
|
||||
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a>`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4 test",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a> test`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123 test",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a>`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4 test",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a> test`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123 test",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
|
||||
test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24",
|
||||
"http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24")
|
||||
test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files",
|
||||
|
@ -381,6 +380,7 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
|
|||
"(abcdefabcdefabcdefabcdefabcdefabcdefabcd)",
|
||||
"[abcdefabcdefabcdefabcdefabcdefabcdefabcd]",
|
||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd.",
|
||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd:",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"test",
|
||||
|
|
|
@ -120,8 +120,8 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||
}
|
||||
|
||||
test(
|
||||
"gogits/gogs#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogits", "gogs", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogits/gogs#12345</a></p>`)
|
||||
"test-owner/test-repo#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
|
||||
test(
|
||||
"go-gitea/gitea#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
|
||||
|
@ -169,13 +169,18 @@ func TestRender_links(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
// Text that should be turned into URL
|
||||
|
||||
defaultCustom := setting.Markdown.CustomURLSchemes
|
||||
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
defer func() {
|
||||
setting.Markdown.CustomURLSchemes = oldCustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
markup.CustomLinkURLSchemes(oldCustomURLSchemes)
|
||||
}()
|
||||
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
|
||||
// Text that should be turned into URL
|
||||
test(
|
||||
"https://www.example.com",
|
||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
||||
|
@ -259,11 +264,6 @@ func TestRender_links(t *testing.T) {
|
|||
test(
|
||||
"ftps://gitea.com",
|
||||
`<p>ftps://gitea.com</p>`)
|
||||
|
||||
// Restore previous settings
|
||||
setting.Markdown.CustomURLSchemes = defaultCustom
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
||||
func TestRender_email(t *testing.T) {
|
||||
|
@ -530,43 +530,31 @@ func TestRender_ShortLinks(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRender_RelativeImages(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
render := func(input string, isWiki bool, links markup.Links) string {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, 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,
|
||||
},
|
||||
Ctx: git.DefaultContext,
|
||||
Links: links,
|
||||
Metas: localMetas,
|
||||
IsWiki: true,
|
||||
IsWiki: isWiki,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
return strings.TrimSpace(string(buffer))
|
||||
}
|
||||
|
||||
rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
|
||||
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
|
||||
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)
|
||||
|
||||
test(
|
||||
`<img src="Link">`,
|
||||
`<img src="`+util.URLJoin(mediatree, "Link")+`"/>`,
|
||||
`<img src="`+util.URLJoin(rawwiki, "Link")+`"/>`)
|
||||
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)
|
||||
|
||||
test(
|
||||
`<img src="./icon.png">`,
|
||||
`<img src="`+util.URLJoin(mediatree, "icon.png")+`"/>`,
|
||||
`<img src="`+util.URLJoin(rawwiki, "icon.png")+`"/>`)
|
||||
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)
|
||||
}
|
||||
|
||||
func Test_ParseClusterFuzz(t *testing.T) {
|
||||
|
@ -719,5 +707,6 @@ func TestIssue18471(t *testing.T) {
|
|||
func TestIsFullURL(t *testing.T) {
|
||||
assert.True(t, markup.IsFullURLString("https://example.com"))
|
||||
assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
|
||||
assert.True(t, markup.IsFullURLString("data:image/11111"))
|
||||
assert.False(t, markup.IsFullURLString("/foo:bar"))
|
||||
}
|
||||
|
|
|
@ -1019,4 +1019,10 @@ func TestAttention(t *testing.T) {
|
|||
test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
|
||||
test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>")
|
||||
|
||||
// escaped by mdformat
|
||||
test(`> \[!NOTE\]`, renderAttention("note", "octicon-info")+"\n</blockquote>")
|
||||
|
||||
// legacy GitHub style
|
||||
test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
}
|
||||
|
|
|
@ -31,10 +31,16 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
|
|||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
dollars := false
|
||||
var dollars bool
|
||||
if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
|
||||
dollars = true
|
||||
} else if line[pos] != '\\' || line[pos+1] != '[' {
|
||||
} else if line[pos] == '\\' && line[pos+1] == '[' {
|
||||
if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) {
|
||||
// do not process escaped attention block: "> \[!NOTE\]"
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
dollars = false
|
||||
} else {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/markup/common"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type prefixedIDs struct {
|
||||
|
@ -36,7 +36,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
|
|||
if !bytes.HasPrefix(result, []byte("user-content-")) {
|
||||
result = append([]byte("user-content-"), result...)
|
||||
}
|
||||
if p.values.Add(util.BytesToReadOnlyString(result)) {
|
||||
if p.values.Add(util.UnsafeBytesToString(result)) {
|
||||
return result
|
||||
}
|
||||
for i := 1; ; i++ {
|
||||
|
@ -49,7 +49,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
|
|||
|
||||
// Put puts a given element id to the used ids table.
|
||||
func (p *prefixedIDs) Put(value []byte) {
|
||||
p.values.Add(util.BytesToReadOnlyString(value))
|
||||
p.values.Add(util.UnsafeBytesToString(value))
|
||||
}
|
||||
|
||||
func newPrefixedIDs() *prefixedIDs {
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
|
||||
// renderAttention renders a quote marked with i.e. "> **Note**" or "> [!Warning]" with a corresponding svg
|
||||
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*Attention)
|
||||
|
@ -37,38 +37,93 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
|
|||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
||||
// We only want attention blockquotes when the AST looks like:
|
||||
// > Text("[") Text("!TYPE") Text("]")
|
||||
func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 1 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Emphasis)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Text(reader.Source()))
|
||||
attentionType := strings.ToLower(val1)
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// grab these nodes and make sure we adhere to the attention blockquote structure
|
||||
firstParagraph := v.FirstChild()
|
||||
g.applyElementDir(firstParagraph)
|
||||
if firstParagraph.ChildCount() < 3 {
|
||||
return ast.WalkContinue, nil
|
||||
func (g *ASTTransformer) extractBlockquoteAttention2(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 2 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
node2, ok := node1.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Segment.Value(reader.Source()))
|
||||
val2 := string(node2.Segment.Value(reader.Source()))
|
||||
if strings.HasPrefix(val1, `\[!`) && val2 == `\]` {
|
||||
attentionType := strings.ToLower(val1[3:])
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1, node2}
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) extractBlockquoteAttention3(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 3 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Text)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
node2, ok := node1.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
node3, ok := node2.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Segment.Value(reader.Source()))
|
||||
val2 := string(node2.Segment.Value(reader.Source()))
|
||||
val3 := string(node3.Segment.Value(reader.Source()))
|
||||
if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// grab attention type from markdown source
|
||||
attentionType := strings.ToLower(val2[1:])
|
||||
if !g.attentionTypes.Contains(attentionType) {
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1, node2, node3}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
||||
// We only want attention blockquotes when the AST looks like:
|
||||
// > Text("[") Text("!TYPE") Text("]")
|
||||
// > Text("\[!TYPE") TEXT("\]")
|
||||
// > Text("**TYPE**")
|
||||
|
||||
// grab these nodes and make sure we adhere to the attention blockquote structure
|
||||
firstParagraph := v.FirstChild()
|
||||
g.applyElementDir(firstParagraph)
|
||||
|
||||
attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader)
|
||||
if attentionType == "" {
|
||||
attentionType, processedNodes = g.extractBlockquoteAttention2(firstParagraph, reader)
|
||||
}
|
||||
if attentionType == "" {
|
||||
attentionType, processedNodes = g.extractBlockquoteAttention3(firstParagraph, reader)
|
||||
}
|
||||
if attentionType == "" {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
|
@ -88,9 +143,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
|
|||
attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
|
||||
attentionParagraph.AppendChild(attentionParagraph, emphasis)
|
||||
firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
|
||||
firstParagraph.RemoveChild(firstParagraph, node1)
|
||||
firstParagraph.RemoveChild(firstParagraph, node2)
|
||||
firstParagraph.RemoveChild(firstParagraph, node3)
|
||||
for _, processed := range processedNodes {
|
||||
firstParagraph.RemoveChild(firstParagraph, processed)
|
||||
}
|
||||
if firstParagraph.ChildCount() == 0 {
|
||||
firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
|
||||
|
@ -21,11 +21,11 @@ func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Headin
|
|||
}
|
||||
txt := v.Text(reader.Source())
|
||||
header := markup.Header{
|
||||
Text: util.BytesToReadOnlyString(txt),
|
||||
Text: util.UnsafeBytesToString(txt),
|
||||
Level: v.Level,
|
||||
}
|
||||
if id, found := v.AttributeString("id"); found {
|
||||
header.ID = util.BytesToReadOnlyString(id.([]byte))
|
||||
header.ID = util.UnsafeBytesToString(id.([]byte))
|
||||
}
|
||||
*tocList = append(*tocList, header)
|
||||
g.applyElementDir(v)
|
||||
|
|
|
@ -47,7 +47,6 @@ func Init(ph *ProcessorHelper) {
|
|||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
NewSanitizer()
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
@ -75,7 +74,7 @@ type RenderContext struct {
|
|||
Type string
|
||||
IsWiki bool
|
||||
Links Links
|
||||
Metas map[string]string
|
||||
Metas map[string]string // user, repo, mode(comment/document)
|
||||
DefaultLink string
|
||||
GitRepo *git.Repository
|
||||
Repo gitrepo.Repository
|
||||
|
@ -87,10 +86,10 @@ type RenderContext struct {
|
|||
}
|
||||
|
||||
type Links struct {
|
||||
AbsolutePrefix bool
|
||||
Base string
|
||||
BranchPath string
|
||||
TreePath string
|
||||
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
||||
Base string // base prefix for pre-provided links and medias (images, videos)
|
||||
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
||||
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
||||
}
|
||||
|
||||
func (l *Links) Prefix() string {
|
||||
|
|
|
@ -5,13 +5,9 @@
|
|||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
|
@ -21,211 +17,35 @@ type Sanitizer struct {
|
|||
defaultPolicy *bluemonday.Policy
|
||||
descriptionPolicy *bluemonday.Policy
|
||||
rendererPolicies map[string]*bluemonday.Policy
|
||||
init sync.Once
|
||||
allowAllRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
sanitizer = &Sanitizer{}
|
||||
allowAllRegex = regexp.MustCompile(".+")
|
||||
defaultSanitizer *Sanitizer
|
||||
defaultSanitizerOnce sync.Once
|
||||
)
|
||||
|
||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
||||
// entire application lifecycle.
|
||||
func NewSanitizer() {
|
||||
sanitizer.init.Do(func() {
|
||||
InitializeSanitizer()
|
||||
})
|
||||
}
|
||||
|
||||
// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
|
||||
func InitializeSanitizer() {
|
||||
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
||||
sanitizer.defaultPolicy = createDefaultPolicy()
|
||||
sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
|
||||
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := createDefaultPolicy()
|
||||
addSanitizerRules(policy, sanitizerRules)
|
||||
sanitizer.rendererPolicies[name] = policy
|
||||
func GetDefaultSanitizer() *Sanitizer {
|
||||
defaultSanitizerOnce.Do(func() {
|
||||
defaultSanitizer = &Sanitizer{
|
||||
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||
allowAllRegex: regexp.MustCompile(".+"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||
defaultSanitizer.rendererPolicies[name] = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||
})
|
||||
return defaultSanitizer
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.descriptionPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
NewSanitizer()
|
||||
policy, exist := sanitizer.rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = sanitizer.defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
func ResetDefaultSanitizerForTesting() {
|
||||
defaultSanitizer = nil
|
||||
defaultSanitizerOnce = sync.Once{}
|
||||
}
|
||||
|
|
25
modules/markup/sanitizer_custom.go
Normal file
25
modules/markup/sanitizer_custom.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
modules/markup/sanitizer_default.go
Normal file
146
modules/markup/sanitizer_default.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(st.allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
policy, exist := GetDefaultSanitizer().rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = GetDefaultSanitizer().defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
}
|
|
@ -5,18 +5,16 @@
|
|||
package markup
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Sanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
func TestSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
// Regular
|
||||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
||||
"<scrİpt><script>alert(document.domain)</script></scrİpt>", "<script>alert(document.domain)</script>",
|
||||
|
||||
// Code highlighting class
|
||||
`<code class="random string"></code>`, `<code></code>`,
|
||||
|
@ -72,34 +70,3 @@ func Test_Sanitizer(t *testing.T) {
|
|||
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeNonEscape(t *testing.T) {
|
||||
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
||||
|
||||
output := template.HTML(Sanitize(descStr))
|
||||
if strings.Contains(string(output), "<script>") {
|
||||
t.Errorf("un-escaped <script> in output: %q", output)
|
||||
}
|
||||
}
|
37
modules/markup/sanitizer_description.go
Normal file
37
modules/markup/sanitizer_description.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func (st *Sanitizer) createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
policy.AllowStandardURLs()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
return GetDefaultSanitizer().descriptionPolicy.Sanitize(s)
|
||||
}
|
31
modules/markup/sanitizer_description_test.go
Normal file
31
modules/markup/sanitizer_description_test.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">https://example.com</a>`,
|
||||
`<a href="data:1234">data</a>`, `data`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ package composer
|
|||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
|
@ -36,10 +37,14 @@ type Package struct {
|
|||
Metadata *Metadata
|
||||
}
|
||||
|
||||
// https://getcomposer.org/doc/04-schema.md
|
||||
|
||||
// Metadata represents the metadata of a Composer package
|
||||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Readme string `json:"readme,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Comments Comments `json:"_comments,omitempty"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
License Licenses `json:"license,omitempty"`
|
||||
Authors []Author `json:"authors,omitempty"`
|
||||
|
@ -74,6 +79,28 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Comments represents the comments of a Composer package
|
||||
type Comments []string
|
||||
|
||||
// UnmarshalJSON reads from a string or array
|
||||
func (c *Comments) UnmarshalJSON(data []byte) error {
|
||||
switch data[0] {
|
||||
case '"':
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = Comments{value}
|
||||
case '[':
|
||||
values := make([]string, 0, 5)
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = Comments(values)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Author represents an author
|
||||
type Author struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
|
@ -101,14 +128,14 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
return ParseComposerFile(f)
|
||||
return ParseComposerFile(archive, path.Dir(file.Name), f)
|
||||
}
|
||||
}
|
||||
return nil, ErrMissingComposerFile
|
||||
}
|
||||
|
||||
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
|
||||
func ParseComposerFile(r io.Reader) (*Package, error) {
|
||||
func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) {
|
||||
var cj struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
@ -137,6 +164,19 @@ func ParseComposerFile(r io.Reader) (*Package, error) {
|
|||
cj.Type = "library"
|
||||
}
|
||||
|
||||
if cj.Readme == "" {
|
||||
cj.Readme = "README.md"
|
||||
}
|
||||
f, err := archive.Open(path.Join(pathPrefix, cj.Readme))
|
||||
if err == nil {
|
||||
// 10kb limit for readme content
|
||||
buf, _ := io.ReadAll(io.LimitReader(f, 10*1024))
|
||||
cj.Readme = string(buf)
|
||||
_ = f.Close()
|
||||
} else {
|
||||
cj.Readme = ""
|
||||
}
|
||||
|
||||
return &Package{
|
||||
Name: cj.Name,
|
||||
Version: cj.Version,
|
||||
|
|
|
@ -17,6 +17,8 @@ import (
|
|||
const (
|
||||
name = "gitea/composer-package"
|
||||
description = "Package Description"
|
||||
readme = "Package Readme"
|
||||
comments = "Package Comment"
|
||||
packageType = "composer-plugin"
|
||||
author = "Gitea Authors"
|
||||
email = "no.reply@gitea.io"
|
||||
|
@ -41,7 +43,8 @@ const composerContent = `{
|
|||
},
|
||||
"require": {
|
||||
"php": ">=7.2 || ^8.0"
|
||||
}
|
||||
},
|
||||
"_comments": "` + comments + `"
|
||||
}`
|
||||
|
||||
func TestLicenseUnmarshal(t *testing.T) {
|
||||
|
@ -54,18 +57,30 @@ func TestLicenseUnmarshal(t *testing.T) {
|
|||
assert.Equal(t, "MIT", l[0])
|
||||
}
|
||||
|
||||
func TestCommentsUnmarshal(t *testing.T) {
|
||||
var c Comments
|
||||
assert.NoError(t, json.NewDecoder(strings.NewReader(`["comment"]`)).Decode(&c))
|
||||
assert.Len(t, c, 1)
|
||||
assert.Equal(t, "comment", c[0])
|
||||
assert.NoError(t, json.NewDecoder(strings.NewReader(`"comment"`)).Decode(&c))
|
||||
assert.Len(t, c, 1)
|
||||
assert.Equal(t, "comment", c[0])
|
||||
}
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
createArchive := func(name, content string) []byte {
|
||||
createArchive := func(files map[string]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
archive := zip.NewWriter(&buf)
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
for name, content := range files {
|
||||
w, _ := archive.Create(name)
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
archive.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
t.Run("MissingComposerFile", func(t *testing.T) {
|
||||
data := createArchive("dummy.txt", "")
|
||||
data := createArchive(map[string]string{"dummy.txt": ""})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, cp)
|
||||
|
@ -73,7 +88,7 @@ func TestParsePackage(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
|
||||
data := createArchive("sub/sub/composer.json", "")
|
||||
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, cp)
|
||||
|
@ -81,43 +96,52 @@ func TestParsePackage(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("InvalidComposerFile", func(t *testing.T) {
|
||||
data := createArchive("composer.json", "")
|
||||
data := createArchive(map[string]string{"composer.json": ""})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, cp)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
data := createArchive("composer.json", composerContent)
|
||||
t.Run("InvalidPackageName", func(t *testing.T) {
|
||||
data := createArchive(map[string]string{"composer.json": "{}"})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseComposerFile(t *testing.T) {
|
||||
t.Run("InvalidPackageName", func(t *testing.T) {
|
||||
cp, err := ParseComposerFile(strings.NewReader(`{}`))
|
||||
assert.Nil(t, cp)
|
||||
assert.ErrorIs(t, err, ErrInvalidName)
|
||||
})
|
||||
|
||||
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
||||
cp, err := ParseComposerFile(strings.NewReader(`{"name": "gitea/composer-package", "version": "1.a.3"}`))
|
||||
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.Nil(t, cp)
|
||||
assert.ErrorIs(t, err, ErrInvalidVersion)
|
||||
})
|
||||
|
||||
t.Run("InvalidReadmePath", func(t *testing.T) {
|
||||
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cp)
|
||||
|
||||
assert.Empty(t, cp.Metadata.Readme)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
cp, err := ParseComposerFile(strings.NewReader(composerContent))
|
||||
data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme})
|
||||
|
||||
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cp)
|
||||
|
||||
assert.Equal(t, name, cp.Name)
|
||||
assert.Empty(t, cp.Version)
|
||||
assert.Equal(t, description, cp.Metadata.Description)
|
||||
assert.Equal(t, readme, cp.Metadata.Readme)
|
||||
assert.Len(t, cp.Metadata.Comments, 1)
|
||||
assert.Equal(t, comments, cp.Metadata.Comments[0])
|
||||
assert.Len(t, cp.Metadata.Authors, 1)
|
||||
assert.Equal(t, author, cp.Metadata.Authors[0].Name)
|
||||
assert.Equal(t, email, cp.Metadata.Authors[0].Email)
|
||||
|
|
|
@ -185,8 +185,6 @@ func ParseDescription(r io.Reader) (*Package, error) {
|
|||
}
|
||||
|
||||
func setField(p *Package, data string) error {
|
||||
const listDelimiter = ", "
|
||||
|
||||
if data == "" {
|
||||
return nil
|
||||
}
|
||||
|
@ -215,19 +213,19 @@ func setField(p *Package, data string) error {
|
|||
case "Description":
|
||||
p.Metadata.Description = value
|
||||
case "URL":
|
||||
p.Metadata.ProjectURL = splitAndTrim(value, listDelimiter)
|
||||
p.Metadata.ProjectURL = splitAndTrim(value)
|
||||
case "License":
|
||||
p.Metadata.License = value
|
||||
case "Author":
|
||||
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""), listDelimiter)
|
||||
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
|
||||
case "Depends":
|
||||
p.Metadata.Depends = splitAndTrim(value, listDelimiter)
|
||||
p.Metadata.Depends = splitAndTrim(value)
|
||||
case "Imports":
|
||||
p.Metadata.Imports = splitAndTrim(value, listDelimiter)
|
||||
p.Metadata.Imports = splitAndTrim(value)
|
||||
case "Suggests":
|
||||
p.Metadata.Suggests = splitAndTrim(value, listDelimiter)
|
||||
p.Metadata.Suggests = splitAndTrim(value)
|
||||
case "LinkingTo":
|
||||
p.Metadata.LinkingTo = splitAndTrim(value, listDelimiter)
|
||||
p.Metadata.LinkingTo = splitAndTrim(value)
|
||||
case "NeedsCompilation":
|
||||
p.Metadata.NeedsCompilation = value == "yes"
|
||||
}
|
||||
|
@ -235,8 +233,8 @@ func setField(p *Package, data string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func splitAndTrim(s, sep string) []string {
|
||||
items := strings.Split(s, sep)
|
||||
func splitAndTrim(s string) []string {
|
||||
items := strings.Split(s, ", ")
|
||||
for i := range items {
|
||||
items[i] = strings.TrimSpace(items[i])
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/mdstripper"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -341,7 +340,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool
|
|||
return false, nil
|
||||
}
|
||||
}
|
||||
r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly)
|
||||
r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
|
||||
if r == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
|||
if err != nil {
|
||||
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||
}
|
||||
repo.ObjectFormatName = objFmt.Name() // keep consistent with db
|
||||
|
||||
allBranches := container.Set[string]{}
|
||||
{
|
||||
|
|
|
@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
|
|||
|
||||
// decodeEnvironmentKey decode the environment key to section and key
|
||||
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
|
||||
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
|
||||
func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { //nolint:unparam
|
||||
if !strings.HasPrefix(envKey, prefixGitea) {
|
||||
return false, "", "", false
|
||||
}
|
||||
|
|
18
modules/setting/global.go
Normal file
18
modules/setting/global.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
// Global settings
|
||||
var (
|
||||
// RunUser is the OS user that Gitea is running as. ini:"RUN_USER"
|
||||
RunUser string
|
||||
// RunMode is the running mode of Gitea, it only accepts two values: "dev" and "prod".
|
||||
// Non-dev values will be replaced by "prod". ini: "RUN_MODE"
|
||||
RunMode string
|
||||
// IsProd is true if RunMode is not "dev"
|
||||
IsProd bool
|
||||
|
||||
// AppName is the Application name, used in the page title. ini: "APP_NAME"
|
||||
AppName string
|
||||
)
|
|
@ -18,14 +18,15 @@ import (
|
|||
// Mailer represents mail service.
|
||||
type Mailer struct {
|
||||
// Mailer
|
||||
Name string `ini:"NAME"`
|
||||
From string `ini:"FROM"`
|
||||
EnvelopeFrom string `ini:"ENVELOPE_FROM"`
|
||||
OverrideEnvelopeFrom bool `ini:"-"`
|
||||
FromName string `ini:"-"`
|
||||
FromEmail string `ini:"-"`
|
||||
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
|
||||
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
|
||||
Name string `ini:"NAME"`
|
||||
From string `ini:"FROM"`
|
||||
EnvelopeFrom string `ini:"ENVELOPE_FROM"`
|
||||
OverrideEnvelopeFrom bool `ini:"-"`
|
||||
FromName string `ini:"-"`
|
||||
FromEmail string `ini:"-"`
|
||||
SendAsPlainText bool `ini:"SEND_AS_PLAIN_TEXT"`
|
||||
SubjectPrefix string `ini:"SUBJECT_PREFIX"`
|
||||
OverrideHeader map[string][]string `ini:"-"`
|
||||
|
||||
// SMTP sender
|
||||
Protocol string `ini:"PROTOCOL"`
|
||||
|
@ -151,6 +152,12 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
|||
log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
|
||||
}
|
||||
|
||||
overrideHeader := rootCfg.Section("mailer.override_header").Keys()
|
||||
MailService.OverrideHeader = make(map[string][]string)
|
||||
for _, key := range overrideHeader {
|
||||
MailService.OverrideHeader[key.Name()] = key.Strings(",")
|
||||
}
|
||||
|
||||
// Infer SMTPPort if not set
|
||||
if MailService.SMTPPort == "" {
|
||||
switch MailService.Protocol {
|
||||
|
|
|
@ -6,7 +6,6 @@ package setting
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
@ -19,7 +18,6 @@ var (
|
|||
Storage *Storage
|
||||
Enabled bool
|
||||
ChunkedUploadPath string
|
||||
RegistryHost string
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
|
@ -66,9 +64,6 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
appURL, _ := url.Parse(AppURL)
|
||||
Packages.RegistryHost = appURL.Host
|
||||
|
||||
Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
|
||||
if !filepath.IsAbs(Packages.ChunkedUploadPath) {
|
||||
Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))
|
||||
|
|
|
@ -40,16 +40,16 @@ const (
|
|||
LandingPageLogin LandingPage = "/user/login"
|
||||
)
|
||||
|
||||
// Server settings
|
||||
var (
|
||||
// AppName is the Application name, used in the page title.
|
||||
// It maps to ini:"APP_NAME"
|
||||
AppName string
|
||||
// AppURL is the Application ROOT_URL. It always has a '/' suffix
|
||||
// It maps to ini:"ROOT_URL"
|
||||
AppURL string
|
||||
// AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'.
|
||||
// This value is empty if site does not have sub-url.
|
||||
AppSubURL string
|
||||
// UseSubURLPath makes Gitea handle requests with sub-path like "/sub-path/owner/repo/...", to make it easier to debug sub-path related problems without a reverse proxy.
|
||||
UseSubURLPath bool
|
||||
// AppDataPath is the default path for storing data.
|
||||
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
|
||||
AppDataPath string
|
||||
|
@ -59,8 +59,6 @@ var (
|
|||
// AssetVersion holds a opaque value that is used for cache-busting assets
|
||||
AssetVersion string
|
||||
|
||||
// Server settings
|
||||
|
||||
Protocol Scheme
|
||||
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
|
||||
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
|
||||
|
@ -275,9 +273,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||
// This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
|
||||
AppURL = strings.TrimRight(appURL.String(), "/") + "/"
|
||||
|
||||
// Suburl should start with '/' and end without '/', such as '/{subpath}'.
|
||||
// AppSubURL should start with '/' and end without '/', such as '/{subpath}'.
|
||||
// This value is empty if site does not have sub-url.
|
||||
AppSubURL = strings.TrimSuffix(appURL.Path, "/")
|
||||
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
|
||||
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
|
||||
|
||||
// Check if Domain differs from AppURL domain than update it to AppURL's domain
|
||||
|
|
|
@ -25,12 +25,7 @@ var (
|
|||
// AppStartTime store time gitea has started
|
||||
AppStartTime time.Time
|
||||
|
||||
// Other global setting objects
|
||||
|
||||
CfgProvider ConfigProvider
|
||||
RunMode string
|
||||
RunUser string
|
||||
IsProd bool
|
||||
IsWindows bool
|
||||
|
||||
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing
|
||||
|
|
|
@ -161,7 +161,7 @@ const (
|
|||
targetSecIsSec // target section is from the name seciont [name]
|
||||
)
|
||||
|
||||
func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) {
|
||||
func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam
|
||||
targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
|
||||
if err != nil {
|
||||
if !IsValidStorageType(StorageType(typ)) {
|
||||
|
|
|
@ -52,6 +52,7 @@ var UI = struct {
|
|||
|
||||
CSV struct {
|
||||
MaxFileSize int64
|
||||
MaxRows int
|
||||
} `ini:"ui.csv"`
|
||||
|
||||
Admin struct {
|
||||
|
@ -107,8 +108,10 @@ var UI = struct {
|
|||
},
|
||||
CSV: struct {
|
||||
MaxFileSize int64
|
||||
MaxRows int
|
||||
}{
|
||||
MaxFileSize: 524288,
|
||||
MaxRows: 2500,
|
||||
},
|
||||
Admin: struct {
|
||||
UserPagingNum int
|
||||
|
|
|
@ -163,10 +163,7 @@ func (a *AzureBlobStorage) getObjectNameFromPath(path string) string {
|
|||
|
||||
// Open opens a file
|
||||
func (a *AzureBlobStorage) Open(path string) (Object, error) {
|
||||
blobClient, err := a.getBlobClient(path)
|
||||
if err != nil {
|
||||
return nil, convertAzureBlobErr(err)
|
||||
}
|
||||
blobClient := a.getBlobClient(path)
|
||||
res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
|
||||
if err != nil {
|
||||
return nil, convertAzureBlobErr(err)
|
||||
|
@ -229,10 +226,7 @@ func (a azureBlobFileInfo) Sys() any {
|
|||
|
||||
// Stat returns the stat information of the object
|
||||
func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
|
||||
blobClient, err := a.getBlobClient(path)
|
||||
if err != nil {
|
||||
return nil, convertAzureBlobErr(err)
|
||||
}
|
||||
blobClient := a.getBlobClient(path)
|
||||
res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
|
||||
if err != nil {
|
||||
return nil, convertAzureBlobErr(err)
|
||||
|
@ -247,20 +241,14 @@ func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
|
|||
|
||||
// Delete delete a file
|
||||
func (a *AzureBlobStorage) Delete(path string) error {
|
||||
blobClient, err := a.getBlobClient(path)
|
||||
if err != nil {
|
||||
return convertAzureBlobErr(err)
|
||||
}
|
||||
_, err = blobClient.Delete(a.ctx, nil)
|
||||
blobClient := a.getBlobClient(path)
|
||||
_, err := blobClient.Delete(a.ctx, nil)
|
||||
return convertAzureBlobErr(err)
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) {
|
||||
blobClient, err := a.getBlobClient(path)
|
||||
if err != nil {
|
||||
return nil, convertAzureBlobErr(err)
|
||||
}
|
||||
blobClient := a.getBlobClient(path)
|
||||
|
||||
startTime := time.Now()
|
||||
u, err := blobClient.GetSASURL(sas.BlobPermissions{
|
||||
|
@ -290,10 +278,7 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
|
|||
return convertAzureBlobErr(err)
|
||||
}
|
||||
for _, object := range resp.Segment.BlobItems {
|
||||
blobClient, err := a.getBlobClient(*object.Name)
|
||||
if err != nil {
|
||||
return convertAzureBlobErr(err)
|
||||
}
|
||||
blobClient := a.getBlobClient(*object.Name)
|
||||
object := &azureBlobObject{
|
||||
Context: a.ctx,
|
||||
blobClient: blobClient,
|
||||
|
@ -313,8 +298,8 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
|
|||
}
|
||||
|
||||
// Delete delete a file
|
||||
func (a *AzureBlobStorage) getBlobClient(path string) (*blob.Client, error) {
|
||||
return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path)), nil
|
||||
func (a *AzureBlobStorage) getBlobClient(path string) *blob.Client {
|
||||
return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path))
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -6,8 +6,11 @@ package structs
|
|||
import "time"
|
||||
|
||||
type Activity struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"` // Receiver user
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"` // Receiver user
|
||||
// the type of action
|
||||
//
|
||||
// enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request
|
||||
OpType string `json:"op_type"`
|
||||
ActUserID int64 `json:"act_user_id"`
|
||||
ActUser *User `json:"act_user"`
|
||||
|
|
|
@ -25,7 +25,8 @@ type MarkupOption struct {
|
|||
//
|
||||
// in: body
|
||||
Mode string
|
||||
// Context to render
|
||||
// URL path for rendering issue, media and file links
|
||||
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
|
||||
//
|
||||
// in: body
|
||||
Context string
|
||||
|
@ -53,7 +54,8 @@ type MarkdownOption struct {
|
|||
//
|
||||
// in: body
|
||||
Mode string
|
||||
// Context to render
|
||||
// URL path for rendering issue, media and file links
|
||||
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
|
||||
//
|
||||
// in: body
|
||||
Context string
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// Tag represents a repository tag
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
|
@ -38,3 +40,29 @@ type CreateTagOption struct {
|
|||
Message string `json:"message"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// TagProtection represents a tag protection
|
||||
type TagProtection struct {
|
||||
ID int64 `json:"id"`
|
||||
NamePattern string `json:"name_pattern"`
|
||||
WhitelistUsernames []string `json:"whitelist_usernames"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateTagProtectionOption options for creating a tag protection
|
||||
type CreateTagProtectionOption struct {
|
||||
NamePattern string `json:"name_pattern"`
|
||||
WhitelistUsernames []string `json:"whitelist_usernames"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
}
|
||||
|
||||
// EditTagProtectionOption options for editing a tag protection
|
||||
type EditTagProtectionOption struct {
|
||||
NamePattern *string `json:"name_pattern"`
|
||||
WhitelistUsernames []string `json:"whitelist_usernames"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// DBStore can be used to store app state items in local filesystem
|
||||
|
@ -24,7 +23,7 @@ func (f *DBStore) Get(ctx context.Context, item StateItem) error {
|
|||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(util.StringToReadOnlyBytes(content), item)
|
||||
return json.Unmarshal(util.UnsafeStringToBytes(content), item)
|
||||
}
|
||||
|
||||
// Set saves the state item
|
||||
|
@ -33,5 +32,5 @@ func (f *DBStore) Set(ctx context.Context, item StateItem) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return system.SaveAppStateContent(ctx, item.Name(), util.BytesToReadOnlyString(b))
|
||||
return system.SaveAppStateContent(ctx, item.Name(), util.UnsafeBytesToString(b))
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -237,8 +238,8 @@ func DotEscape(raw string) string {
|
|||
|
||||
// Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
|
||||
// and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
|
||||
func Iif(condition bool, vals ...any) any {
|
||||
if condition {
|
||||
func Iif(condition any, vals ...any) any {
|
||||
if isTemplateTruthy(condition) {
|
||||
return vals[0]
|
||||
} else if len(vals) > 1 {
|
||||
return vals[1]
|
||||
|
@ -246,6 +247,32 @@ func Iif(condition bool, vals ...any) any {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isTemplateTruthy(v any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return rv.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return rv.Uint() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return rv.Float() != 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return rv.Complex() != 0
|
||||
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
|
||||
return rv.Len() > 0
|
||||
case reflect.Struct:
|
||||
return true
|
||||
default:
|
||||
return !rv.IsNil()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval the expression and return the result, see the comment of eval.Expr for details.
|
||||
// To use this helper function in templates, pass each token as a separate parameter.
|
||||
//
|
||||
|
|
|
@ -5,8 +5,11 @@ package templates
|
|||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -65,3 +68,41 @@ func TestHTMLFormat(t *testing.T) {
|
|||
func TestSanitizeHTML(t *testing.T) {
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
|
||||
}
|
||||
|
||||
func TestTemplateTruthy(t *testing.T) {
|
||||
tmpl := template.New("test")
|
||||
tmpl.Funcs(template.FuncMap{"Iif": Iif})
|
||||
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
|
||||
|
||||
cases := []any{
|
||||
nil, false, true, "", "string", 0, 1,
|
||||
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
|
||||
complex(0, 0), complex(1, 0),
|
||||
(chan int)(nil), make(chan int),
|
||||
(func())(nil), func() {},
|
||||
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
|
||||
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
|
||||
[0]int{},
|
||||
[1]int{0},
|
||||
[]int(nil),
|
||||
[]int{},
|
||||
[]int{0},
|
||||
map[any]any(nil),
|
||||
map[any]any{},
|
||||
map[any]any{"k": "v"},
|
||||
(*struct{})(nil),
|
||||
struct{}{},
|
||||
util.ToPointer(struct{}{}),
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
truthyCount := 0
|
||||
for i, v := range cases {
|
||||
w.Reset()
|
||||
assert.NoError(t, tmpl.Execute(w, struct{ Value any }{v}), "case %d (%T) %#v fails", i, v, v)
|
||||
out := w.String()
|
||||
truthyCount += util.Iif(out == "true:true", 1, 0)
|
||||
truthyMatches := out == "true:true" || out == "false:false"
|
||||
assert.True(t, truthyMatches, "case %d (%T) %#v fail: %s", i, v, v, out)
|
||||
}
|
||||
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
|
||||
}
|
||||
|
|
|
@ -34,8 +34,10 @@ func IsNormalPageCompleted(s string) bool {
|
|||
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
|
||||
}
|
||||
|
||||
func MockVariableValue[T any](p *T, v T) (reset func()) {
|
||||
func MockVariableValue[T any](p *T, v ...T) (reset func()) {
|
||||
old := *p
|
||||
*p = v
|
||||
if len(v) > 0 {
|
||||
*p = v[0]
|
||||
}
|
||||
return func() { *p = old }
|
||||
}
|
||||
|
|
|
@ -15,10 +15,7 @@ import (
|
|||
// GenerateKeyPair generates a public and private keypair
|
||||
func GenerateKeyPair(bits int) (string, string, error) {
|
||||
priv, _ := rsa.GenerateKey(rand.Reader, bits)
|
||||
privPem, err := pemBlockForPriv(priv)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
privPem := pemBlockForPriv(priv)
|
||||
pubPem, err := pemBlockForPub(&priv.PublicKey)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
@ -26,12 +23,12 @@ func GenerateKeyPair(bits int) (string, string, error) {
|
|||
return privPem, pubPem, nil
|
||||
}
|
||||
|
||||
func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) {
|
||||
func pemBlockForPriv(priv *rsa.PrivateKey) string {
|
||||
privBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
})
|
||||
return string(privBytes), nil
|
||||
return string(privBytes)
|
||||
}
|
||||
|
||||
func pemBlockForPub(pub *rsa.PublicKey) (string, error) {
|
||||
|
|
|
@ -6,8 +6,6 @@ package util
|
|||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type sanitizedError struct {
|
||||
|
@ -33,7 +31,7 @@ var schemeSep = []byte("://")
|
|||
|
||||
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
|
||||
func SanitizeCredentialURLs(s string) string {
|
||||
bs := util.StringToReadOnlyBytes(s)
|
||||
bs := UnsafeStringToBytes(s)
|
||||
schemeSepPos := bytes.Index(bs, schemeSep)
|
||||
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
|
||||
return s // fast return if there is no URL scheme or no userinfo
|
||||
|
@ -70,5 +68,5 @@ func SanitizeCredentialURLs(s string) string {
|
|||
schemeSepPos = bytes.Index(bs, schemeSep)
|
||||
}
|
||||
out = append(out, bs...)
|
||||
return util.BytesToReadOnlyString(out)
|
||||
return UnsafeBytesToString(out)
|
||||
}
|
||||
|
|
|
@ -87,11 +87,11 @@ func ToSnakeCase(input string) string {
|
|||
}
|
||||
|
||||
// UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a string.
|
||||
// TODO: replace all "goldmark/util.BytesToReadOnlyString" with this official approach
|
||||
func UnsafeBytesToString(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
||||
|
||||
// UnsafeStringToBytes uses Go's unsafe package to convert a string to a byte slice.
|
||||
func UnsafeStringToBytes(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ func GetSiteCookie(req *http.Request, name string) string {
|
|||
|
||||
// SetSiteCookie returns given cookie value from request header.
|
||||
func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
|
||||
// Previous versions would use a cookie path with a trailing /.
|
||||
// These are more specific than cookies without a trailing /, so
|
||||
// we need to delete these if they exist.
|
||||
deleteLegacySiteCookie(resp, name)
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: url.QueryEscape(value),
|
||||
|
@ -46,10 +50,6 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
|
|||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
resp.Header().Add("Set-Cookie", cookie.String())
|
||||
// Previous versions would use a cookie path with a trailing /.
|
||||
// These are more specific than cookies without a trailing /, so
|
||||
// we need to delete these if they exist.
|
||||
deleteLegacySiteCookie(resp, name)
|
||||
}
|
||||
|
||||
// deleteLegacySiteCookie deletes the cookie with the given name at the cookie
|
||||
|
|
44
options/gitignore/Alteryx
Normal file
44
options/gitignore/Alteryx
Normal file
|
@ -0,0 +1,44 @@
|
|||
# gitignore template for Alteryx Designer
|
||||
# website: https://www.alteryx.com/
|
||||
# website: https://help.alteryx.com/current/designer/alteryx-file-types
|
||||
|
||||
# Alteryx Data Files
|
||||
*.yxdb
|
||||
*.cydb
|
||||
*.cyidx
|
||||
*.rptx
|
||||
*.vvf
|
||||
*.aws
|
||||
|
||||
# Alteryx Special Files
|
||||
*.yxwv
|
||||
*.yxft
|
||||
*.yxbe
|
||||
*.bak
|
||||
*.pcxml
|
||||
*.log
|
||||
*.bin
|
||||
*.yxlang
|
||||
CASS.ini
|
||||
|
||||
# Alteryx License Files
|
||||
*.yxlc
|
||||
*.slc
|
||||
*.cylc
|
||||
*.alc
|
||||
*.gzlc
|
||||
|
||||
## gitignore reference sites
|
||||
# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files
|
||||
# https://git-scm.com/docs/gitignore
|
||||
# https://help.github.com/articles/ignoring-files/
|
||||
|
||||
## Useful knowledge from stackoverflow
|
||||
# Even if you haven't tracked the files so far, git seems to be able to "know" about them even after you add them to .gitignore.
|
||||
# WARNING: First commit your current changes, or you will lose them.
|
||||
# Then run the following commands from the top folder of your git repo:
|
||||
# git rm -r --cached .
|
||||
# git add .
|
||||
# git commit -m "fixed untracked files"
|
||||
|
||||
# author: Kacper Ksieski
|
|
@ -14,6 +14,8 @@
|
|||
*.lzma
|
||||
*.cab
|
||||
*.xar
|
||||
*.zst
|
||||
*.tzst
|
||||
|
||||
# Packing-only formats
|
||||
*.iso
|
||||
|
|
11
options/gitignore/Ballerina
Normal file
11
options/gitignore/Ballerina
Normal file
|
@ -0,0 +1,11 @@
|
|||
# generated files
|
||||
target/
|
||||
generated/
|
||||
|
||||
# dependencies
|
||||
Dependencies.toml
|
||||
|
||||
# config files
|
||||
Config.toml
|
||||
# the config files used for testing, Uncomment the following line if you want to commit the test config files
|
||||
#!**/tests/Config.toml
|
|
@ -9,3 +9,4 @@ install_manifest.txt
|
|||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
|
|
|
@ -26,6 +26,18 @@
|
|||
#*.obj
|
||||
#
|
||||
|
||||
# Default Delphi compiler directories
|
||||
# Content of this directories are generated with each Compile/Construct of a project.
|
||||
# Most of the time, files here have not there place in a code repository.
|
||||
#Win32/
|
||||
#Win64/
|
||||
#OSX64/
|
||||
#OSXARM64/
|
||||
#Android/
|
||||
#Android64/
|
||||
#iOSDevice64/
|
||||
#Linux64/
|
||||
|
||||
# Delphi compiler-generated binaries (safe to delete)
|
||||
*.exe
|
||||
*.dll
|
||||
|
|
18
options/gitignore/GitHubPages
Normal file
18
options/gitignore/GitHubPages
Normal file
|
@ -0,0 +1,18 @@
|
|||
# This .gitignore is appropriate for repositories deployed to GitHub Pages and using
|
||||
# a Gemfile as specified at https://github.com/github/pages-gem#conventional
|
||||
|
||||
# Basic Jekyll gitignores (synchronize to Jekyll.gitignore)
|
||||
_site/
|
||||
.sass-cache/
|
||||
.jekyll-cache/
|
||||
.jekyll-metadata
|
||||
|
||||
# Additional Ruby/bundler ignore for when you run: bundle install
|
||||
/vendor
|
||||
|
||||
# Specific ignore for GitHub Pages
|
||||
# GitHub Pages will always use its own deployed version of pages-gem
|
||||
# This means GitHub Pages will NOT use your Gemfile.lock and therefore it is
|
||||
# counterproductive to check this file into the repository.
|
||||
# Details at https://github.com/github/pages-gem/issues/768
|
||||
Gemfile.lock
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user