1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-06-29 01:45:30 +00:00

Compare commits

...

63 Commits

Author SHA1 Message Date
silverwind
5d14bfa3b4
remove unnecessary exclude 2024-06-17 21:40:38 +02:00
silverwind
22ee0ab5ee
update @typescript-eslint/parser 2024-06-17 21:34:00 +02:00
silverwind
0d40c0b963
Merge branch 'main' into tsconfig 2024-06-17 21:32:15 +02:00
6543
363c123598
Add cache test for admins (#31265)
Add a test to probe the cache similar to the email test func.


![image](https://github.com/go-gitea/gitea/assets/24977596/700e2733-586d-4091-900f-f5f71e6e94bf)


![image](https://github.com/go-gitea/gitea/assets/24977596/2a953802-18fc-4e81-a37d-24ebe1297365)


![image](https://github.com/go-gitea/gitea/assets/24977596/e00d62ad-bb60-41cc-9138-09993daee156)

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: silverwind <me@silverwind.io>
2024-06-17 21:22:39 +02:00
silverwind
4b6eb46e69
Fix double border in system status table (#31363)
Fix regression from https://github.com/go-gitea/gitea/pull/30712 where
the introduction of this `<div>` caused the `.ui.attached:not(.message)
+ .ui.attached.segment:not(.top)` CSS selector to no longer work and
cause a double border.

Before:

<img width="200" alt="Screenshot 2024-06-13 at 19 06 12"
src="https://github.com/go-gitea/gitea/assets/115237/a9fa0688-adf0-4b2d-a958-6a7679a62031">

After:
<img width="232" alt="Screenshot 2024-06-13 at 19 05 57"
src="https://github.com/go-gitea/gitea/assets/115237/025b780f-f72f-4049-86de-a5d84851bd1d">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-06-17 15:21:59 +02:00
wxiaoguang
0f09c22663
Improve rubygems package registry (#31357)
To make it work with Bundler:
https://guides.rubygems.org/rubygems-org-compact-index-api/

It only adds 2 new API endpoints and improves some tests, existing logic
is not changed.
2024-06-17 08:42:46 +00:00
wxiaoguang
25f3ec5b65
Fix natural sort (#31384)
Fix #31374
2024-06-17 06:45:12 +00:00
Brecht Van Lommel
597d1da96b
Fix missing images in editor preview due to wrong links (#31299)
Parse base path and tree path so that media links can be correctly
created with /media/.

Resolves #31294

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-06-17 14:16:14 +08:00
wxiaoguang
f5dfd7d73c
Add a simple test for AdoptRepository (#31391)
Follow #31333
2024-06-17 01:18:35 +00:00
GiteaBot
129206da45 [skip ci] Updated licenses and gitignores 2024-06-17 00:27:39 +00:00
wxiaoguang
f446e3b4ab
Fix JS error when creating new issue (#31383)
Fix #31336
2024-06-16 02:07:21 +00:00
GiteaBot
c70d4f03c9 [skip ci] Updated translations via Crowdin 2024-06-16 00:28:50 +00:00
6543
78e8296e11
Rename repo_model.SearchOrderByMap to repo_model.OrderByMap (#31359)
https://github.com/go-gitea/gitea/pull/30876#discussion_r1637112394
2024-06-15 06:45:02 +00:00
6543
e37ecd1732
rm const do inline (#31360)
https://github.com/go-gitea/gitea/pull/30876/files#r1637288202
2024-06-15 04:48:52 +00:00
Zettat123
42718d32af
Allow downloading attachments of draft releases (#31369)
Fix #31362
2024-06-15 12:20:14 +08:00
wxiaoguang
84cbb6c4d2
Fix duplicate sub-path for avatars (#31365)
Fix #31361, and add tests

And this PR introduces an undocumented & debug-purpose-only config
option: `USE_SUB_URL_PATH`. It does nothing for end users, it only helps
the development of sub-path related problems.

And also fix #31366

Co-authored-by: @ExplodingDragon
2024-06-15 11:43:57 +08:00
GiteaBot
23147494a7 [skip ci] Updated translations via Crowdin 2024-06-15 00:26:00 +00:00
mzroot
d4e4226c3c
Add tag protection via rest api #17862 (#31295)
Add tag protection manage via rest API.

---------

Co-authored-by: Alexander Kogay <kogay.a@citilink.ru>
Co-authored-by: Giteabot <teabot@gitea.io>
2024-06-14 18:56:10 +02:00
KN4CK3R
4e7b067a7f
Extract and display readme and comments for Composer packages (#30927)
Related #30075

CC @thojo0

Example with rendered readme:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/3516fef5-2631-40fd-8841-5d9894ec8904)
2024-06-14 04:45:52 +00:00
Lunny Xiao
fa82a8af12
Have new announcement about docs contributions (#31364)
According to the maintainers' discussion and voting. We decide to move
docs to https://gitea.com/gitea/docs . Add some hints on this repository
to not make contributors confusing.
2024-06-14 11:17:05 +08:00
Lunny Xiao
e4abaff7ff
Fix bug filtering issues which have no project (#31337)
Fix #31327
This is a quick patch to fix the bug.
Some parameters are using 0, some are using -1. I think it needs a
refactor to keep consistent. But that will be another PR.
2024-06-14 02:31:07 +00:00
Oleksandr Redko
1761459ebc
Refactor to use UnsafeStringToBytes (#31358)
The PR replaces all `goldmark/util.BytesToReadOnlyString` with
`util.UnsafeBytesToString`, `goldmark/util.StringToReadOnlyBytes` with
`util.UnsafeStringToBytes`. This removes one `TODO`.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-06-14 01:26:33 +00:00
GiteaBot
5b56d13e0d [skip ci] Updated translations via Crowdin 2024-06-14 00:27:38 +00:00
yp05327
e61e9a36b7
Fix PullRequestList.GetIssueIDs's logic (#31352)
fix a bug from #30490

`prs.GetIssueIDs()` will also be used in other places, e.g.
`InvalidateCodeComments`
so we should not add `if pr.Issue == nil` in it, or if `pr.Issue` is
already loaded, you will not get the issueID in the results list and
this is not an expected result.

So this will caused a bug:
before calling `InvalidateCodeComments`, all `pr.Issues` in `prs` are
loaded, so `issueIDs` in this function will always be `[]`.

![image](https://github.com/go-gitea/gitea/assets/18380374/ef94d9d2-0bf9-455a-abd6-4d5e6497db7c)
2024-06-13 09:42:07 +00:00
6543
bb04311b0b
[Refactor] Unify repo search order by logic (#30876)
have repo OrderBy definitions defined in one place and use a single type
for OrderBy database options
2024-06-13 09:13:11 +00:00
Kerwin Bryant
fede3cbada
Fixed incorrect localization explorer.go (#31348)
see: https://github.com/go-gitea/gitea/pull/29701/files#r1637325139
2024-06-13 01:43:41 +00:00
wxiaoguang
47ca61d8ba
Improve detecting empty files (#31332)
Co-authored-by: silverwind <me@silverwind.io>
2024-06-13 01:06:46 +00:00
Lunny Xiao
7115dce773
Fix hash render end with colon (#31319)
Fix a hash render problem like `<hash>: xxxxx` which is usually used in
release notes.
2024-06-12 22:35:46 +00:00
silverwind
90bcdf9829
Fix line number widths (#31341)
Fixes regression
https://github.com/go-gitea/gitea/pull/31307#issuecomment-2162554913

Table CSS is weird. A `auto` value does not work and causes the
regression while any pixel value causes another regression in diff where
the code lines do not stretch. Partially revert that PR and clean up
some related too-deep CSS selectors.

<img width="109" alt="Screenshot 2024-06-12 at 15 07 22"
src="https://github.com/go-gitea/gitea/assets/115237/756c5dea-44b8-49f9-8a08-acef68075f62">
<img width="119" alt="Screenshot 2024-06-12 at 15 07 43"
src="https://github.com/go-gitea/gitea/assets/115237/28ae1adc-118e-4016-8d09-033b9f1c9a6f">
<img width="151" alt="Screenshot 2024-06-12 at 15 07 07"
src="https://github.com/go-gitea/gitea/assets/115237/08db7ed9-de4e-405e-874d-c7ebe3082557">
<img width="141" alt="Screenshot 2024-06-12 at 15 07 14"
src="https://github.com/go-gitea/gitea/assets/115237/c4a5492b-1bf1-4773-bc8d-64eb36d823f9">
2024-06-12 15:23:42 +00:00
silverwind
21ba5ca03b
Fix navbar + menu flashing on page load (#31281)
Fixes
https://github.com/go-gitea/gitea/pull/31273#issuecomment-2153771331.
Same method as used in https://github.com/go-gitea/gitea/pull/30215. All
left-opening dropdowns need to use it method.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2024-06-12 14:58:03 +00:00
Rowan Bohde
45dbeb5600
Reduce memory usage for chunked artifact uploads to MinIO (#31325)
When using the MinIO storage driver for Actions Artifacts, we found that
the chunked artifact required significantly more memory usage to both
upload and merge than the local storage driver. This seems to be related
to hardcoding a value of `-1` for the size to the MinIO client [which
has a warning about memory usage in the respective
docs](https://pkg.go.dev/github.com/minio/minio-go/v7#Client.PutObject).
Specifying the size in both the upload and merge case reduces memory
usage of the MinIO client.

Co-authored-by: Kyle D <kdumontnu@gmail.com>
2024-06-12 11:34:35 +00:00
Yarden Shoham
130ea31d6d
Fix dates displaying in a wrong manner when we're close to the end of the month (#31331)
I tested and all timestamps work as before.

- Reference https://github.com/github/relative-time-element/pull/285
- Fixes https://github.com/go-gitea/gitea/issues/31197

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2024-06-12 10:27:00 +00:00
Lunny Xiao
1968c2222d
Fix adopt repository has empty object name in database (#31333)
Fix #31330
Fix #31311

A workaround to fix the old database is to update object_format_name to
`sha1` if it's empty or null.
2024-06-12 18:22:01 +08:00
Kerwin Bryant
a975ce8d9d
Optimize profile layout to enhance visual experience (#31278)
Co-authored-by: silverwind <me@silverwind.io>
2024-06-12 04:06:12 +00:00
Zoupers Zou
e25d6960b5
Fix #31185 try fix lfs download from bitbucket failed (#31201)
Fix #31185
2024-06-11 22:22:28 +00:00
silverwind
fc2d75f86d
Enable unparam linter (#31277)
Enable [unparam](https://github.com/mvdan/unparam) linter.

Often I could not tell the intention why param is unused, so I put
`//nolint` for those cases like webhook request creation functions never
using `ctx`.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
2024-06-11 18:47:45 +00:00
wxiaoguang
4bf848a06b
Make template Iif exactly match if (#31322) 2024-06-11 14:52:12 +00:00
6543
61c97fdef1
update nix flake and add gofumpt (#31320)
nix flake maintenance
2024-06-11 08:47:13 -05:00
Kerwin Bryant
e6ab6e637f
code optimization (#31315)
Simplifying complex if-else to existing Iif operations
2024-06-11 21:07:10 +08:00
silverwind
397930d8c1
Fix line number width in code preview (#31307)
Line numbers were using some hacky CSS `width: 1%` that did nothing to
the code rendering as far as I can tell but broken the inline preview in
markup when line numbers are greater than 2 digits. Also I removed one
duplicate `font-family` rule (it is set below in the `.lines-num,
.lines-code` selector.
2024-06-11 04:54:39 +00:00
wxiaoguang
5342a61124
Delete legacy cookie before setting new cookie (#31306)
Try to fix #31202
2024-06-11 11:31:23 +08:00
GiteaBot
1844dc6c1d [skip ci] Updated translations via Crowdin 2024-06-11 00:26:13 +00:00
silverwind
507fbf4c3c
Use querySelector over alternative DOM methods (#31280)
As per
https://github.com/go-gitea/gitea/pull/30115#discussion_r1626060164,
prefer `querySelector` by enabling
[`unicorn/prefer-query-selector`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-query-selector.md)
and autofixing all except 10 issues.

According to
[this](https://old.reddit.com/r/learnjavascript/comments/i0f5o8/performance_of_getelementbyid_vs_queryselector/),
querySelector may be faster as well, so it's a win-win.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2024-06-10 22:49:33 +02:00
silverwind
a2304cb163
Remove jQuery .text() (#30506)
Remove and forbid [.text()](https://api.jquery.com/text/). Tested some,
but not all functionality, but I think these are pretty safe
replacements.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-06-10 18:12:31 +08:00
GiteaBot
4f7d6feab7 [skip ci] Updated translations via Crowdin 2024-06-10 00:27:20 +00:00
wxiaoguang
6106a61eff
Remove sub-path from container registry realm (#31293)
Container registry requires that the "/v2" must be in the root, so the
sub-path in AppURL should be removed
2024-06-09 16:29:29 +08:00
wxiaoguang
0188d82e49
Fix some URLs whose sub-path is missing (#31289)
Fix #31285
2024-06-07 15:15:17 +00:00
silverwind
291a00dc57
Fix and clean up ConfirmModal (#31283)
Bug: orange button color was removed in
https://github.com/go-gitea/gitea/pull/30475, replaced with red
Bug: translation text was not html-escaped
Refactor: Replaced as much jQuery as possible, added useful
`createElementFromHTML`
Refactor: Remove colors checks that don't exist on `.link-action`

<img width="381" alt="image"
src="https://github.com/go-gitea/gitea/assets/115237/5900bf6a-8a86-4a86-b368-0559cbfea66e">

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
2024-06-07 13:42:31 +00:00
silverwind
15debbbe4e
Enable poetry non-package mode (#31282)
[Poetry
1.8.0](https://github.com/python-poetry/poetry/releases/tag/1.8.0) added
support for [non-package
mode](https://python-poetry.org/docs/basic-usage/#operating-modes), e.g.
projects that are not python packages themselves like we are. Make use
of that and remove the previous workaround via `--no-root`.
2024-06-07 15:37:33 +02:00
Kerwin Bryant
ab1948d4a3
fixed the dropdown menu for the top New button to expand to the left (#31273)
before: 

![1717660314025](https://github.com/go-gitea/gitea/assets/3371163/17ae7a48-31c5-4c71-b285-f65d9106bf86)

after: 

![1717660674763](https://github.com/go-gitea/gitea/assets/3371163/85f847ac-a044-4695-9004-26e6485288c6)
2024-06-06 23:49:53 +00:00
Kerwin Bryant
8e33746746
Optimize repo-list layout to enhance visual experience (#31272)
before: 

![1717655078227](https://github.com/go-gitea/gitea/assets/3371163/4d564f96-c2f8-46b1-996f-6cc7abb940ef)
***The problem was that the icon and text were not on a horizontal line,
and the horizontal was not centered;***

after: 

![1717655094071](https://github.com/go-gitea/gitea/assets/3371163/b11797f6-05f8-486c-b5fd-df89d0cbdcfd)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
2024-06-07 01:22:03 +02:00
Max Wipfli
da4bbc4247
Allow including Reviewed-on/Reviewed-by lines for custom merge messages (#31211)
This PR introduces the `ReviewedOn` and `ReviewedBy` variables for the
default merge message templates (e.g.,
`.gitea/default_merge_message/MERGE_TEMPLATE.md`).

This allows customizing the default merge messages while retaining these
trailers.

This also moves the associated logic out of `pull.tmpl` into the
relevant Go function.

This is a first contribution towards #11077.

---

For illustration, this allows to recreate the "default default" merge
message with the following template:
```
.gitea/default_merge_message/MERGE_TEMPLATE.md
Merge pull request '${PullRequestTitle}' (${PullRequestReference}) from ${HeadBranch} into ${BaseBranch}

${ReviewedOn}
${ReviewedBy}
```
2024-06-06 08:35:04 +00:00
Henrique Pimentel
f7125ab61a
Add MAX_ROWS option for CSV rendering (#30268)
This solution implements a new config variable MAX_ROWS, which
corresponds to the “Maximum allowed rows to render CSV files. (0 for no
limit)” and rewrites the Render function for CSV files in markup module.
Now the render function only reads the file once, having MAX_FILE_SIZE+1
as a reader limit and MAX_ROWS as a row limit. When the file is larger
than MAX_FILE_SIZE or has more rows than MAX_ROWS, it only renders until
the limit, and displays a user-friendly warning informing that the
rendered data is not complete, in the user's language.

---

Previously, when a CSV file was larger than the limit, the render
function lost its function to render the code. There were also multiple
reads to the file, in order to determine its size and render or
pre-render.

The warning: ![image](https://s3.amazonaws.com/i.snag.gy/vcKh90.jpg)
2024-06-06 16:06:59 +08:00
silverwind
24dace8f76
Update golang.org/x/net (#31260)
Result of `go get -u golang.org/x/net && make tidy`. ~~Fixes
https://pkg.go.dev/vuln/GO-2024-2887.~~
2024-06-06 04:29:42 +00:00
silverwind
6a3c487d07
Add replacement module for mholt/archiver (#31267)
Switch to this fork tag:
https://github.com/anchore/archiver/releases/tag/v3.5.2 which includes
82ca88a2eb.

Ref: https://pkg.go.dev/vuln/GO-2024-2698
Ref: https://github.com/advisories/GHSA-rhh4-rh7c-7r5v

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-06-06 03:37:08 +00:00
wxiaoguang
e728fd741b
Fix Activity Page Contributors dropdown (#31264)
Fix #31261
2024-06-06 10:28:33 +08:00
Kerwin Bryant
06ebae7472
Optimize runner-tags layout to enhance visual experience (#31258)
![image](https://github.com/go-gitea/gitea/assets/3371163/b8199005-94f2-45be-8ca9-4fa1b3f221b2)
2024-06-05 22:39:45 +08:00
Rowan Bohde
8de8972baf
fix: allow actions artifacts storage migration to complete succesfully (#31251)
Change the copy to use `ActionsArtifact.StoragePath` instead of the
`ArtifactPath`. Skip artifacts that are expired, and don't error if the
file to copy does not exist.

---

When trying to migrate actions artifact storage from local to MinIO, we
encountered errors that prevented the process from completing
successfully:

* The migration tries to copy the files using the per-run
`ArtifactPath`, instead of the unique `StoragePath`.
* Artifacts that have been marked expired and had their files deleted
would throw an error
* Artifacts that are pending, but don't have a file uploaded yet will
throw an error.

This PR addresses these cases, and allow the process to complete
successfully.
2024-06-05 12:00:56 +08:00
silverwind
816222243a
Add lint-go-gopls (#30729)
Uses `gopls check <files>` as a linter. Tested locally and brings up 149
errors currently for me. I don't think I want to fix them in this PR,
but I would like at least to get this analysis running on CI.

List of errors:
```
modules/indexer/code/indexer.go:181:11: impossible condition: nil != nil
routers/private/hook_post_receive.go:120:15: tautological condition: nil == nil
services/auth/source/oauth2/providers.go:185:9: tautological condition: nil == nil
services/convert/issue.go:216:11: tautological condition: non-nil != nil
tests/integration/git_test.go:332:9: impossible condition: nil != nil
services/migrations/migrate.go:179:24-43: unused parameter: ctx
services/repository/transfer.go:288:48-69: unused parameter: doer
tests/integration/api_repo_tags_test.go:75:41-61: unused parameter: session
tests/integration/git_test.go:696:64-74: unused parameter: baseBranch
tests/integration/gpg_git_test.go:265:27-39: unused parameter: t
tests/integration/gpg_git_test.go:284:23-29: unused parameter: tmpDir
tests/integration/gpg_git_test.go:284:31-35: unused parameter: name
tests/integration/gpg_git_test.go:284:37-42: unused parameter: email
```
2024-06-05 09:22:38 +08:00
wxiaoguang
bd80225ec3
Make blockquote attention recognize more syntaxes (#31240)
Fix #31214
2024-06-04 15:35:29 +00:00
wxiaoguang
fcc061ae44
Fix admin oauth2 custom URL settings (#31246)
Fix #31244
2024-06-04 23:06:21 +08:00
silverwind
138e946c3d
Replace gt-word-break with tw-break-anywhere (#31183)
`overflow-wrap: anywhere` is a superior alternative to `word-wrap:
break-word` and we were already setting it in the class. I tested a few
cases, all look good.
2024-06-04 13:57:11 +00:00
wxiaoguang
9000811118
Make pasted "img" tag has the same behavior as markdown image (#31235)
Fix #31230

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2024-06-04 12:19:41 +00:00
242 changed files with 3088 additions and 1366 deletions

View File

@ -350,7 +350,7 @@ rules:
jquery/no-sizzle: [2] jquery/no-sizzle: [2]
jquery/no-slide: [2] jquery/no-slide: [2]
jquery/no-submit: [2] jquery/no-submit: [2]
jquery/no-text: [0] jquery/no-text: [2]
jquery/no-toggle: [2] jquery/no-toggle: [2]
jquery/no-trigger: [0] jquery/no-trigger: [0]
jquery/no-trim: [2] jquery/no-trim: [2]
@ -503,7 +503,7 @@ rules:
no-jquery/no-slide: [2] no-jquery/no-slide: [2]
no-jquery/no-sub: [2] no-jquery/no-sub: [2]
no-jquery/no-support: [2] no-jquery/no-support: [2]
no-jquery/no-text: [0] no-jquery/no-text: [2]
no-jquery/no-trigger: [0] no-jquery/no-trigger: [0]
no-jquery/no-trim: [2] no-jquery/no-trim: [2]
no-jquery/no-type: [2] no-jquery/no-type: [2]
@ -824,7 +824,7 @@ rules:
unicorn/prefer-object-has-own: [0] unicorn/prefer-object-has-own: [0]
unicorn/prefer-optional-catch-binding: [2] unicorn/prefer-optional-catch-binding: [2]
unicorn/prefer-prototype-methods: [0] unicorn/prefer-prototype-methods: [0]
unicorn/prefer-query-selector: [0] unicorn/prefer-query-selector: [2]
unicorn/prefer-reflect-apply: [0] unicorn/prefer-reflect-apply: [0]
unicorn/prefer-regexp-test: [2] unicorn/prefer-regexp-test: [2]
unicorn/prefer-set-has: [0] unicorn/prefer-set-has: [0]

View File

@ -1,9 +1,10 @@
<!-- start tips --> <!-- start tips -->
Please check the following: Please check the following:
1. Make sure you are targeting the `main` branch, pull requests on release branches are only allowed for backports. 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 . 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). 3. For documentations contribution, please go to https://gitea.com/gitea/docs
4. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily. 4. Describe what your pull request does and which issue you're targeting (if any).
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 `---`. 5. It is recommended to enable "Allow edits by maintainers", so maintainers can help more easily.
6. Delete all these tips before posting. 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 --> <!-- end tips -->

View File

@ -22,6 +22,7 @@ linters:
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- unparam
- wastedassign - wastedassign
run: run:

View File

@ -358,7 +358,8 @@ $REWRITTEN_PR_SUMMARY
## Documentation ## 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 ## API v1

View File

@ -36,6 +36,7 @@ XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@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_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest DOCKER_TAG ?= latest
@ -213,6 +214,7 @@ help:
@echo " - lint-go lint go files" @echo " - lint-go lint go files"
@echo " - lint-go-fix lint go files and fix issues" @echo " - lint-go-fix lint go files and fix issues"
@echo " - lint-go-vet lint go files with vet" @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 lint js files"
@echo " - lint-js-fix lint js files and fix issues" @echo " - lint-js-fix lint js files and fix issues"
@echo " - lint-css lint css files" @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 lint-frontend-fix: lint-js-fix lint-css-fix
.PHONY: lint-backend .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 .PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
@ -426,6 +428,11 @@ lint-go-vet:
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=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 .PHONY: lint-editorconfig
lint-editorconfig: lint-editorconfig:
@$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES) @$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) $(EDITORCONFIG_FILES)
@ -866,13 +873,14 @@ deps-tools:
$(GO) install $(GO_LICENSES_PACKAGE) $(GO) install $(GO_LICENSES_PACKAGE)
$(GO) install $(GOVULNCHECK_PACKAGE) $(GO) install $(GOVULNCHECK_PACKAGE)
$(GO) install $(ACTIONLINT_PACKAGE) $(GO) install $(ACTIONLINT_PACKAGE)
$(GO) install $(GOPLS_PACKAGE)
node_modules: package-lock.json node_modules: package-lock.json
npm install --no-save npm install --no-save
@touch node_modules @touch node_modules
.venv: poetry.lock .venv: poetry.lock
poetry install --no-root poetry install
@touch .venv @touch .venv
.PHONY: update .PHONY: update
@ -889,7 +897,7 @@ update-js: node-check | node_modules
update-py: node-check | node_modules update-py: node-check | node_modules
npx updates -u -f pyproject.toml npx updates -u -f pyproject.toml
rm -rf .venv poetry.lock rm -rf .venv poetry.lock
poetry install --no-root poetry install
@touch .venv @touch .venv
.PHONY: fomantic .PHONY: fomantic

View File

@ -5,7 +5,9 @@ package cmd
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io/fs"
"strings" "strings"
actions_model "code.gitea.io/gitea/models/actions" 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 { func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) 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) if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
return err 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
}) })
} }

View File

@ -81,6 +81,10 @@ RUN_USER = ; git
;; Overwrite the automatically generated public URL. Necessary for proxies and docker. ;; Overwrite the automatically generated public URL. Necessary for proxies and docker.
;ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ;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 ;; when STATIC_URL_PREFIX is empty it will follow ROOT_URL
;STATIC_URL_PREFIX = ;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). ;; Maximum allowed file size in bytes to render CSV files as table. (Set to 0 for no limit).
;MAX_FILE_SIZE = 524288 ;MAX_FILE_SIZE = 524288
;;
;; Maximum allowed rows to render CSV files. (Set to 0 for no limit)
;MAX_ROWS = 2500
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -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. 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. 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. 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. 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 ### Accessibility / ARIA

View File

@ -47,7 +47,7 @@ HTML 页面由[Go HTML Template](https://pkg.go.dev/html/template)渲染。
9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。 9. 避免在 CSS 中使用不必要的`!important`,如果无法避免,添加注释解释为什么需要它。
10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。 10. 避免在一个事件监听器中混合不同的事件,优先为每个事件使用独立的事件监听器。
11. 推荐使用自定义事件名称前缀`ce-`。 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类。如果内联脚本和样式不可避免请解释无法避免的原因。 13. 尽量避免内联脚本和样式建议将JS代码放入JS文件中并使用CSS类。如果内联脚本和样式不可避免请解释无法避免的原因。
### 可访问性 / ARIA ### 可访问性 / ARIA

View File

@ -44,6 +44,8 @@ You can use the following variables enclosed in `${}` inside these templates whi
- PullRequestIndex: Pull request's index number - PullRequestIndex: Pull request's index number
- PullRequestReference: Pull request's reference char with index number. i.e. #1, !2 - 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` - 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 ## Rebase

View File

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1715534503, "lastModified": 1717974879,
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", "narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06", "rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -30,6 +30,7 @@
# backend # backend
go_1_22 go_1_22
gofumpt
]; ];
}; };
} }

17
go.mod
View File

@ -108,13 +108,13 @@ require (
github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark v1.7.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0 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/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/oauth2 v0.18.0
golang.org/x/sys v0.19.0 golang.org/x/sys v0.21.0
golang.org/x/text v0.14.0 golang.org/x/text v0.16.0
golang.org/x/tools v0.19.0 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/grpc v1.62.1 google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.33.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 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/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.16.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // 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 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 v3.2.0+incompatible
exclude github.com/gofrs/uuid v4.0.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible

35
go.sum
View File

@ -92,6 +92,8 @@ 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/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 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 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.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 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/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 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= 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 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs= 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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 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 h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 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= 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.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 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 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 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= 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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.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.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.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.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.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.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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= 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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -18,12 +18,6 @@ const (
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
SearchOrderByOldest SearchOrderBy = "created_unix ASC" SearchOrderByOldest SearchOrderBy = "created_unix ASC"
SearchOrderByNewest SearchOrderBy = "created_unix DESC" 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" SearchOrderByID SearchOrderBy = "id ASC"
SearchOrderByIDReverse SearchOrderBy = "id DESC" SearchOrderByIDReverse SearchOrderBy = "id DESC"
SearchOrderByStars SearchOrderBy = "num_stars ASC" SearchOrderByStars SearchOrderBy = "num_stars ASC"

View File

@ -215,16 +215,15 @@ func fileTimestampToTime(timestamp int64) time.Time {
return time.UnixMicro(timestamp) return time.UnixMicro(timestamp)
} }
func (f *file) loadMetaByPath() (*dbfsMeta, error) { func (f *file) loadMetaByPath() error {
var fileMeta dbfsMeta var fileMeta dbfsMeta
if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil { if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil {
return nil, err return err
} else if ok { } else if ok {
f.metaID = fileMeta.ID f.metaID = fileMeta.ID
f.blockSize = fileMeta.BlockSize f.blockSize = fileMeta.BlockSize
return &fileMeta, nil
} }
return nil, nil return nil
} }
func (f *file) open(flag int) (err error) { func (f *file) open(flag int) (err error) {
@ -288,10 +287,7 @@ func (f *file) createEmpty() error {
if err != nil { if err != nil {
return err return err
} }
if _, err = f.loadMetaByPath(); err != nil { return f.loadMetaByPath()
return err
}
return nil
} }
func (f *file) truncate() error { func (f *file) truncate() error {
@ -368,8 +364,5 @@ func buildPath(path string) string {
func newDbFile(ctx context.Context, path string) (*file, error) { func newDbFile(ctx context.Context, path string) (*file, error) {
path = buildPath(path) path = buildPath(path)
f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize} f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize}
if _, err := f.loadMetaByPath(); err != nil { return f, f.loadMetaByPath()
return nil, err
}
return f, nil
} }

View File

@ -110,6 +110,19 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
return tag, nil 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. // 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. // 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) { func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) {

View File

@ -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() { if opts.Paginator == nil || opts.Paginator.IsListAll() {
return sess return
} }
start := 0 start := 0
@ -109,11 +109,9 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
} }
sess.Limit(opts.Paginator.PageSize, start) 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 len(opts.LabelIDs) > 0 {
if opts.LabelIDs[0] == 0 { if opts.LabelIDs[0] == 0 {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") 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 { if len(opts.ExcludedLabelNames) > 0 {
sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) 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 { if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID {
sess.And("issue.milestone_id = 0") sess.And("issue.milestone_id = 0")
} else if len(opts.MilestoneIDs) > 0 { } else if len(opts.MilestoneIDs) > 0 {
@ -153,11 +149,9 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
From("milestone"). From("milestone").
Where(builder.In("name", opts.IncludeMilestones))) 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 if opts.ProjectID > 0 { // specific project
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
And("project_issue.project_id=?", opts.ProjectID) 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, // opts.ProjectID == 0 means all projects,
// do not need to apply any condition // 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, // opts.ProjectColumnID == 0 means all project columns,
// do not need to apply any condition // do not need to apply any condition
if opts.ProjectColumnID > 0 { if opts.ProjectColumnID > 0 {
@ -177,10 +170,9 @@ func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.
} else if opts.ProjectColumnID == db.NoConditionID { } else if opts.ProjectColumnID == db.NoConditionID {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) 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 { if len(opts.RepoIDs) == 1 {
opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]} opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
} else if len(opts.RepoIDs) > 1 { } else if len(opts.RepoIDs) > 1 {
@ -195,10 +187,9 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
if opts.RepoCond != nil { if opts.RepoCond != nil {
sess.And(opts.RepoCond) 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 { if len(opts.IssueIDs) > 0 {
sess.In("issue.id", opts.IssueIDs) sess.In("issue.id", opts.IssueIDs)
} }
@ -261,8 +252,6 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
if opts.User != nil { if opts.User != nil {
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) 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 // 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 return cond
} }
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", assigneeID) And("issue_assignees.assignee_id = ?", assigneeID)
} }
func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { func applyPosterCondition(sess *xorm.Session, posterID int64) {
return sess.And("issue.poster_id=?", posterID) sess.And("issue.poster_id=?", posterID)
} }
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
And("issue_user.is_mentioned = ?", true). And("issue_user.is_mentioned = ?", true).
And("issue_user.uid = ?", mentionedID) 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"). existInTeamQuery := builder.Select("team_user.team_id").
From("team_user"). From("team_user").
Where(builder.Eq{"team_user.uid": reviewRequestedID}) Where(builder.Eq{"team_user.uid": reviewRequestedID})
@ -375,11 +364,11 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
), ),
builder.In("review.id", maxReview), builder.In("review.id", maxReview),
)) ))
return sess.Where("issue.poster_id <> ?", reviewRequestedID). sess.Where("issue.poster_id <> ?", reviewRequestedID).
And(builder.In("issue.id", subQuery)) 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 // Query for pull requests where you are a reviewer or commenter, excluding
// any pull requests already returned by the review requested filter. // any pull requests already returned by the review requested filter.
notPoster := builder.Neq{"issue.poster_id": reviewedID} 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), 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 { func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
return sess.And( sess.And(
builder. builder.
NotIn("issue.id", NotIn("issue.id",
builder.Select("issue_id"). builder.Select("issue_id").

View File

@ -28,7 +28,7 @@ type PullRequestsOptions struct {
MilestoneID int64 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 := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
@ -46,7 +46,7 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.milestone_id=?", opts.MilestoneID) 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 // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
@ -130,23 +130,15 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
opts.Page = 1 opts.Page = 1
} }
countSession, err := listPullRequestStatement(ctx, baseRepoID, opts) countSession := listPullRequestStatement(ctx, baseRepoID, opts)
if err != nil {
log.Error("listPullRequestStatement: %v", err)
return nil, 0, err
}
maxResults, err := countSession.Count(new(PullRequest)) maxResults, err := countSession.Count(new(PullRequest))
if err != nil { if err != nil {
log.Error("Count PRs: %v", err) log.Error("Count PRs: %v", err)
return nil, maxResults, err return nil, maxResults, err
} }
findSession, err := listPullRequestStatement(ctx, baseRepoID, opts) findSession := listPullRequestStatement(ctx, baseRepoID, opts)
applySorts(findSession, opts.SortType, 0) applySorts(findSession, opts.SortType, 0)
if err != nil {
log.Error("listPullRequestStatement: %v", err)
return nil, maxResults, err
}
findSession = db.SetSessionPagination(findSession, opts) findSession = db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize) prs := make([]*PullRequest, 0, opts.PageSize)
return prs, maxResults, findSession.Find(&prs) return prs, maxResults, findSession.Find(&prs)
@ -200,8 +192,10 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
return nil, nil return nil, nil
} }
// Load issues. // Load issues which are not loaded
issueIDs := prs.GetIssueIDs() 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)) issues := make(map[int64]*Issue, len(issueIDs))
if err := db.GetEngine(ctx). if err := db.GetEngine(ctx).
In("id", issueIDs). In("id", issueIDs).
@ -237,10 +231,7 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
// GetIssueIDs returns all issue ids // GetIssueIDs returns all issue ids
func (prs PullRequestList) GetIssueIDs() []int64 { func (prs PullRequestList) GetIssueIDs() []int64 {
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) { return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
if pr.Issue == nil { return pr.IssueID, pr.IssueID > 0
return pr.IssueID, pr.IssueID > 0
}
return 0, false
}) })
} }

View 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)
}

View File

@ -207,31 +207,6 @@ type SearchRepoOptions struct {
OnlyShowRelevant bool 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 // UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond { func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{ return builder.Eq{

View File

@ -5,20 +5,48 @@ package repo
import "code.gitea.io/gitea/models/db" import "code.gitea.io/gitea/models/db"
// SearchOrderByMap represents all possible search order // OrderByMap represents all possible search order
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ var OrderByMap = map[string]map[string]db.SearchOrderBy{
"asc": { "asc": {
"alpha": "owner_name ASC, name ASC", "alpha": "owner_name ASC, name ASC",
"created": db.SearchOrderByOldest, "created": db.SearchOrderByOldest,
"updated": db.SearchOrderByLeastUpdated, "updated": db.SearchOrderByLeastUpdated,
"size": db.SearchOrderBySize, "size": "size ASC",
"id": db.SearchOrderByID, "git_size": "git_size ASC",
"lfs_size": "lfs_size ASC",
"id": db.SearchOrderByID,
"stars": db.SearchOrderByStars,
"forks": db.SearchOrderByForks,
}, },
"desc": { "desc": {
"alpha": "owner_name DESC, name DESC", "alpha": "owner_name DESC, name DESC",
"created": db.SearchOrderByNewest, "created": db.SearchOrderByNewest,
"updated": db.SearchOrderByRecentUpdated, "updated": db.SearchOrderByRecentUpdated,
"size": db.SearchOrderBySizeReverse, "size": "size DESC",
"id": db.SearchOrderByIDReverse, "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"],
}

View File

@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size) return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
} }
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment // AvatarLink returns the full avatar url with http host.
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
func (u *User) AvatarLink(ctx context.Context) string { func (u *User) AvatarLink(ctx context.Context) string {
return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0)) relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty
return httplib.MakeAbsoluteURL(ctx, relLink)
} }
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data // IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data

View 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)
}

View File

@ -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 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) parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil { if err != nil {
log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)

View File

@ -4,12 +4,67 @@
package base package base
import ( import (
"unicode/utf8"
"golang.org/x/text/collate" "golang.org/x/text/collate"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) {
if pos >= len(str) {
return 0, 0, false
}
r, size = utf8.DecodeRuneInString(str[pos:])
if r == utf8.RuneError {
r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii
}
return r, size, true
}
func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
end = pos
for {
r, size, has := naturalSortGetRune(str, end)
if !has {
break
}
isCurRuneNum := '0' <= r && r <= '9'
if end == pos {
isNumber = isCurRuneNum
end += size
} else if isCurRuneNum == isNumber {
end += size
} else {
break
}
}
return end, isNumber
}
// NaturalSortLess compares two strings so that they could be sorted in natural order // NaturalSortLess compares two strings so that they could be sorted in natural order
func NaturalSortLess(s1, s2 string) bool { func NaturalSortLess(s1, s2 string) bool {
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
// So we need to handle the number parts by ourselves
c := collate.New(language.English, collate.Numeric) c := collate.New(language.English, collate.Numeric)
return c.CompareString(s1, s2) < 0 pos1, pos2 := 0, 0
for pos1 < len(s1) && pos2 < len(s2) {
end1, isNum1 := naturalSortAdvance(s1, pos1)
end2, isNum2 := naturalSortAdvance(s2, pos2)
part1, part2 := s1[pos1:end1], s2[pos2:end2]
if isNum1 && isNum2 {
if part1 != part2 {
if len(part1) != len(part2) {
return len(part1) < len(part2)
}
return part1 < part2
}
} else {
if cmp := c.CompareString(part1, part2); cmp != 0 {
return cmp < 0
}
}
pos1, pos2 = end1, end2
}
return len(s1) < len(s2)
} }

View File

@ -10,21 +10,36 @@ import (
) )
func TestNaturalSortLess(t *testing.T) { func TestNaturalSortLess(t *testing.T) {
test := func(s1, s2 string, less bool) { testLess := func(s1, s2 string) {
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2) assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
}
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) testEqual("", "")
test("a", "B", true) testLess("", "a")
testLess("", "1")
test("cafe", "café", true) testLess("v1.2", "v1.2.0")
test("café", "cafe", false) testLess("v1.2.0", "v1.10.0")
test("caff", "café", false) 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")
} }

View File

@ -4,6 +4,7 @@
package cache package cache
import ( import (
"fmt"
"strconv" "strconv"
"time" "time"
@ -35,6 +36,37 @@ func Init() error {
return nil 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 // GetCache returns the currently configured cache
func GetCache() StringCache { func GetCache() StringCache {
return defaultCache return defaultCache

View File

@ -34,6 +34,18 @@ func TestNewContext(t *testing.T) {
assert.Nil(t, con) 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) { func TestGetCache(t *testing.T) {
createTestCache() createTestCache()

View File

@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string {
return req.Header.Get("X-Forwarded-Host") return req.Header.Get("X-Forwarded-Host")
} }
// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL // GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
func GuessCurrentAppURL(ctx context.Context) string { func GuessCurrentAppURL(ctx context.Context) string {
return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
}
// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
func GuessCurrentHostURL(ctx context.Context) string {
req, ok := ctx.Value(RequestContextKey).(*http.Request) req, ok := ctx.Value(RequestContextKey).(*http.Request)
if !ok { if !ok {
return setting.AppURL return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
} }
// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one. // If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong. // At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
@ -74,20 +79,27 @@ func GuessCurrentAppURL(ctx context.Context) string {
// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL. // So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
reqScheme := getRequestScheme(req) reqScheme := getRequestScheme(req)
if reqScheme == "" { if reqScheme == "" {
return setting.AppURL return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
} }
reqHost := getForwardedHost(req) reqHost := getForwardedHost(req)
if reqHost == "" { if reqHost == "" {
reqHost = req.Host reqHost = req.Host
} }
return reqScheme + "://" + reqHost + setting.AppSubURL + "/" return reqScheme + "://" + reqHost
} }
func MakeAbsoluteURL(ctx context.Context, s string) string { // MakeAbsoluteURL tries to make a link to an absolute URL:
if IsRelativeURL(s) { // * If link is empty, it returns the current app URL.
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/") // * If link is absolute, it returns the link.
// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
func MakeAbsoluteURL(ctx context.Context, link string) string {
if link == "" {
return GuessCurrentAppURL(ctx)
} }
return s if !IsRelativeURL(link) {
return link
}
return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
} }
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool { func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {

View File

@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) {
ctx := context.Background() ctx := context.Background()
assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, "")) assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo")) assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "foo"))
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo")) assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", Host: "user-host",
}) })
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", Host: "user-host",
@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
"X-Forwarded-Host": {"forwarded-host"}, "X-Forwarded-Host": {"forwarded-host"},
}, },
}) })
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{ ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
Host: "user-host", Host: "user-host",
@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
"X-Forwarded-Proto": {"https"}, "X-Forwarded-Proto": {"https"},
}, },
}) })
assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo")) assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo"))
} }
func TestIsCurrentGiteaSiteURL(t *testing.T) { func TestIsCurrentGiteaSiteURL(t *testing.T) {

View File

@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
searchOpt.MilestoneIDs = opts.MilestoneIDs 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 // See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id int64) optional.Option[int64] { convertID := func(id int64) optional.Option[int64] {
if id > 0 { if id > 0 {
@ -49,7 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
return nil return nil
} }
searchOpt.ProjectID = convertID(opts.ProjectID)
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
searchOpt.PosterID = convertID(opts.PosterID) searchOpt.PosterID = convertID(opts.PosterID)
searchOpt.AssigneeID = convertID(opts.AssigneeID) searchOpt.AssigneeID = convertID(opts.AssigneeID)

View File

@ -211,7 +211,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
for key, value := range headers { for key, value := range headers {
req.Header.Set(key, value) req.Header.Set(key, value)
} }
req.Header.Set("Accept", MediaType) req.Header.Set("Accept", AcceptHeader)
return req, nil return req, nil
} }
@ -251,6 +251,6 @@ func handleErrorResponse(resp *http.Response) error {
return err return err
} }
log.Trace("ErrorResponse: %v", er) log.Trace("ErrorResponse(%v): %v", resp.Status, er)
return errors.New(er.Message) return errors.New(er.Message)
} }

View File

@ -155,7 +155,7 @@ func TestHTTPClientDownload(t *testing.T) {
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response { hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
assert.Equal(t, "POST", req.Method) assert.Equal(t, "POST", req.Method)
assert.Equal(t, MediaType, req.Header.Get("Content-type")) 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 var batchRequest BatchRequest
err := json.NewDecoder(req.Body).Decode(&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 { hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
assert.Equal(t, "POST", req.Method) assert.Equal(t, "POST", req.Method)
assert.Equal(t, MediaType, req.Header.Get("Content-type")) 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 var batchRequest BatchRequest
err := json.NewDecoder(req.Body).Decode(&batchRequest) err := json.NewDecoder(req.Body).Decode(&batchRequest)

View File

@ -10,6 +10,8 @@ import (
const ( const (
// MediaType contains the media type for LFS server requests // MediaType contains the media type for LFS server requests
MediaType = "application/vnd.git-lfs+json" 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. // BatchRequest contains multiple requests processed in one batch operation.

View File

@ -37,6 +37,7 @@ func (a *BasicTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCl
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug("Download Request: %+v", req)
resp, err := performRequest(ctx, a.client, req) resp, err := performRequest(ctx, a.client, req)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -26,7 +26,7 @@ func TestBasicTransferAdapter(t *testing.T) {
p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5} p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5}
roundTripHandler := func(req *http.Request) *http.Response { 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")) assert.Equal(t, "test-value", req.Header.Get("test-header"))
url := req.URL.String() url := req.URL.String()

View File

@ -5,8 +5,6 @@ package markup
import ( import (
"bufio" "bufio"
"bytes"
"fmt"
"html" "html"
"io" "io"
"regexp" "regexp"
@ -15,6 +13,8 @@ import (
"code.gitea.io/gitea/modules/csv" "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
) )
func init() { 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 { func (r Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
tmpBlock := bufio.NewWriter(output) tmpBlock := bufio.NewWriter(output)
maxSize := setting.UI.CSV.MaxFileSize maxSize := setting.UI.CSV.MaxFileSize
maxRows := setting.UI.CSV.MaxRows
if maxSize == 0 { if maxSize != 0 {
return r.tableRender(ctx, input, tmpBlock) 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("&amp;")
case `'`:
_, err = tmpBlock.WriteString("&#39;") // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
case `<`:
_, err = tmpBlock.WriteString("&lt;")
case `>`:
_, err = tmpBlock.WriteString("&gt;")
case `"`:
_, err = tmpBlock.WriteString("&#34;") // "&#34;" is shorter than "&quot;".
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) rd, err := csv.CreateReaderAndDetermineDelimiter(ctx, input)
if err != nil { if err != nil {
return err return err
} }
if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil { if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil {
return err return err
} }
row := 1
row := 0
for { for {
fields, err := rd.Read() fields, err := rd.Read()
if err == io.EOF { if err == io.EOF || (row >= maxRows && maxRows != 0) {
break break
} }
if err != nil { if err != nil {
continue continue
} }
if _, err := tmpBlock.WriteString("<tr>"); err != nil { if _, err := tmpBlock.WriteString("<tr>"); err != nil {
return err return err
} }
element := "td" element := "td"
if row == 1 { if row == 0 {
element = "th" 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 return err
} }
for _, field := range fields { for _, field := range fields {
@ -174,8 +126,32 @@ func (Renderer) tableRender(ctx *markup.RenderContext, input io.Reader, tmpBlock
row++ row++
} }
if _, err = tmpBlock.WriteString("</table>"); err != nil { if _, err = tmpBlock.WriteString("</table>"); err != nil {
return err 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() return tmpBlock.Flush()
} }

View File

@ -4,8 +4,6 @@
package markup package markup
import ( import (
"bufio"
"bytes"
"strings" "strings"
"testing" "testing"
@ -31,12 +29,4 @@ func TestRenderCSV(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, v, buf.String()) 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,&lt;a&gt;\n2,&lt;b&gt;</pre>"
assert.Equal(t, want, buf.String())
})
} }

View File

@ -49,7 +49,7 @@ var (
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // 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 // 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. // 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 matches short but difficult to parse [[name|link|arg=test]] syntax
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
@ -372,7 +372,42 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
return nil 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 // Add user-content- to IDs and "#" links if they don't already have them
for idx, attr := range node.Attr { for idx, attr := range node.Attr {
val := strings.TrimPrefix(attr.Val, "#") val := strings.TrimPrefix(attr.Val, "#")
@ -397,21 +432,14 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
textNode(ctx, procs, node) textNode(ctx, procs, node)
case html.ElementNode: case html.ElementNode:
if node.Data == "img" { if node.Data == "img" {
for i, attr := range node.Attr { next := node.NextSibling
if attr.Key != "src" { handleNodeImg(ctx, node)
continue return next
}
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
}
} else if node.Data == "a" { } else if node.Data == "a" {
// Restrict text in links to emojis // Restrict text in links to emojis
procs = emojiProcessors procs = emojiProcessors
} else if node.Data == "code" || node.Data == "pre" { } else if node.Data == "code" || node.Data == "pre" {
return return node.NextSibling
} else if node.Data == "i" { } else if node.Data == "i" {
for _, attr := range node.Attr { for _, attr := range node.Attr {
if attr.Key != "class" { 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 { for n := node.FirstChild; n != nil; {
visitNode(ctx, procs, n) n = visitNode(ctx, procs, n)
} }
} }
// ignore everything else return node.NextSibling
} }
// textNode runs the passed node through various processors, in order to handle // 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? // 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. // 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 ( var (
found bool found bool

View File

@ -18,8 +18,7 @@ import (
const ( const (
TestAppURL = "http://localhost:3000/" TestAppURL = "http://localhost:3000/"
TestOrgRepo = "gogits/gogs" TestRepoURL = TestAppURL + "test-owner/test-repo/"
TestRepoURL = TestAppURL + TestOrgRepo + "/"
) )
// externalIssueLink an HTML link to an alphanumeric-style issue // 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 // these values should match the TestOrgRepo const above
var localMetas = map[string]string{ var localMetas = map[string]string{
"user": "gogits", "user": "test-owner",
"repo": "gogs", "repo": "test-repo",
} }
func TestRender_IssueIndexPattern(t *testing.T) { 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>`) `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", 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>`) `<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", test("http://localhost:3000/test-owner/test-repo/issues/4",
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a>`) `<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a>`)
test("http://localhost:3000/gogits/gogs/issues/4 test", test("http://localhost:3000/test-owner/test-repo/issues/4 test",
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a> test`) `<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a> test`)
test("http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123 test", test("http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123 test",
`<a href="http://localhost:3000/gogits/gogs/issues/4?a=1&amp;b=2#comment-123" class="ref-issue">#4 (comment)</a> test`) `<a href="http://localhost:3000/test-owner/test-repo/issues/4?a=1&amp;b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24", test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24",
"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", test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files",
@ -381,6 +380,7 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
"(abcdefabcdefabcdefabcdefabcdefabcdefabcd)", "(abcdefabcdefabcdefabcdefabcdefabcdefabcd)",
"[abcdefabcdefabcdefabcdefabcdefabcdefabcd]", "[abcdefabcdefabcdefabcdefabcdefabcdefabcd]",
"abcdefabcdefabcdefabcdefabcdefabcdefabcd.", "abcdefabcdefabcdefabcdefabcdefabcdefabcd.",
"abcdefabcdefabcdefabcdefabcdefabcdefabcd:",
} }
falseTestCases := []string{ falseTestCases := []string{
"test", "test",

View File

@ -120,8 +120,8 @@ func TestRender_CrossReferences(t *testing.T) {
} }
test( test(
"gogits/gogs#12345", "test-owner/test-repo#12345",
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogits", "gogs", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogits/gogs#12345</a></p>`) `<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( test(
"go-gitea/gitea#12345", "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>`) `<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
@ -530,43 +530,31 @@ func TestRender_ShortLinks(t *testing.T) {
} }
func TestRender_RelativeImages(t *testing.T) { func TestRender_RelativeImages(t *testing.T) {
setting.AppURL = markup.TestAppURL render := func(input string, isWiki bool, links markup.Links) string {
test := func(input, expected, expectedWiki string) {
buffer, err := markdown.RenderString(&markup.RenderContext{ buffer, err := markdown.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: 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,
},
Metas: localMetas, Metas: localMetas,
IsWiki: true, IsWiki: isWiki,
}, input) }, input)
assert.NoError(t, err) 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") out := render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master") 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( out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
`<img src="Link">`, 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)
`<img src="`+util.URLJoin(mediatree, "Link")+`"/>`,
`<img src="`+util.URLJoin(rawwiki, "Link")+`"/>`)
test( out = render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
`<img src="./icon.png">`, 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)
`<img src="`+util.URLJoin(mediatree, "icon.png")+`"/>`,
`<img src="`+util.URLJoin(rawwiki, "icon.png")+`"/>`) 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) { func Test_ParseClusterFuzz(t *testing.T) {
@ -719,5 +707,6 @@ func TestIssue18471(t *testing.T) {
func TestIsFullURL(t *testing.T) { func TestIsFullURL(t *testing.T) {
assert.True(t, markup.IsFullURLString("https://example.com")) assert.True(t, markup.IsFullURLString("https://example.com"))
assert.True(t, markup.IsFullURLString("mailto:test@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")) assert.False(t, markup.IsFullURLString("/foo:bar"))
} }

View File

@ -1019,4 +1019,10 @@ func TestAttention(t *testing.T) {
test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>") test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>") test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\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>")
} }

View File

@ -31,10 +31,16 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
return nil, parser.NoChildren return nil, parser.NoChildren
} }
dollars := false var dollars bool
if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' { if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
dollars = true 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 return nil, parser.NoChildren
} }

View File

@ -9,9 +9,9 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/markup/common"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/util"
) )
type prefixedIDs struct { type prefixedIDs struct {
@ -36,7 +36,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
if !bytes.HasPrefix(result, []byte("user-content-")) { if !bytes.HasPrefix(result, []byte("user-content-")) {
result = append([]byte("user-content-"), result...) result = append([]byte("user-content-"), result...)
} }
if p.values.Add(util.BytesToReadOnlyString(result)) { if p.values.Add(util.UnsafeBytesToString(result)) {
return result return result
} }
for i := 1; ; i++ { 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. // Put puts a given element id to the used ids table.
func (p *prefixedIDs) Put(value []byte) { func (p *prefixedIDs) Put(value []byte) {
p.values.Add(util.BytesToReadOnlyString(value)) p.values.Add(util.UnsafeBytesToString(value))
} }
func newPrefixedIDs() *prefixedIDs { func newPrefixedIDs() *prefixedIDs {

View File

@ -15,7 +15,7 @@ import (
"golang.org/x/text/language" "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) { func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if entering { if entering {
n := node.(*Attention) n := node.(*Attention)
@ -37,38 +37,93 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) { func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
// We only want attention blockquotes when the AST looks like: if firstParagraph.ChildCount() < 1 {
// > Text("[") Text("!TYPE") Text("]") 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 func (g *ASTTransformer) extractBlockquoteAttention2(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
firstParagraph := v.FirstChild() if firstParagraph.ChildCount() < 2 {
g.applyElementDir(firstParagraph) return "", nil
if firstParagraph.ChildCount() < 3 {
return ast.WalkContinue, nil
} }
node1, ok := firstParagraph.FirstChild().(*ast.Text) node1, ok := firstParagraph.FirstChild().(*ast.Text)
if !ok { if !ok {
return ast.WalkContinue, nil return "", nil
} }
node2, ok := node1.NextSibling().(*ast.Text) node2, ok := node1.NextSibling().(*ast.Text)
if !ok { 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) node3, ok := node2.NextSibling().(*ast.Text)
if !ok { if !ok {
return ast.WalkContinue, nil return "", nil
} }
val1 := string(node1.Segment.Value(reader.Source())) val1 := string(node1.Segment.Value(reader.Source()))
val2 := string(node2.Segment.Value(reader.Source())) val2 := string(node2.Segment.Value(reader.Source()))
val3 := string(node3.Segment.Value(reader.Source())) val3 := string(node3.Segment.Value(reader.Source()))
if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") { if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
return ast.WalkContinue, nil return "", nil
} }
// grab attention type from markdown source
attentionType := strings.ToLower(val2[1:]) 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 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, NewAttention(attentionType))
attentionParagraph.AppendChild(attentionParagraph, emphasis) attentionParagraph.AppendChild(attentionParagraph, emphasis)
firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph) firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
firstParagraph.RemoveChild(firstParagraph, node1) for _, processed := range processedNodes {
firstParagraph.RemoveChild(firstParagraph, node2) firstParagraph.RemoveChild(firstParagraph, processed)
firstParagraph.RemoveChild(firstParagraph, node3) }
if firstParagraph.ChildCount() == 0 { if firstParagraph.ChildCount() == 0 {
firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph) firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
} }

View File

@ -7,10 +7,10 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text" "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) { 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()) txt := v.Text(reader.Source())
header := markup.Header{ header := markup.Header{
Text: util.BytesToReadOnlyString(txt), Text: util.UnsafeBytesToString(txt),
Level: v.Level, Level: v.Level,
} }
if id, found := v.AttributeString("id"); found { if id, found := v.AttributeString("id"); found {
header.ID = util.BytesToReadOnlyString(id.([]byte)) header.ID = util.UnsafeBytesToString(id.([]byte))
} }
*tocList = append(*tocList, header) *tocList = append(*tocList, header)
g.applyElementDir(v) g.applyElementDir(v)

View File

@ -74,7 +74,7 @@ type RenderContext struct {
Type string Type string
IsWiki bool IsWiki bool
Links Links Links Links
Metas map[string]string Metas map[string]string // user, repo, mode(comment/document)
DefaultLink string DefaultLink string
GitRepo *git.Repository GitRepo *git.Repository
Repo gitrepo.Repository Repo gitrepo.Repository
@ -86,10 +86,10 @@ type RenderContext struct {
} }
type Links struct { type Links struct {
AbsolutePrefix bool AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
Base string Base string // base prefix for pre-provided links and medias (images, videos)
BranchPath string BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
TreePath string TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
} }
func (l *Links) Prefix() string { func (l *Links) Prefix() string {

View File

@ -6,6 +6,7 @@ package composer
import ( import (
"archive/zip" "archive/zip"
"io" "io"
"path"
"regexp" "regexp"
"strings" "strings"
@ -36,10 +37,14 @@ type Package struct {
Metadata *Metadata Metadata *Metadata
} }
// https://getcomposer.org/doc/04-schema.md
// Metadata represents the metadata of a Composer package // Metadata represents the metadata of a Composer package
type Metadata struct { type Metadata struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"` Keywords []string `json:"keywords,omitempty"`
Comments Comments `json:"_comments,omitempty"`
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
License Licenses `json:"license,omitempty"` License Licenses `json:"license,omitempty"`
Authors []Author `json:"authors,omitempty"` Authors []Author `json:"authors,omitempty"`
@ -74,6 +79,28 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
return nil 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 // Author represents an author
type Author struct { type Author struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -101,14 +128,14 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
} }
defer f.Close() defer f.Close()
return ParseComposerFile(f) return ParseComposerFile(archive, path.Dir(file.Name), f)
} }
} }
return nil, ErrMissingComposerFile return nil, ErrMissingComposerFile
} }
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package // 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 { var cj struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
@ -137,6 +164,19 @@ func ParseComposerFile(r io.Reader) (*Package, error) {
cj.Type = "library" 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{ return &Package{
Name: cj.Name, Name: cj.Name,
Version: cj.Version, Version: cj.Version,

View File

@ -17,6 +17,8 @@ import (
const ( const (
name = "gitea/composer-package" name = "gitea/composer-package"
description = "Package Description" description = "Package Description"
readme = "Package Readme"
comments = "Package Comment"
packageType = "composer-plugin" packageType = "composer-plugin"
author = "Gitea Authors" author = "Gitea Authors"
email = "no.reply@gitea.io" email = "no.reply@gitea.io"
@ -41,7 +43,8 @@ const composerContent = `{
}, },
"require": { "require": {
"php": ">=7.2 || ^8.0" "php": ">=7.2 || ^8.0"
} },
"_comments": "` + comments + `"
}` }`
func TestLicenseUnmarshal(t *testing.T) { func TestLicenseUnmarshal(t *testing.T) {
@ -54,18 +57,30 @@ func TestLicenseUnmarshal(t *testing.T) {
assert.Equal(t, "MIT", l[0]) 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) { func TestParsePackage(t *testing.T) {
createArchive := func(name, content string) []byte { createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer var buf bytes.Buffer
archive := zip.NewWriter(&buf) archive := zip.NewWriter(&buf)
w, _ := archive.Create(name) for name, content := range files {
w.Write([]byte(content)) w, _ := archive.Create(name)
w.Write([]byte(content))
}
archive.Close() archive.Close()
return buf.Bytes() return buf.Bytes()
} }
t.Run("MissingComposerFile", func(t *testing.T) { 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))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
@ -73,7 +88,7 @@ func TestParsePackage(t *testing.T) {
}) })
t.Run("MissingComposerFileInRoot", func(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))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
@ -81,43 +96,52 @@ func TestParsePackage(t *testing.T) {
}) })
t.Run("InvalidComposerFile", func(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))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("Valid", func(t *testing.T) { t.Run("InvalidPackageName", func(t *testing.T) {
data := createArchive("composer.json", composerContent) data := createArchive(map[string]string{"composer.json": "{}"})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) 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.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName) assert.ErrorIs(t, err, ErrInvalidName)
}) })
t.Run("InvalidPackageVersion", func(t *testing.T) { 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.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion) 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) { 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.NoError(t, err)
assert.NotNil(t, cp) assert.NotNil(t, cp)
assert.Equal(t, name, cp.Name) assert.Equal(t, name, cp.Name)
assert.Empty(t, cp.Version) assert.Empty(t, cp.Version)
assert.Equal(t, description, cp.Metadata.Description) 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.Len(t, cp.Metadata.Authors, 1)
assert.Equal(t, author, cp.Metadata.Authors[0].Name) assert.Equal(t, author, cp.Metadata.Authors[0].Name)
assert.Equal(t, email, cp.Metadata.Authors[0].Email) assert.Equal(t, email, cp.Metadata.Authors[0].Email)

View File

@ -185,8 +185,6 @@ func ParseDescription(r io.Reader) (*Package, error) {
} }
func setField(p *Package, data string) error { func setField(p *Package, data string) error {
const listDelimiter = ", "
if data == "" { if data == "" {
return nil return nil
} }
@ -215,19 +213,19 @@ func setField(p *Package, data string) error {
case "Description": case "Description":
p.Metadata.Description = value p.Metadata.Description = value
case "URL": case "URL":
p.Metadata.ProjectURL = splitAndTrim(value, listDelimiter) p.Metadata.ProjectURL = splitAndTrim(value)
case "License": case "License":
p.Metadata.License = value p.Metadata.License = value
case "Author": case "Author":
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""), listDelimiter) p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
case "Depends": case "Depends":
p.Metadata.Depends = splitAndTrim(value, listDelimiter) p.Metadata.Depends = splitAndTrim(value)
case "Imports": case "Imports":
p.Metadata.Imports = splitAndTrim(value, listDelimiter) p.Metadata.Imports = splitAndTrim(value)
case "Suggests": case "Suggests":
p.Metadata.Suggests = splitAndTrim(value, listDelimiter) p.Metadata.Suggests = splitAndTrim(value)
case "LinkingTo": case "LinkingTo":
p.Metadata.LinkingTo = splitAndTrim(value, listDelimiter) p.Metadata.LinkingTo = splitAndTrim(value)
case "NeedsCompilation": case "NeedsCompilation":
p.Metadata.NeedsCompilation = value == "yes" p.Metadata.NeedsCompilation = value == "yes"
} }
@ -235,8 +233,8 @@ func setField(p *Package, data string) error {
return nil return nil
} }
func splitAndTrim(s, sep string) []string { func splitAndTrim(s string) []string {
items := strings.Split(s, sep) items := strings.Split(s, ", ")
for i := range items { for i := range items {
items[i] = strings.TrimSpace(items[i]) items[i] = strings.TrimSpace(items[i])
} }

View File

@ -14,8 +14,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/mdstripper" "code.gitea.io/gitea/modules/markup/mdstripper"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/util"
) )
var ( var (
@ -341,7 +340,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool
return false, nil 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 { if r == nil {
return false, nil return false, nil
} }

View File

@ -45,6 +45,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
if err != nil { if err != nil {
return 0, fmt.Errorf("UpdateRepository: %w", err) return 0, fmt.Errorf("UpdateRepository: %w", err)
} }
repo.ObjectFormatName = objFmt.Name() // keep consistent with db
allBranches := container.Set[string]{} allBranches := container.Set[string]{}
{ {

View File

@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
// decodeEnvironmentKey decode the environment key to section and key // 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 // 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) { if !strings.HasPrefix(envKey, prefixGitea) {
return false, "", "", false return false, "", "", false
} }

18
modules/setting/global.go Normal file
View 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
)

View File

@ -6,7 +6,6 @@ package setting
import ( import (
"fmt" "fmt"
"math" "math"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -19,7 +18,6 @@ var (
Storage *Storage Storage *Storage
Enabled bool Enabled bool
ChunkedUploadPath string ChunkedUploadPath string
RegistryHost string
LimitTotalOwnerCount int64 LimitTotalOwnerCount int64
LimitTotalOwnerSize int64 LimitTotalOwnerSize int64
@ -66,9 +64,6 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
return err return err
} }
appURL, _ := url.Parse(AppURL)
Packages.RegistryHost = appURL.Host
Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload")) Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
if !filepath.IsAbs(Packages.ChunkedUploadPath) { if !filepath.IsAbs(Packages.ChunkedUploadPath) {
Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath)) Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))

View File

@ -40,16 +40,16 @@ const (
LandingPageLogin LandingPage = "/user/login" LandingPageLogin LandingPage = "/user/login"
) )
// Server settings
var ( 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 // AppURL is the Application ROOT_URL. It always has a '/' suffix
// It maps to ini:"ROOT_URL" // It maps to ini:"ROOT_URL"
AppURL string AppURL string
// AppSubURL represents the sub-url mounting point for gitea. It is either "" or starts with '/' and ends without '/', such as '/{subpath}'. // 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. // This value is empty if site does not have sub-url.
AppSubURL string 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. // AppDataPath is the default path for storing data.
// It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data" // It maps to ini:"APP_DATA_PATH" in [server] and defaults to AppWorkPath + "/data"
AppDataPath string AppDataPath string
@ -59,8 +59,6 @@ var (
// AssetVersion holds a opaque value that is used for cache-busting assets // AssetVersion holds a opaque value that is used for cache-busting assets
AssetVersion string AssetVersion string
// Server settings
Protocol Scheme Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` 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. // This should be TrimRight to ensure that there is only a single '/' at the end of AppURL.
AppURL = strings.TrimRight(appURL.String(), "/") + "/" 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. // This value is empty if site does not have sub-url.
AppSubURL = strings.TrimSuffix(appURL.Path, "/") AppSubURL = strings.TrimSuffix(appURL.Path, "/")
UseSubURLPath = sec.Key("USE_SUB_URL_PATH").MustBool(false)
StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/")
// Check if Domain differs from AppURL domain than update it to AppURL's domain // Check if Domain differs from AppURL domain than update it to AppURL's domain

View File

@ -25,12 +25,7 @@ var (
// AppStartTime store time gitea has started // AppStartTime store time gitea has started
AppStartTime time.Time AppStartTime time.Time
// Other global setting objects
CfgProvider ConfigProvider CfgProvider ConfigProvider
RunMode string
RunUser string
IsProd bool
IsWindows bool IsWindows bool
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing

View File

@ -161,7 +161,7 @@ const (
targetSecIsSec // target section is from the name seciont [name] 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) targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
if err != nil { if err != nil {
if !IsValidStorageType(StorageType(typ)) { if !IsValidStorageType(StorageType(typ)) {

View File

@ -52,6 +52,7 @@ var UI = struct {
CSV struct { CSV struct {
MaxFileSize int64 MaxFileSize int64
MaxRows int
} `ini:"ui.csv"` } `ini:"ui.csv"`
Admin struct { Admin struct {
@ -107,8 +108,10 @@ var UI = struct {
}, },
CSV: struct { CSV: struct {
MaxFileSize int64 MaxFileSize int64
MaxRows int
}{ }{
MaxFileSize: 524288, MaxFileSize: 524288,
MaxRows: 2500,
}, },
Admin: struct { Admin: struct {
UserPagingNum int UserPagingNum int

View File

@ -163,10 +163,7 @@ func (a *AzureBlobStorage) getObjectNameFromPath(path string) string {
// Open opens a file // Open opens a file
func (a *AzureBlobStorage) Open(path string) (Object, error) { func (a *AzureBlobStorage) Open(path string) (Object, error) {
blobClient, err := a.getBlobClient(path) blobClient := a.getBlobClient(path)
if err != nil {
return nil, convertAzureBlobErr(err)
}
res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{}) res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
if err != nil { if err != nil {
return nil, convertAzureBlobErr(err) return nil, convertAzureBlobErr(err)
@ -229,10 +226,7 @@ func (a azureBlobFileInfo) Sys() any {
// Stat returns the stat information of the object // Stat returns the stat information of the object
func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) { func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
blobClient, err := a.getBlobClient(path) blobClient := a.getBlobClient(path)
if err != nil {
return nil, convertAzureBlobErr(err)
}
res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{}) res, err := blobClient.GetProperties(a.ctx, &blob.GetPropertiesOptions{})
if err != nil { if err != nil {
return nil, convertAzureBlobErr(err) return nil, convertAzureBlobErr(err)
@ -247,20 +241,14 @@ func (a *AzureBlobStorage) Stat(path string) (os.FileInfo, error) {
// Delete delete a file // Delete delete a file
func (a *AzureBlobStorage) Delete(path string) error { func (a *AzureBlobStorage) Delete(path string) error {
blobClient, err := a.getBlobClient(path) blobClient := a.getBlobClient(path)
if err != nil { _, err := blobClient.Delete(a.ctx, nil)
return convertAzureBlobErr(err)
}
_, err = blobClient.Delete(a.ctx, nil)
return convertAzureBlobErr(err) return convertAzureBlobErr(err)
} }
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. // 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) { func (a *AzureBlobStorage) URL(path, name string) (*url.URL, error) {
blobClient, err := a.getBlobClient(path) blobClient := a.getBlobClient(path)
if err != nil {
return nil, convertAzureBlobErr(err)
}
startTime := time.Now() startTime := time.Now()
u, err := blobClient.GetSASURL(sas.BlobPermissions{ u, err := blobClient.GetSASURL(sas.BlobPermissions{
@ -290,10 +278,7 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
return convertAzureBlobErr(err) return convertAzureBlobErr(err)
} }
for _, object := range resp.Segment.BlobItems { for _, object := range resp.Segment.BlobItems {
blobClient, err := a.getBlobClient(*object.Name) blobClient := a.getBlobClient(*object.Name)
if err != nil {
return convertAzureBlobErr(err)
}
object := &azureBlobObject{ object := &azureBlobObject{
Context: a.ctx, Context: a.ctx,
blobClient: blobClient, blobClient: blobClient,
@ -313,8 +298,8 @@ func (a *AzureBlobStorage) IterateObjects(dirName string, fn func(path string, o
} }
// Delete delete a file // Delete delete a file
func (a *AzureBlobStorage) getBlobClient(path string) (*blob.Client, error) { func (a *AzureBlobStorage) getBlobClient(path string) *blob.Client {
return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path)), nil return a.client.ServiceClient().NewContainerClient(a.cfg.Container).NewBlobClient(a.buildAzureBlobPath(path))
} }
func init() { func init() {

View File

@ -25,7 +25,8 @@ type MarkupOption struct {
// //
// in: body // in: body
Mode string Mode string
// Context to render // URL path for rendering issue, media and file links
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
// //
// in: body // in: body
Context string Context string
@ -53,7 +54,8 @@ type MarkdownOption struct {
// //
// in: body // in: body
Mode string Mode string
// Context to render // URL path for rendering issue, media and file links
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
// //
// in: body // in: body
Context string Context string

View File

@ -3,6 +3,8 @@
package structs package structs
import "time"
// Tag represents a repository tag // Tag represents a repository tag
type Tag struct { type Tag struct {
Name string `json:"name"` Name string `json:"name"`
@ -38,3 +40,29 @@ type CreateTagOption struct {
Message string `json:"message"` Message string `json:"message"`
Target string `json:"target"` 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"`
}

View File

@ -8,8 +8,7 @@ import (
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/util"
) )
// DBStore can be used to store app state items in local filesystem // 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 == "" { if content == "" {
return nil return nil
} }
return json.Unmarshal(util.StringToReadOnlyBytes(content), item) return json.Unmarshal(util.UnsafeStringToBytes(content), item)
} }
// Set saves the state item // Set saves the state item
@ -33,5 +32,5 @@ func (f *DBStore) Set(ctx context.Context, item StateItem) error {
if err != nil { if err != nil {
return err return err
} }
return system.SaveAppStateContent(ctx, item.Name(), util.BytesToReadOnlyString(b)) return system.SaveAppStateContent(ctx, item.Name(), util.UnsafeBytesToString(b))
} }

View File

@ -9,6 +9,7 @@ import (
"html" "html"
"html/template" "html/template"
"net/url" "net/url"
"reflect"
"slices" "slices"
"strings" "strings"
"time" "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, // 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). // and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
func Iif(condition bool, vals ...any) any { func Iif(condition any, vals ...any) any {
if condition { if isTemplateTruthy(condition) {
return vals[0] return vals[0]
} else if len(vals) > 1 { } else if len(vals) > 1 {
return vals[1] return vals[1]
@ -246,6 +247,32 @@ func Iif(condition bool, vals ...any) any {
return nil 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. // 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. // To use this helper function in templates, pass each token as a separate parameter.
// //

View File

@ -5,8 +5,11 @@ package templates
import ( import (
"html/template" "html/template"
"strings"
"testing" "testing"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -65,3 +68,41 @@ func TestHTMLFormat(t *testing.T) {
func TestSanitizeHTML(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>`)) 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))
}

View File

@ -34,8 +34,10 @@ func IsNormalPageCompleted(s string) bool {
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`) 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 old := *p
*p = v if len(v) > 0 {
*p = v[0]
}
return func() { *p = old } return func() { *p = old }
} }

View File

@ -15,10 +15,7 @@ import (
// GenerateKeyPair generates a public and private keypair // GenerateKeyPair generates a public and private keypair
func GenerateKeyPair(bits int) (string, string, error) { func GenerateKeyPair(bits int) (string, string, error) {
priv, _ := rsa.GenerateKey(rand.Reader, bits) priv, _ := rsa.GenerateKey(rand.Reader, bits)
privPem, err := pemBlockForPriv(priv) privPem := pemBlockForPriv(priv)
if err != nil {
return "", "", err
}
pubPem, err := pemBlockForPub(&priv.PublicKey) pubPem, err := pemBlockForPub(&priv.PublicKey)
if err != nil { if err != nil {
return "", "", err return "", "", err
@ -26,12 +23,12 @@ func GenerateKeyPair(bits int) (string, string, error) {
return privPem, pubPem, nil return privPem, pubPem, nil
} }
func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) { func pemBlockForPriv(priv *rsa.PrivateKey) string {
privBytes := pem.EncodeToMemory(&pem.Block{ privBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv), Bytes: x509.MarshalPKCS1PrivateKey(priv),
}) })
return string(privBytes), nil return string(privBytes)
} }
func pemBlockForPub(pub *rsa.PublicKey) (string, error) { func pemBlockForPub(pub *rsa.PublicKey) (string, error) {

View File

@ -6,8 +6,6 @@ package util
import ( import (
"bytes" "bytes"
"unicode" "unicode"
"github.com/yuin/goldmark/util"
) )
type sanitizedError struct { 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" // 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 { func SanitizeCredentialURLs(s string) string {
bs := util.StringToReadOnlyBytes(s) bs := UnsafeStringToBytes(s)
schemeSepPos := bytes.Index(bs, schemeSep) schemeSepPos := bytes.Index(bs, schemeSep)
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 { if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
return s // fast return if there is no URL scheme or no userinfo 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) schemeSepPos = bytes.Index(bs, schemeSep)
} }
out = append(out, bs...) out = append(out, bs...)
return util.BytesToReadOnlyString(out) return UnsafeBytesToString(out)
} }

View File

@ -87,11 +87,11 @@ func ToSnakeCase(input string) string {
} }
// UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a 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 { func UnsafeBytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b)) 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 { func UnsafeStringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s)) return unsafe.Slice(unsafe.StringData(s), len(s))
} }

View File

@ -35,6 +35,10 @@ func GetSiteCookie(req *http.Request, name string) string {
// SetSiteCookie returns given cookie value from request header. // SetSiteCookie returns given cookie value from request header.
func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) { 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{ cookie := &http.Cookie{
Name: name, Name: name,
Value: url.QueryEscape(value), Value: url.QueryEscape(value),
@ -46,10 +50,6 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
SameSite: setting.SessionConfig.SameSite, SameSite: setting.SessionConfig.SameSite,
} }
resp.Header().Add("Set-Cookie", cookie.String()) 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 // deleteLegacySiteCookie deletes the cookie with the given name at the cookie

47
options/gitignore/IAR Normal file
View File

@ -0,0 +1,47 @@
# Compiled binaries
*.o
*.bin
*.elf
*.hex
*.map
*.out
*.obj
# Trash
*.bak
thumbs.db
*.~*
# IAR Settings
**/settings/*.crun
**/settings/*.dbgdt
**/settings/*.cspy
**/settings/*.cspy.*
**/settings/*.xcl
**/settings/*.dni
**/settings/*.wsdt
**/settings/*.wspos
# IAR Debug Exe
**/Exe/*.sim
# IAR Debug Obj
**/Obj/*.pbd
**/Obj/*.pbd.*
**/Obj/*.pbi
**/Obj/*.pbi.*
# IAR project "Debug" directory
Debug/
# IAR project "Release" directory
Release/
# IAR project settings directory
settings/
# IAR backup files
Backup*
# IAR .dep files
*.dep

View File

@ -42,10 +42,3 @@ fastlane/report.xml
fastlane/Preview.html fastlane/Preview.html
fastlane/screenshots/**/*.png fastlane/screenshots/**/*.png
fastlane/test_output fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/

View File

@ -35,6 +35,3 @@ override.tf.json
# Ignore CLI configuration files # Ignore CLI configuration files
.terraformrc .terraformrc
terraform.rc terraform.rc
# Ignore hcl file
.terraform.lock.hcl

View File

@ -164,6 +164,8 @@ search=Hledat...
type_tooltip=Druh vyhledávání type_tooltip=Druh vyhledávání
fuzzy=Fuzzy fuzzy=Fuzzy
fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu fuzzy_tooltip=Zahrnout výsledky, které také úzce odpovídají hledanému výrazu
exact=Přesně
exact_tooltip=Zahrnout pouze výsledky, které přesně odpovídají hledanému výrazu
repo_kind=Hledat repozitáře... repo_kind=Hledat repozitáře...
user_kind=Hledat uživatele... user_kind=Hledat uživatele...
org_kind=Hledat organizace... org_kind=Hledat organizace...
@ -177,6 +179,8 @@ branch_kind=Hledat větve...
commit_kind=Hledat commity... commit_kind=Hledat commity...
runner_kind=Hledat runnery... runner_kind=Hledat runnery...
no_results=Nebyly nalezeny žádné odpovídající výsledky. no_results=Nebyly nalezeny žádné odpovídající výsledky.
issue_kind=Hledat úkoly...
pull_kind=Hledat pull request...
keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu. keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
[aria] [aria]
@ -432,6 +436,7 @@ oauth_signin_submit=Propojit účet
oauth.signin.error=Došlo k chybě při zpracování žádosti o autorizaci. Pokud tato chyba přetrvává, obraťte se na správce webu. oauth.signin.error=Došlo k chybě při zpracování žádosti o autorizaci. Pokud tato chyba přetrvává, obraťte se na správce webu.
oauth.signin.error.access_denied=Žádost o autorizaci byla zamítnuta. oauth.signin.error.access_denied=Žádost o autorizaci byla zamítnuta.
oauth.signin.error.temporarily_unavailable=Autorizace se nezdařila, protože ověřovací server je dočasně nedostupný. Opakujte akci později. oauth.signin.error.temporarily_unavailable=Autorizace se nezdařila, protože ověřovací server je dočasně nedostupný. Opakujte akci později.
oauth_callback_unable_auto_reg=Automatická registrace je povolena, ale OAuth2 poskytovatel %[1]s vrátil chybějící pole: %[2]s, nelze vytvořit účet automaticky, vytvořte účet nebo se připojte k účtu, nebo kontaktujte správce webu.
openid_connect_submit=Připojit openid_connect_submit=Připojit
openid_connect_title=Připojení k existujícímu účtu openid_connect_title=Připojení k existujícímu účtu
openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde. openid_connect_desc=Zvolené OpenID URI není známé. Přidružte nový účet zde.
@ -712,8 +717,9 @@ cancel=Zrušit
language=Jazyk language=Jazyk
ui=Motiv vzhledu ui=Motiv vzhledu
hidden_comment_types=Skryté typy komentářů hidden_comment_types=Skryté typy komentářů
hidden_comment_types_description=Zde zaškrtnuté typy komentářů nebudou zobrazeny na stránkách úkolů. Zaškrtnutím položky „Štítek“ například odstraní všechny komentáře „{uživatel} přidal/odstranil {štítek}“.
hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/… hidden_comment_types.ref_tooltip=Komentáře, na které se odkazovalo z jiného úkolu/commitu/…
hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s problémem hidden_comment_types.issue_ref_tooltip=Komentáře, kde uživatel změní větev/značku spojenou s úkolem
comment_type_group_reference=Reference comment_type_group_reference=Reference
comment_type_group_label=Štítek comment_type_group_label=Štítek
comment_type_group_milestone=Milník comment_type_group_milestone=Milník
@ -758,6 +764,8 @@ manage_themes=Vyberte výchozí motiv vzhledu
manage_openid=Správa OpenID adres manage_openid=Správa OpenID adres
email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu. email_desc=Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla, a pokud není skrytá, pro operace Gitu.
theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou. theme_desc=Toto bude váš výchozí motiv vzhledu napříč stránkou.
theme_colorblindness_help=Podpora šablony pro barvoslepost
theme_colorblindness_prompt=Gitea právě získala některé motivy se základní podporou barvosleposti, které mají pouze několik barev. Práce stále probíhá. Další vylepšení by bylo možné provést definováním více barev v CSS souborů.
primary=Hlavní primary=Hlavní
activated=Aktivován activated=Aktivován
requires_activation=Vyžaduje aktivaci requires_activation=Vyžaduje aktivaci
@ -882,6 +890,7 @@ repo_and_org_access=Repozitář a přístup organizace
permissions_public_only=Pouze veřejnost permissions_public_only=Pouze veřejnost
permissions_access_all=Vše (veřejné, soukromé a omezené) permissions_access_all=Vše (veřejné, soukromé a omezené)
select_permissions=Vyberte oprávnění select_permissions=Vyberte oprávnění
permission_not_set=Není nastaveno
permission_no_access=Bez přístupu permission_no_access=Bez přístupu
permission_read=Přečtené permission_read=Přečtené
permission_write=čtení i zápis permission_write=čtení i zápis
@ -1061,6 +1070,7 @@ watchers=Sledující
stargazers=Sledující stargazers=Sledující
stars_remove_warning=Tímto odstraníte všechny hvězdičky z tohoto repozitáře. stars_remove_warning=Tímto odstraníte všechny hvězdičky z tohoto repozitáře.
forks=Rozštěpení forks=Rozštěpení
stars=Oblíbené
reactions_more=a %d dalších reactions_more=a %d dalších
unit_disabled=Správce webu zakázal tuto sekci repozitáře. unit_disabled=Správce webu zakázal tuto sekci repozitáře.
language_other=Jiný language_other=Jiný
@ -1108,7 +1118,7 @@ template.one_item=Musíte vybrat alespoň jednu položku šablony
template.invalid=Musíte vybrat repositář šablony template.invalid=Musíte vybrat repositář šablony
archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty. archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly nebo pull requesty.
archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo pull requesty. archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat úkoly nebo pull requesty.
archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly. archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly.
archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty. archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat pull requesty.
@ -1228,6 +1238,9 @@ file_view_rendered=Zobrazit vykreslené
file_view_raw=Zobrazit v surovém stavu file_view_raw=Zobrazit v surovém stavu
file_permalink=Trvalý odkaz file_permalink=Trvalý odkaz
file_too_large=Soubor je příliš velký pro zobrazení. file_too_large=Soubor je příliš velký pro zobrazení.
file_is_empty=Soubor je prázdný.
code_preview_line_from_to=Řádky %[1]d do%[2]d v %[3]s
code_preview_line_in=Řádek %[1]d v %[2]s
invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode` invisible_runes_header=`Tento soubor obsahuje neviditelné znaky Unicode`
invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.` invisible_runes_description=`Tento soubor obsahuje neviditelné Unicode znaky, které jsou pro člověka nerozeznatelné, ale mohou být zpracovány jiným způsobem. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.`
ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode` ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode`
@ -1282,6 +1295,7 @@ editor.or=nebo
editor.cancel_lower=Zrušit editor.cancel_lower=Zrušit
editor.commit_signed_changes=Odevzdat podepsané změny editor.commit_signed_changes=Odevzdat podepsané změny
editor.commit_changes=Odevzdat změny editor.commit_changes=Odevzdat změny
editor.add_tmpl=Přidán „{nazev_souboru}“
editor.add=Přidat %s editor.add=Přidat %s
editor.update=Aktualizovat %s editor.update=Aktualizovat %s
editor.delete=Odstranit %s editor.delete=Odstranit %s
@ -1310,6 +1324,7 @@ editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není
editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání. editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání.
editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit. editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi začal/a s úpravami. Odevzdat do záplatové větve a poté sloučit.
editor.push_out_of_date=Nahrání se zdá být zastaralé.
editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_header=Odevzdat prázdný soubor
editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat? editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat?
editor.no_changes_to_show=Žádné změny k zobrazení. editor.no_changes_to_show=Žádné změny k zobrazení.
@ -1364,6 +1379,7 @@ commitstatus.success=Úspěch
ext_issues=Přístup k externím úkolům ext_issues=Přístup k externím úkolům
ext_issues.desc=Odkaz na externí systém úkolů. ext_issues.desc=Odkaz na externí systém úkolů.
projects.desc=Spravujte úkoly a pull requesty v projektech.
projects.description=Popis (volitelné) projects.description=Popis (volitelné)
projects.description_placeholder=Popis projects.description_placeholder=Popis
projects.create=Vytvořit projekt projects.create=Vytvořit projekt
@ -1391,6 +1407,7 @@ projects.column.new=Nový sloupec
projects.column.set_default=Nastavit jako výchozí projects.column.set_default=Nastavit jako výchozí
projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekategorizované úkoly a požadavky na natažení
projects.column.delete=Smazat sloupec projects.column.delete=Smazat sloupec
projects.column.deletion_desc=Smazání sloupce projektu přesune všechny související úkoly do výchozího sloupce. Pokračovat?
projects.column.color=Barva projects.column.color=Barva
projects.open=Otevřít projects.open=Otevřít
projects.close=Zavřít projects.close=Zavřít
@ -1426,6 +1443,7 @@ issues.new.clear_assignees=Smazat zpracovatele
issues.new.no_assignees=Bez zpracovatelů issues.new.no_assignees=Bez zpracovatelů
issues.new.no_reviewers=Žádní posuzovatelé issues.new.no_reviewers=Žádní posuzovatelé
issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.new.blocked_user=Nemůžete vytvořit úkol, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
issues.edit.already_changed=Nelze uložit změny v úkolu. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste ji znovu problém upravit, abyste se vyhnuli přepsání jejich změn
issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.edit.blocked_user=Nemůžete upravovat obsah, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
issues.choose.get_started=Začínáme issues.choose.get_started=Začínáme
issues.choose.open_external_link=Otevřít issues.choose.open_external_link=Otevřít
@ -1433,7 +1451,7 @@ issues.choose.blank=Výchozí
issues.choose.blank_about=Vytvořit úkol z výchozí šablony. issues.choose.blank_about=Vytvořit úkol z výchozí šablony.
issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány
issues.choose.invalid_templates=%v nalezených neplatných šablon issues.choose.invalid_templates=%v nalezených neplatných šablon
issues.choose.invalid_config=Nastavení problému obsahuje chyby: issues.choose.invalid_config=Nastavení úkolu obsahuje chyby:
issues.no_ref=Není určena žádná větev/značka issues.no_ref=Není určena žádná větev/značka
issues.create=Vytvořit úkol issues.create=Vytvořit úkol
issues.new_label=Nový štítek issues.new_label=Nový štítek
@ -1534,10 +1552,12 @@ issues.context.reference_issue=Odkázat v novém úkolu
issues.context.edit=Upravit issues.context.edit=Upravit
issues.context.delete=Smazat issues.context.delete=Smazat
issues.no_content=K dispozici není žádný popis. issues.no_content=K dispozici není žádný popis.
issues.close=Zavřít problém issues.close=Zavřít úkol
issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s
issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s
issues.close_comment_issue=Okomentovat a zavřít
issues.reopen_issue=Znovuotevřít issues.reopen_issue=Znovuotevřít
issues.reopen_comment_issue=Znovu otevřít s komentářem
issues.create_comment=Okomentovat issues.create_comment=Okomentovat
issues.comment.blocked_user=Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře. issues.comment.blocked_user=Nemůžete vytvořit nebo upravovat komentář, protože jste zablokováni zadavatelem příspěvku nebo vlastníkem repozitáře.
issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -1598,7 +1618,7 @@ issues.attachment.open_tab=`Klikněte pro zobrazení „%s“ v nové záložce`
issues.attachment.download=`Klikněte pro stažení „%s“` issues.attachment.download=`Klikněte pro stažení „%s“`
issues.subscribe=Odebírat issues.subscribe=Odebírat
issues.unsubscribe=Zrušit odběr issues.unsubscribe=Zrušit odběr
issues.unpin_issue=Odepnout problém issues.unpin_issue=Odepnout úkol
issues.max_pinned=Nemůžete připnout další úkoly issues.max_pinned=Nemůžete připnout další úkoly
issues.pin_comment=připnuto %s issues.pin_comment=připnuto %s
issues.unpin_comment=odepnul/a tento %s issues.unpin_comment=odepnul/a tento %s
@ -1657,7 +1677,7 @@ issues.due_date_form=rrrr-mm-dd
issues.due_date_form_add=Přidat termín dokončení issues.due_date_form_add=Přidat termín dokončení
issues.due_date_form_edit=Upravit issues.due_date_form_edit=Upravit
issues.due_date_form_remove=Odstranit issues.due_date_form_remove=Odstranit
issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení problému. issues.due_date_not_writer=Potřebujete přístup k zápisu do tohoto repozitáře, abyste mohli aktualizovat datum dokončení úkolu.
issues.due_date_not_set=Žádný termín dokončení. issues.due_date_not_set=Žádný termín dokončení.
issues.due_date_added=přidal/a termín dokončení %s %s issues.due_date_added=přidal/a termín dokončení %s %s
issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s issues.due_date_modified=upravil/a termín termínu z %[2]s na %[1]s %[3]s
@ -1739,6 +1759,7 @@ compare.compare_head=porovnat
pulls.desc=Povolit pull requesty a posuzování kódu. pulls.desc=Povolit pull requesty a posuzování kódu.
pulls.new=Nový pull request pulls.new=Nový pull request
pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře. pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře.
pulls.edit.already_changed=Nelze uložit změny v pull requestu. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn
pulls.view=Zobrazit pull request pulls.view=Zobrazit pull request
pulls.compare_changes=Nový pull request pulls.compare_changes=Nový pull request
pulls.allow_edits_from_maintainers=Povolit úpravy od správců pulls.allow_edits_from_maintainers=Povolit úpravy od správců
@ -1858,6 +1879,7 @@ pulls.close=Zavřít pull request
pulls.closed_at=`uzavřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.closed_at=`uzavřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at=`znovuotevřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`znovuotevřel/a tento pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.` pulls.cmd_instruction_hint=`Zobrazit <a class="show-instruction">instrukce příkazové řádky</a>.`
pulls.cmd_instruction_checkout_title=Checkout
pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
pulls.cmd_instruction_merge_title=Sloučit pulls.cmd_instruction_merge_title=Sloučit
pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea. pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea.
@ -1883,6 +1905,7 @@ pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %
pull.deleted_branch=(odstraněno):%s pull.deleted_branch=(odstraněno):%s
comments.edit.already_changed=Nelze uložit změny v komentáři. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn
milestones.new=Nový milník milestones.new=Nový milník
milestones.closed=Zavřen dne %s milestones.closed=Zavřen dne %s
@ -1959,6 +1982,7 @@ wiki.page_name_desc=Zadejte název této Wiki stránky. Některé speciální n
wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu. wiki.original_git_entry_tooltip=Zobrazit originální Git soubor namísto použití přátelského odkazu.
activity=Aktivita activity=Aktivita
activity.navbar.pulse=Pulz
activity.navbar.code_frequency=Frekvence kódu activity.navbar.code_frequency=Frekvence kódu
activity.navbar.contributors=Přispěvatelé activity.navbar.contributors=Přispěvatelé
activity.navbar.recent_commits=Nedávné commity activity.navbar.recent_commits=Nedávné commity
@ -2052,11 +2076,13 @@ settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Právě t
settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu. settings.mirror_settings.docs.disabled_push_mirror.info=Push zrcadla byla zakázána administrátorem vašeho webu.
settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla. settings.mirror_settings.docs.no_new_mirrors=Váš repozitář zrcadlí změny do nebo z jiného repozitáře. Mějte prosím na paměti, že v tuto chvíli nemůžete vytvořit žádná nová zrcadla.
settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo. settings.mirror_settings.docs.can_still_use=I když nemůžete upravit stávající zrcadla nebo vytvořit nová, stále můžete použít své stávající zrcadlo.
settings.mirror_settings.docs.pull_mirror_instructions=Chcete-li nastavit zrcadlo pro natažení, konzultujte prosím:
settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde: settings.mirror_settings.docs.more_information_if_disabled=Více informací o zrcadlech pro nahrání a natažení naleznete zde:
settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře? settings.mirror_settings.docs.doc_link_title=Jak mohu zrcadlit repozitáře?
settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci. settings.mirror_settings.docs.doc_link_pull_section=sekci "stahovat ze vzdáleného úložiště" v dokumentaci.
settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště settings.mirror_settings.docs.pulling_remote_title=Stažení ze vzdáleného úložiště
settings.mirror_settings.mirrored_repository=Zrcadlený repozitář settings.mirror_settings.mirrored_repository=Zrcadlený repozitář
settings.mirror_settings.pushed_repository=Odeslaný repozitář
settings.mirror_settings.direction=Směr settings.mirror_settings.direction=Směr
settings.mirror_settings.direction.pull=Natáhnout settings.mirror_settings.direction.pull=Natáhnout
settings.mirror_settings.direction.push=Nahrát settings.mirror_settings.direction.push=Nahrát
@ -2079,6 +2105,7 @@ settings.advanced_settings=Pokročilá nastavení
settings.wiki_desc=Povolit Wiki repozitáře settings.wiki_desc=Povolit Wiki repozitáře
settings.use_internal_wiki=Používat vestavěnou Wiki settings.use_internal_wiki=Používat vestavěnou Wiki
settings.default_wiki_branch_name=Výchozí název větve Wiki settings.default_wiki_branch_name=Výchozí název větve Wiki
settings.default_wiki_everyone_access=Výchozí přístupová práva pro přihlášené uživatele:
settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila. settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila.
settings.use_external_wiki=Používat externí Wiki settings.use_external_wiki=Používat externí Wiki
settings.external_wiki_url=URL externí Wiki settings.external_wiki_url=URL externí Wiki
@ -2760,6 +2787,7 @@ teams.invite.by=Pozvání od %s
teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže. teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
[admin] [admin]
maintenance=Údržba
dashboard=Přehled dashboard=Přehled
self_check=Samokontrola self_check=Samokontrola
identity_access=Identita a přístup identity_access=Identita a přístup
@ -2782,6 +2810,7 @@ settings=Nastavení správce
dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací. dashboard.new_version_hint=Gitea %s je nyní k dispozici, právě u vás běži %s. Podívej se na <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">blogu</a> pro více informací.
dashboard.statistic=Souhrn dashboard.statistic=Souhrn
dashboard.maintenance_operations=Operace údržby
dashboard.system_status=Status systému dashboard.system_status=Status systému
dashboard.operation_name=Název operace dashboard.operation_name=Název operace
dashboard.operation_switch=Přepnout dashboard.operation_switch=Přepnout
@ -3067,12 +3096,14 @@ auths.tips=Tipy
auths.tips.oauth2.general=Ověřování OAuth2 auths.tips.oauth2.general=Ověřování OAuth2
auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být: auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být:
auths.tip.oauth2_provider=Poskytovatel OAuth2 auths.tip.oauth2_provider=Poskytovatel OAuth2
auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na https://bitbucket.org/account/user/{vase-uzivatelske-jmeno}/oauth-consumers/new a přidejte oprávnění „Account“ - „Read“
auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“ auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“
auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps auths.tip.dropbox=Vytvořte novou aplikaci na https://www.dropbox.com/developers/apps
auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“ auths.tip.facebook=Registrujte novou aplikaci na https://developers.facebook.com/apps a přidejte produkt „Facebook Login“
auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new auths.tip.github=Registrujte novou OAuth aplikaci na https://github.com/settings/applications/new
auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications auths.tip.gitlab_new=Zaregistrujte novou aplikaci na https://gitlab.com/-/profile/applications
auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/ auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na https://console.developers.google.com/
auths.tip.openid_connect=Použijte OpenID Connect URL pro objevování spojení „https://{server}/.well-known/openid-configuration“ k nastavení koncových bodů
auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená
auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me
auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider
@ -3256,6 +3287,7 @@ monitor.queue.name=Název
monitor.queue.type=Typ monitor.queue.type=Typ
monitor.queue.exemplar=Typ vzoru monitor.queue.exemplar=Typ vzoru
monitor.queue.numberworkers=Počet workerů monitor.queue.numberworkers=Počet workerů
monitor.queue.activeworkers=Aktivní workery
monitor.queue.maxnumberworkers=Maximální počet workerů monitor.queue.maxnumberworkers=Maximální počet workerů
monitor.queue.numberinqueue=Číslo ve frontě monitor.queue.numberinqueue=Číslo ve frontě
monitor.queue.review_add=Posoudit / přidat workery monitor.queue.review_add=Posoudit / přidat workery
@ -3285,11 +3317,13 @@ notices.op=Akce
notices.delete_success=Systémové upozornění bylo smazáno. notices.delete_success=Systémové upozornění bylo smazáno.
self_check.no_problem_found=Zatím nebyl nalezen žádný problém. self_check.no_problem_found=Zatím nebyl nalezen žádný problém.
self_check.startup_warnings=Upozornění při spuštění:
self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s self_check.database_collation_mismatch=Očekávejte, že databáze použije collation: %s
self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání. self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Gitea může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání.
self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy. self_check.database_inconsistent_collation_columns=Databáze používá collation %s, ale tyto sloupce používají chybné collation. To může způsobit neočekávané problémy.
self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně. self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít příkaz "gitea doctor convert", který opraví problémy s collation, nebo můžete také problém vyřešit příkazem "ALTER ... COLLATE ..." SQL ručně.
self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně. self_check.database_fix_mssql=Uživatelé MSSQL mohou problém vyřešit pouze pomocí příkazu "ALTER ... COLLATE ..." SQL ručně.
self_check.location_origin_mismatch=Aktuální URL (%[1]s) se neshoduje s URL viditelnou pro Gitea (%[2]s). Pokud používáte reverzní proxy, ujistěte se, že hlavičky „Host“ a „X-Forwarded-Proto“ jsou nastaveny správně.
[action] [action]
create_repo=vytvořil/a repozitář <a href="%s">%s</a> create_repo=vytvořil/a repozitář <a href="%s">%s</a>
@ -3301,7 +3335,7 @@ reopen_issue=`znovuotevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
create_pull_request=`vytvořil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` create_pull_request=`vytvořil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
close_pull_request=`uzavřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` close_pull_request=`uzavřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
reopen_pull_request=`znovuotevřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` reopen_pull_request=`znovuotevřel/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
comment_issue=`okomentoval/a problém <a href="%[1]s">%[3]s#%[2]s</a>` comment_issue=`okomentoval/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
comment_pull=`okomentoval/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` comment_pull=`okomentoval/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
merge_pull_request=`sloučil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>` merge_pull_request=`sloučil/a pull request <a href="%[1]s">%[3]s#%[2]s</a>`
auto_merge_pull_request=`automaticky sloučen pull request <a href="%[1]s">%[3]s#%[2]s</a>` auto_merge_pull_request=`automaticky sloučen pull request <a href="%[1]s">%[3]s#%[2]s</a>`
@ -3317,6 +3351,7 @@ mirror_sync_create=synchronizoval/a novou referenci <a href="%[2]s">%[3]s</a> do
mirror_sync_delete=synchronizoval/a a smazal/a referenci <code>%[2]s</code> v <a href="%[1]s">%[3]s</a> ze zrcadla mirror_sync_delete=synchronizoval/a a smazal/a referenci <code>%[2]s</code> v <a href="%[1]s">%[3]s</a> ze zrcadla
approve_pull_request=`schválil/a <a href="%[1]s">%[3]s#%[2]s</a>` approve_pull_request=`schválil/a <a href="%[1]s">%[3]s#%[2]s</a>`
reject_pull_request=`navrhl/a změny pro <a href="%[1]s">%[3]s#%[2]s</a>` reject_pull_request=`navrhl/a změny pro <a href="%[1]s">%[3]s#%[2]s</a>`
publish_release=`vydal/a <a href="%[2]s"> "%[4]s" </a> v <a href="%[1]s">%[3]s</a>`
review_dismissed=`zamítl/a posouzení z <b>%[4]s</b> pro <a href="%[1]s">%[3]s#%[2]s</a>` review_dismissed=`zamítl/a posouzení z <b>%[4]s</b> pro <a href="%[1]s">%[3]s#%[2]s</a>`
review_dismissed_reason=Důvod: review_dismissed_reason=Důvod:
create_branch=vytvořil/a větev <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a> create_branch=vytvořil/a větev <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a>
@ -3383,6 +3418,7 @@ error.unit_not_allowed=Nejste oprávněni přistupovat k této části repozitá
title=Balíčky title=Balíčky
desc=Správa balíčků repozitáře. desc=Správa balíčků repozitáře.
empty=Zatím nejsou žádné balíčky. empty=Zatím nejsou žádné balíčky.
no_metadata=Žádná metadata.
empty.documentation=Další informace o registru balíčků naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>. empty.documentation=Další informace o registru balíčků naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na <a href="%[1]s">nastavení balíčku</a> a propojte jej s tímto repozitářem. empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na <a href="%[1]s">nastavení balíčku</a> a propojte jej s tímto repozitářem.
registry.documentation=Další informace o registru %s naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>. registry.documentation=Další informace o registru %s naleznete v <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>.
@ -3464,6 +3500,7 @@ npm.install=Pro instalaci balíčku pomocí npm spusťte následující příkaz
npm.install2=nebo ho přidejte do souboru package.json: npm.install2=nebo ho přidejte do souboru package.json:
npm.dependencies=Závislosti npm.dependencies=Závislosti
npm.dependencies.development=Vývojové závislosti npm.dependencies.development=Vývojové závislosti
npm.dependencies.bundle=Vnitřní závislosti
npm.dependencies.peer=Vzájemné závislosti npm.dependencies.peer=Vzájemné závislosti
npm.dependencies.optional=Volitelné závislosti npm.dependencies.optional=Volitelné závislosti
npm.details.tag=Značka npm.details.tag=Značka
@ -3560,6 +3597,8 @@ status.cancelled=Zrušeno
status.skipped=Přeskočeno status.skipped=Přeskočeno
status.blocked=Blokováno status.blocked=Blokováno
runners=Runnery
runners.runner_manage_panel=Správa runnerů
runners.new=Vytvořit nový runner runners.new=Vytvořit nový runner
runners.new_notice=Jak spustit runner runners.new_notice=Jak spustit runner
runners.status=Status runners.status=Status
@ -3586,6 +3625,7 @@ runners.delete_runner_success=Runner byl úspěšně odstraněn
runners.delete_runner_failed=Odstranění runneru selhalo runners.delete_runner_failed=Odstranění runneru selhalo
runners.delete_runner_header=Potvrdit odstranění tohoto runneru runners.delete_runner_header=Potvrdit odstranění tohoto runneru
runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu. runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření pracovního postupu.
runners.none=Žádné runnery nejsou k dispozici
runners.status.unspecified=Neznámý runners.status.unspecified=Neznámý
runners.status.idle=Nečinný runners.status.idle=Nečinný
runners.status.active=Aktivní runners.status.active=Aktivní
@ -3601,6 +3641,7 @@ runs.pushed_by=náhrán
runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s
runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s
runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí. runs.no_job_without_needs=Pracovní postup musí obsahovat alespoň jednu úlohu bez závislostí.
runs.no_job=Pracovní postup musí obsahovat alespoň jednu úlohu
runs.actor=Aktér runs.actor=Aktér
runs.status=Status runs.status=Status
runs.actors_no_select=Všichni aktéři runs.actors_no_select=Všichni aktéři

View File

@ -93,6 +93,7 @@ remove_all = Remove All
remove_label_str = Remove item "%s" remove_label_str = Remove item "%s"
edit = Edit edit = Edit
view = View view = View
test = Test
enabled = Enabled enabled = Enabled
disabled = Disabled disabled = Disabled
@ -1238,6 +1239,7 @@ file_view_rendered = View Rendered
file_view_raw = View Raw file_view_raw = View Raw
file_permalink = Permalink file_permalink = Permalink
file_too_large = The file is too large to be shown. file_too_large = The file is too large to be shown.
file_is_empty = The file is empty.
code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
code_preview_line_in = Line %[1]d in %[2]s code_preview_line_in = Line %[1]d in %[2]s
invisible_runes_header = `This file contains invisible Unicode characters` invisible_runes_header = `This file contains invisible Unicode characters`
@ -3224,6 +3226,10 @@ config.cache_adapter = Cache Adapter
config.cache_interval = Cache Interval config.cache_interval = Cache Interval
config.cache_conn = Cache Connection config.cache_conn = Cache Connection
config.cache_item_ttl = Cache Item TTL config.cache_item_ttl = Cache Item TTL
config.cache_test = Test Cache
config.cache_test_failed = Failed to probe the cache: %v.
config.cache_test_slow = Cache test successful, but response is slow: %s.
config.cache_test_succeeded = Cache test successful, got a response in %s.
config.session_config = Session Configuration config.session_config = Session Configuration
config.session_provider = Session Provider config.session_provider = Session Provider

View File

@ -25,6 +25,7 @@ enable_javascript=Ce site Web nécessite JavaScript.
toc=Sommaire toc=Sommaire
licenses=Licences licenses=Licences
return_to_gitea=Revenir à Gitea return_to_gitea=Revenir à Gitea
more_items=Plus d'éléments
username=Nom d'utilisateur username=Nom d'utilisateur
email=Courriel email=Courriel
@ -113,6 +114,7 @@ loading=Chargement…
error=Erreur error=Erreur
error404=La page que vous essayez d'atteindre <strong>n'existe pas</strong> ou <strong>vous n'êtes pas autorisé</strong> à la voir. error404=La page que vous essayez d'atteindre <strong>n'existe pas</strong> ou <strong>vous n'êtes pas autorisé</strong> à la voir.
go_back=Retour go_back=Retour
invalid_data=Données invalides : %v
never=Jamais never=Jamais
unknown=Inconnu unknown=Inconnu
@ -143,17 +145,43 @@ name=Nom
value=Valeur value=Valeur
filter=Filtrer filter=Filtrer
filter.clear=Effacer le filtre
filter.is_archived=Archivé filter.is_archived=Archivé
filter.not_archived=Non archivé
filter.is_fork=Bifurqué
filter.not_fork=Non bifurqué
filter.is_mirror=Miroité
filter.not_mirror=Non miroité
filter.is_template=Modèle filter.is_template=Modèle
filter.not_template=Pas un modèle
filter.public=Public filter.public=Public
filter.private=Privé filter.private=Privé
no_results_found=Aucun résultat trouvé.
[search] [search]
search=Rechercher…
type_tooltip=Type de recherche
fuzzy=Approximative
fuzzy_tooltip=Inclure également les résultats proches de la recherche
exact=Exact exact=Exact
exact_tooltip=Inclure uniquement les résultats qui correspondent exactement au terme de recherche exact_tooltip=Inclure uniquement les résultats qui correspondent exactement au terme de recherche
repo_kind=Chercher des dépôts…
user_kind=Chercher des utilisateurs…
org_kind=Chercher des organisations…
team_kind=Chercher des équipes…
code_kind=Chercher du code…
code_search_unavailable=La recherche dans le code nest pas disponible actuellement. Veuillez contacter ladministrateur de votre instance Gitea.
code_search_by_git_grep=Les résultats de recherche de code actuels sont fournis par « git grep ». Ladministrateur peut activer lindexeur de dépôt, qui pourrait fournir de meilleurs résultats.
package_kind=Chercher des paquets…
project_kind=Chercher des projets…
branch_kind=Chercher des branches…
commit_kind=Chercher des révisions…
runner_kind=Chercher des exécuteurs…
no_results=Aucun résultat correspondant trouvé.
issue_kind=Recherche de tickets… issue_kind=Recherche de tickets…
pull_kind=Recherche de demandes dajouts… pull_kind=Recherche de demandes dajouts…
keyword_search_unavailable=La recherche par mot clé nest pas disponible actuellement. Veuillez contacter ladministrateur de votre instance Gitea.
[aria] [aria]
navbar=Barre de navigation navbar=Barre de navigation
@ -260,6 +288,7 @@ email_title=Paramètres de Messagerie
smtp_addr=Hôte SMTP smtp_addr=Hôte SMTP
smtp_port=Port SMTP smtp_port=Port SMTP
smtp_from=Envoyer les courriels en tant que smtp_from=Envoyer les courriels en tant que
smtp_from_invalid=Ladresse « Envoyer le courriel sous » est invalide
smtp_from_helper=Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom <email@example.com> ». smtp_from_helper=Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom <email@example.com> ».
mailer_user=Utilisateur SMTP mailer_user=Utilisateur SMTP
mailer_password=Mot de passe SMTP mailer_password=Mot de passe SMTP
@ -319,6 +348,7 @@ env_config_keys=Configuration de l'environnement
env_config_keys_prompt=Les variables d'environnement suivantes seront également ajoutées à votre fichier de configuration : env_config_keys_prompt=Les variables d'environnement suivantes seront également ajoutées à votre fichier de configuration :
[home] [home]
nav_menu=Menu de navigation
uname_holder=Nom dutilisateur ou adresse courriel uname_holder=Nom dutilisateur ou adresse courriel
password_holder=Mot de passe password_holder=Mot de passe
switch_dashboard_context=Basculer le contexte du tableau de bord switch_dashboard_context=Basculer le contexte du tableau de bord
@ -333,14 +363,14 @@ filter_by_team_repositories=Dépôts filtrés par équipe
feed_of=Flux de « %s » feed_of=Flux de « %s »
show_archived=Archivé show_archived=Archivé
show_both_archived_unarchived=Afficher à la fois archivé et non archivé show_both_archived_unarchived=Afficher à la fois les dépôts archivés et non archivés
show_only_archived=Afficher uniquement les archivés show_only_archived=Afficher uniquement les dépôts archivés
show_only_unarchived=Afficher uniquement les non archivés show_only_unarchived=Afficher uniquement les dépôts non archivés
show_private=Privé show_private=Privé
show_both_private_public=Afficher les publics et privés show_both_private_public=Afficher les dépôts publics et privés
show_only_private=Afficher uniquement les privés show_only_private=Afficher uniquement les dépôts privés
show_only_public=Afficher uniquement les publics show_only_public=Afficher uniquement les dépôts publics
issues.in_your_repos=Dans vos dépôts issues.in_your_repos=Dans vos dépôts
@ -367,6 +397,7 @@ forgot_password_title=Mot de passe oublié
forgot_password=Mot de passe oublié ? forgot_password=Mot de passe oublié ?
sign_up_now=Pas de compte ? Inscrivez-vous maintenant. sign_up_now=Pas de compte ? Inscrivez-vous maintenant.
sign_up_successful=Le compte a été créé avec succès. Bienvenue ! sign_up_successful=Le compte a été créé avec succès. Bienvenue !
confirmation_mail_sent_prompt_ex=Un nouveau courriel de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans la prochaine %s pour terminer le processus dinscription. Si votre adresse courriel est incorrecte, vous pouvez vous reconnecter et la modifier.
must_change_password=Réinitialisez votre mot de passe must_change_password=Réinitialisez votre mot de passe
allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé) allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé)
reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour terminer la procédure de récupération du compte. reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans les prochaines %s pour terminer la procédure de récupération du compte.
@ -376,6 +407,7 @@ prohibit_login=Connexion interdite
prohibit_login_desc=Votre compte n'autorise pas la connexion, veuillez contacter l'administrateur de votre site. prohibit_login_desc=Votre compte n'autorise pas la connexion, veuillez contacter l'administrateur de votre site.
resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d'activation. Veuillez réessayer dans 3 minutes. resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d'activation. Veuillez réessayer dans 3 minutes.
has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) na pas été confirmée. Si vous navez reçu aucun mail de confirmation ou souhaitez renouveler lenvoi, cliquez sur le bouton ci-dessous. has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) na pas été confirmée. Si vous navez reçu aucun mail de confirmation ou souhaitez renouveler lenvoi, cliquez sur le bouton ci-dessous.
change_unconfirmed_mail_address=Si votre adresse courriel dinscription est incorrecte, vous pouvez la modifier ici et renvoyer un nouvel courriel de confirmation.
resend_mail=Cliquez ici pour renvoyer un mail de confirmation resend_mail=Cliquez ici pour renvoyer un mail de confirmation
email_not_associate=Ladresse courriel nest associée à aucun compte. email_not_associate=Ladresse courriel nest associée à aucun compte.
send_reset_mail=Envoyer un courriel de récupération du compte send_reset_mail=Envoyer un courriel de récupération du compte
@ -404,6 +436,7 @@ oauth_signin_submit=Lier un compte
oauth.signin.error=Une erreur s'est produite lors du traitement de la demande d'autorisation. Si cette erreur persiste, veuillez contacter l'administrateur du site. oauth.signin.error=Une erreur s'est produite lors du traitement de la demande d'autorisation. Si cette erreur persiste, veuillez contacter l'administrateur du site.
oauth.signin.error.access_denied=La demande d'autorisation a été refusée. oauth.signin.error.access_denied=La demande d'autorisation a été refusée.
oauth.signin.error.temporarily_unavailable=L'autorisation a échoué car le serveur d'authentification est temporairement indisponible. Veuillez réessayer plus tard. oauth.signin.error.temporarily_unavailable=L'autorisation a échoué car le serveur d'authentification est temporairement indisponible. Veuillez réessayer plus tard.
oauth_callback_unable_auto_reg=Linscription automatique est activée, mais le fournisseur OAuth2 %[1]s a signalé des champs manquants : %[2]s, impossible de créer un compte automatiquement, veuillez créer ou lier un compte, ou bien contacter ladministrateur du site.
openid_connect_submit=Se connecter openid_connect_submit=Se connecter
openid_connect_title=Se connecter à un compte existant openid_connect_title=Se connecter à un compte existant
openid_connect_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici. openid_connect_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici.
@ -556,6 +589,7 @@ team_name_been_taken=Le nom d'équipe est déjà pris.
team_no_units_error=Autoriser laccès à au moins une section du dépôt. team_no_units_error=Autoriser laccès à au moins une section du dépôt.
email_been_used=Cette adresse courriel est déjà utilisée. email_been_used=Cette adresse courriel est déjà utilisée.
email_invalid=Cette adresse courriel est invalide. email_invalid=Cette adresse courriel est invalide.
email_domain_is_not_allowed=Le domaine <b>%s</b> du courriel utilisateur entre en conflit avec EMAIL_DOMAIN_ALLOWLIST ou EMAIL_DOMAIN_BLOCKLIST. Veuillez vous assurer que votre opération est attendue.
openid_been_used=Adresse OpenID "%s" déjà utilisée. openid_been_used=Adresse OpenID "%s" déjà utilisée.
username_password_incorrect=Identifiant ou mot de passe invalide. username_password_incorrect=Identifiant ou mot de passe invalide.
password_complexity=Le mot de passe ne respecte pas les exigences de complexité: password_complexity=Le mot de passe ne respecte pas les exigences de complexité:
@ -567,6 +601,8 @@ enterred_invalid_repo_name=Le nom de dépôt saisi est incorrect.
enterred_invalid_org_name=Le nom de l'organisation que vous avez entré est incorrect. enterred_invalid_org_name=Le nom de l'organisation que vous avez entré est incorrect.
enterred_invalid_owner_name=Le nom du nouveau propriétaire est invalide. enterred_invalid_owner_name=Le nom du nouveau propriétaire est invalide.
enterred_invalid_password=Le mot de passe saisi est incorrect. enterred_invalid_password=Le mot de passe saisi est incorrect.
unset_password=Lutilisateur na pas défini de mot de passe.
unsupported_login_type=Le type de connexion nest pas pris en charge pour supprimer le compte.
user_not_exist=Cet utilisateur n'existe pas. user_not_exist=Cet utilisateur n'existe pas.
team_not_exist=L'équipe n'existe pas. team_not_exist=L'équipe n'existe pas.
last_org_owner=Vous ne pouvez pas retirer le dernier utilisateur de léquipe « propriétaires ». Il doit y avoir au moins un propriétaire dans chaque organisation. last_org_owner=Vous ne pouvez pas retirer le dernier utilisateur de léquipe « propriétaires ». Il doit y avoir au moins un propriétaire dans chaque organisation.
@ -616,6 +652,29 @@ form.name_reserved=Le nom dutilisateur "%s" est réservé.
form.name_pattern_not_allowed=Le motif « %s » nest pas autorisé dans un nom de d'utilisateur. form.name_pattern_not_allowed=Le motif « %s » nest pas autorisé dans un nom de d'utilisateur.
form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides. form.name_chars_not_allowed=Le nom d'utilisateur "%s" contient des caractères non valides.
block.block=Bloquer
block.block.user=Bloquer lutilisateur
block.block.org=Bloquer lutilisateur pour lorganisation
block.block.failure=Impossible de bloquer lutilisateur : %s
block.unblock=Débloquer
block.unblock.failure=Impossible de débloquer lutilisateur : %s
block.blocked=Vous avez bloqué cet utilisateur.
block.title=Bloquer un utilisateur
block.info=Bloquer un utilisateur lempêche dinteragir avec des dépôts, comme ouvrir ou commenter des demandes de fusion ou des tickets. Apprenez-en plus sur le blocage dun utilisateur.
block.info_1=Bloquer un utilisateur empêche les actions suivantes sur votre compte et vos dépôts :
block.info_2=suivre votre compte
block.info_3=vous envoyer des notifications en vous @mentionnant
block.info_4=vous inviter en tant que collaborateur de son(ses) dépôt(s)
block.info_5=aimer, bifurquer ou suivre vos dépôts
block.info_6=ouvrir ou commenter vos tickets et demandes dajouts
block.info_7=réagir à vos commentaires dans les tickets ou les demandes dajout
block.user_to_block=Utilisateur à bloquer
block.note=Note
block.note.title=Note facultative :
block.note.info=La note nest pas visible par lutilisateur bloqué.
block.note.edit=Modifier la note
block.list=Utilisateurs bloqués
block.list.none=Vous navez bloqué aucun utilisateur.
[settings] [settings]
profile=Profil profile=Profil
@ -658,6 +717,7 @@ cancel=Annuler
language=Langue language=Langue
ui=Thème ui=Thème
hidden_comment_types=Catégories de commentaires masqués hidden_comment_types=Catégories de commentaires masqués
hidden_comment_types_description=Cochez les catégories suivantes pour masquer les commentaires correspondants des fils d'activité. Par exemple, « Label » cache les commentaires du genre « Cerise a attribué le label Bug il y a 2 heures. »
hidden_comment_types.ref_tooltip=Commentaires où ce ticket a été référencé sur un autre ticket, révision, etc. hidden_comment_types.ref_tooltip=Commentaires où ce ticket a été référencé sur un autre ticket, révision, etc.
hidden_comment_types.issue_ref_tooltip=Commentaires où lutilisateur change la branche/étiquette associée au ticket hidden_comment_types.issue_ref_tooltip=Commentaires où lutilisateur change la branche/étiquette associée au ticket
comment_type_group_reference=Référence comment_type_group_reference=Référence
@ -704,6 +764,8 @@ manage_themes=Sélectionner le thème par défaut
manage_openid=Gérer les adresses OpenID manage_openid=Gérer les adresses OpenID
email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web. email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web.
theme_desc=Ce sera votre thème par défaut sur le site. theme_desc=Ce sera votre thème par défaut sur le site.
theme_colorblindness_help=Support du thème daltonien
theme_colorblindness_prompt=Gitea fournit depuis peu des thèmes daltonien basé sur un spectre coloré réduit. Encore en développement, de futures améliorations devraient enrichir les fichiers de thèmes CSS.
primary=Principale primary=Principale
activated=Activé activated=Activé
requires_activation=Nécessite une activation requires_activation=Nécessite une activation
@ -953,7 +1015,9 @@ fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être m
fork_branch=Branche à cloner sur la bifurcation fork_branch=Branche à cloner sur la bifurcation
all_branches=Toutes les branches all_branches=Toutes les branches
fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il na pas de propriétaire valide. fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il na pas de propriétaire valide.
fork.blocked_user=Impossible de bifurquer le dépôt car vous êtes bloqué par son propriétaire.
use_template=Utiliser ce modèle use_template=Utiliser ce modèle
open_with_editor=Ouvrir avec %s
download_zip=Télécharger le ZIP download_zip=Télécharger le ZIP
download_tar=Télécharger le TAR.GZ download_tar=Télécharger le TAR.GZ
download_bundle=Télécharger le BUNDLE download_bundle=Télécharger le BUNDLE
@ -1006,6 +1070,7 @@ watchers=Observateurs
stargazers=Fans stargazers=Fans
stars_remove_warning=Ceci supprimera toutes les étoiles de ce dépôt. stars_remove_warning=Ceci supprimera toutes les étoiles de ce dépôt.
forks=Bifurcations forks=Bifurcations
stars=Favoris
reactions_more=et %d de plus reactions_more=et %d de plus
unit_disabled=L'administrateur du site a désactivé cette section du dépôt. unit_disabled=L'administrateur du site a désactivé cette section du dépôt.
language_other=Autre language_other=Autre
@ -1127,6 +1192,7 @@ watch=Suivre
unstar=Retirer des favoris unstar=Retirer des favoris
star=Ajouter aux favoris star=Ajouter aux favoris
fork=Bifurcation fork=Bifurcation
action.blocked_user=Impossible deffectuer cette action car vous êtes bloqué par le propriétaire du dépôt.
download_archive=Télécharger ce dépôt download_archive=Télécharger ce dépôt
more_operations=Plus d'opérations more_operations=Plus d'opérations
@ -1172,6 +1238,8 @@ file_view_rendered=Voir le rendu
file_view_raw=Voir le Raw file_view_raw=Voir le Raw
file_permalink=Lien permanent file_permalink=Lien permanent
file_too_large=Le fichier est trop gros pour être affiché. file_too_large=Le fichier est trop gros pour être affiché.
code_preview_line_from_to=Lignes %[1]d à %[2]d dans %[3]s
code_preview_line_in=Ligne %[1]d dans %[2]s
invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles.` invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles.`
invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles à l'œil nu, mais peuvent être traités différemment par un ordinateur. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.` invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles à l'œil nu, mais peuvent être traités différemment par un ordinateur. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.`
ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus.` ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus.`
@ -1226,6 +1294,7 @@ editor.or=ou
editor.cancel_lower=Annuler editor.cancel_lower=Annuler
editor.commit_signed_changes=Réviser les changements (signé) editor.commit_signed_changes=Réviser les changements (signé)
editor.commit_changes=Réviser les changements editor.commit_changes=Réviser les changements
editor.add_tmpl=Ajouter {filename}
editor.add=Ajouter %s editor.add=Ajouter %s
editor.update=Actualiser %s editor.update=Actualiser %s
editor.delete=Supprimer %s editor.delete=Supprimer %s
@ -1253,6 +1322,8 @@ editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s 
editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il nexiste plus dans ce dépôt. editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il nexiste plus dans ce dépôt.
editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser. editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser.
editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt. editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt.
editor.commit_id_not_matching=LID de la révision ne correspond pas à lID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez.
editor.push_out_of_date=Cet envoi semble être obsolète.
editor.commit_empty_file_header=Réviser un fichier vide editor.commit_empty_file_header=Réviser un fichier vide
editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ? editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ?
editor.no_changes_to_show=Il ny a aucune modification à afficher. editor.no_changes_to_show=Il ny a aucune modification à afficher.
@ -1277,6 +1348,7 @@ commits.commits=Révisions
commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents. commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.
commits.nothing_to_compare=Ces branches sont égales. commits.nothing_to_compare=Ces branches sont égales.
commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13". commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13".
commits.search_branch=Cette branche
commits.search_all=Toutes les branches commits.search_all=Toutes les branches
commits.author=Auteur commits.author=Auteur
commits.message=Message commits.message=Message
@ -1306,6 +1378,7 @@ commitstatus.success=Succès
ext_issues=Accès aux tickets externes ext_issues=Accès aux tickets externes
ext_issues.desc=Lien vers un gestionnaire de tickets externe. ext_issues.desc=Lien vers un gestionnaire de tickets externe.
projects.desc=Gérer les tickets et les demandes dajouts dans les projets.
projects.description=Description (facultative) projects.description=Description (facultative)
projects.description_placeholder=Description projects.description_placeholder=Description
projects.create=Créer un projet projects.create=Créer un projet
@ -1333,6 +1406,7 @@ projects.column.new=Nouvelle colonne
projects.column.set_default=Définir par défaut projects.column.set_default=Définir par défaut
projects.column.set_default_desc=Les tickets et demandes dajout non-catégorisés seront placés dans cette colonne. projects.column.set_default_desc=Les tickets et demandes dajout non-catégorisés seront placés dans cette colonne.
projects.column.delete=Supprimer la colonne projects.column.delete=Supprimer la colonne
projects.column.deletion_desc=La suppression dune colonne déplace tous ses tickets dans la colonne par défaut. Continuer ?
projects.column.color=Couleur projects.column.color=Couleur
projects.open=Ouvrir projects.open=Ouvrir
projects.close=Fermer projects.close=Fermer
@ -1367,6 +1441,9 @@ issues.new.assignees=Assignés
issues.new.clear_assignees=Supprimer les affectations issues.new.clear_assignees=Supprimer les affectations
issues.new.no_assignees=Sans assignation issues.new.no_assignees=Sans assignation
issues.new.no_reviewers=Sans évaluateur issues.new.no_reviewers=Sans évaluateur
issues.new.blocked_user=Impossible de créer un ticket car vous êtes bloqué par le propriétaire du dépôt.
issues.edit.already_changed=Impossible denregistrer le ticket. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer pour éviter décraser ses modifications.
issues.edit.blocked_user=Impossible de modifier ce contenu car vous êtes bloqué par son propriétaire.
issues.choose.get_started=Démarrons issues.choose.get_started=Démarrons
issues.choose.open_external_link=Ouvrir issues.choose.open_external_link=Ouvrir
issues.choose.blank=Par défaut issues.choose.blank=Par défaut
@ -1477,8 +1554,11 @@ issues.no_content=Sans contenu.
issues.close=Fermer le ticket issues.close=Fermer le ticket
issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s
issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s
issues.close_comment_issue=Commenter et Fermer
issues.reopen_issue=Rouvrir issues.reopen_issue=Rouvrir
issues.reopen_comment_issue=Commenter et Réouvrir
issues.create_comment=Commenter issues.create_comment=Commenter
issues.comment.blocked_user=Impossible créer ou de modifier un commentaire car vous êtes bloqué par le propriétaire du dépôt.
issues.closed_at=`a fermé ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.` issues.closed_at=`a fermé ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
issues.reopened_at=`a réouvert ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.` issues.reopened_at=`a réouvert ce ticket <a id="%[1]s" href="#%[1]s">%[2]s</a>.`
issues.commit_ref_at=`a référencé ce ticket depuis une révision <a id="%[1]s" href="#%[1]s"> %[2]s</a>.` issues.commit_ref_at=`a référencé ce ticket depuis une révision <a id="%[1]s" href="#%[1]s"> %[2]s</a>.`
@ -1677,6 +1757,8 @@ compare.compare_head=comparer
pulls.desc=Active les demandes dajouts et lévaluation du code. pulls.desc=Active les demandes dajouts et lévaluation du code.
pulls.new=Nouvelle demande d'ajout pulls.new=Nouvelle demande d'ajout
pulls.new.blocked_user=Impossible de créer une demande dajout car vous êtes bloqué par le propriétaire du dépôt.
pulls.edit.already_changed=Impossible denregistrer la demande dajout. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer afin déviter décraser leurs modifications.
pulls.view=Voir la demande d'ajout pulls.view=Voir la demande d'ajout
pulls.compare_changes=Nouvelle demande dajout pulls.compare_changes=Nouvelle demande dajout
pulls.allow_edits_from_maintainers=Autoriser les modifications des mainteneurs pulls.allow_edits_from_maintainers=Autoriser les modifications des mainteneurs
@ -1822,6 +1904,7 @@ pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s
pull.deleted_branch=(supprimé) : %s pull.deleted_branch=(supprimé) : %s
comments.edit.already_changed=Impossible denregistrer ce commentaire. Il semble que le contenu ait été modifié par un autre utilisateur. Veuillez rafraîchir la page et réessayer afin déviter décraser leurs modifications.
milestones.new=Nouveau jalon milestones.new=Nouveau jalon
milestones.closed=%s fermé milestones.closed=%s fermé
@ -1898,7 +1981,10 @@ wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux
wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial. wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial.
activity=Activité activity=Activité
activity.navbar.pulse=Impulsion
activity.navbar.code_frequency=Fréquence du code
activity.navbar.contributors=Contributeurs activity.navbar.contributors=Contributeurs
activity.navbar.recent_commits=Révisions récentes
activity.period.filter_label=Période : activity.period.filter_label=Période :
activity.period.daily=1 jour activity.period.daily=1 jour
activity.period.halfweekly=3 jours activity.period.halfweekly=3 jours
@ -2017,7 +2103,9 @@ settings.branches.add_new_rule=Ajouter une nouvelle règle
settings.advanced_settings=Paramètres avancés settings.advanced_settings=Paramètres avancés
settings.wiki_desc=Activer le wiki du dépôt settings.wiki_desc=Activer le wiki du dépôt
settings.use_internal_wiki=Utiliser le wiki interne settings.use_internal_wiki=Utiliser le wiki interne
settings.default_wiki_branch_name=Nom de la branche du Wiki par défaut
settings.default_wiki_everyone_access=Autorisation daccès par défaut pour les utilisateurs connectés : settings.default_wiki_everyone_access=Autorisation daccès par défaut pour les utilisateurs connectés :
settings.failed_to_change_default_wiki_branch=Impossible de modifier la branche du wiki par défaut.
settings.use_external_wiki=Utiliser un wiki externe settings.use_external_wiki=Utiliser un wiki externe
settings.external_wiki_url=URL Wiki externe settings.external_wiki_url=URL Wiki externe
settings.external_wiki_url_error=LURL du wiki externe nest pas une URL valide. settings.external_wiki_url_error=LURL du wiki externe nest pas une URL valide.
@ -2048,6 +2136,9 @@ settings.pulls.default_allow_edits_from_maintainers=Autoriser les modifications
settings.releases_desc=Activer les publications du dépôt settings.releases_desc=Activer les publications du dépôt
settings.packages_desc=Activer le registre des paquets du dépôt settings.packages_desc=Activer le registre des paquets du dépôt
settings.projects_desc=Activer les projets de dépôt settings.projects_desc=Activer les projets de dépôt
settings.projects_mode_desc=Mode Projets (type de projets à afficher)
settings.projects_mode_repo=Projets de dépôt uniquement
settings.projects_mode_owner=Projets dutilisateur ou dorganisation uniquement
settings.projects_mode_all=Tous les projets settings.projects_mode_all=Tous les projets
settings.actions_desc=Activer les actions du dépôt settings.actions_desc=Activer les actions du dépôt
settings.admin_settings=Paramètres administrateur settings.admin_settings=Paramètres administrateur
@ -2074,6 +2165,7 @@ settings.convert_fork_succeed=La bifurcation a été convertie en dépôt standa
settings.transfer=Changer de propriétaire settings.transfer=Changer de propriétaire
settings.transfer.rejected=Le transfert du dépôt a été rejeté. settings.transfer.rejected=Le transfert du dépôt a été rejeté.
settings.transfer.success=Le transfert du dépôt a réussi. settings.transfer.success=Le transfert du dépôt a réussi.
settings.transfer.blocked_user=Impossible de transférer ce dépôt car vous êtes bloqué par lacquéreur.
settings.transfer_abort=Annuler le transfert settings.transfer_abort=Annuler le transfert
settings.transfer_abort_invalid=Vous ne pouvez pas annuler un transfert de dépôt inexistant. settings.transfer_abort_invalid=Vous ne pouvez pas annuler un transfert de dépôt inexistant.
settings.transfer_abort_success=Le transfert du dépôt vers %s a bien été stoppé. settings.transfer_abort_success=Le transfert du dépôt vers %s a bien été stoppé.
@ -2119,6 +2211,7 @@ settings.add_collaborator_success=Le collaborateur a été ajouté.
settings.add_collaborator_inactive_user=Impossible d'ajouter un utilisateur inactif en tant que collaborateur. settings.add_collaborator_inactive_user=Impossible d'ajouter un utilisateur inactif en tant que collaborateur.
settings.add_collaborator_owner=Impossible d'ajouter un propriétaire en tant que collaborateur. settings.add_collaborator_owner=Impossible d'ajouter un propriétaire en tant que collaborateur.
settings.add_collaborator_duplicate=Le collaborateur est déjà ajouté à ce dépôt. settings.add_collaborator_duplicate=Le collaborateur est déjà ajouté à ce dépôt.
settings.add_collaborator.blocked_user=Ce collaborateur est bloqué par le propriétaire du dépôt ou inversement.
settings.delete_collaborator=Supprimer settings.delete_collaborator=Supprimer
settings.collaborator_deletion=Supprimer le collaborateur settings.collaborator_deletion=Supprimer le collaborateur
settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque son accès à ce dépôt. Continuer ? settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque son accès à ce dépôt. Continuer ?
@ -2557,13 +2650,16 @@ find_file.no_matching=Aucun fichier correspondant trouvé
error.csv.too_large=Impossible de visualiser le fichier car il est trop volumineux. error.csv.too_large=Impossible de visualiser le fichier car il est trop volumineux.
error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d. error.csv.unexpected=Impossible de visualiser ce fichier car il contient un caractère inattendu ligne %d, colonne %d.
error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d. error.csv.invalid_field_count=Impossible de visualiser ce fichier car il contient un nombre de champs incorrect à la ligne %d.
error.broken_git_hook=Les crochets Git de ce dépôt semblent cassés. Veuillez suivre la <a target="_blank" rel="noreferrer" href="%s">documentation</a> pour les corriger, puis pousser des révisions pour actualiser le statut.
[graphs] [graphs]
component_loading=Chargement de %s… component_loading=Chargement de %s…
component_loading_failed=Impossible de charger %s. component_loading_failed=Impossible de charger %s.
component_loading_info=Ça prend son temps… component_loading_info=Ça prend son temps…
component_failed_to_load=Une erreur inattendue sest produite. component_failed_to_load=Une erreur inattendue sest produite.
code_frequency.what=fréquence du code
contributors.what=contributions contributors.what=contributions
recent_commits.what=révisions récentes
[org] [org]
org_name_holder=Nom de l'organisation org_name_holder=Nom de l'organisation
@ -2677,6 +2773,7 @@ teams.add_nonexistent_repo=Le dépôt que vous essayez d'ajouter n'existe pas, v
teams.add_duplicate_users=Lutilisateur est déjà un membre de léquipe. teams.add_duplicate_users=Lutilisateur est déjà un membre de léquipe.
teams.repos.none=Aucun dépôt n'est accessible par cette équipe. teams.repos.none=Aucun dépôt n'est accessible par cette équipe.
teams.members.none=Aucun membre dans cette équipe. teams.members.none=Aucun membre dans cette équipe.
teams.members.blocked_user=Impossible dajouter lutilisateur car il est bloqué par lorganisation.
teams.specific_repositories=Dépôts spécifiques teams.specific_repositories=Dépôts spécifiques
teams.specific_repositories_helper=Les membres auront seulement accès aux dépôts explicitement ajoutés à l'équipe. Sélectionner ceci <strong>ne supprimera pas automatiquement</strong> les dépôts déjà ajoutés avec <i>Tous les dépôts</i>. teams.specific_repositories_helper=Les membres auront seulement accès aux dépôts explicitement ajoutés à l'équipe. Sélectionner ceci <strong>ne supprimera pas automatiquement</strong> les dépôts déjà ajoutés avec <i>Tous les dépôts</i>.
teams.all_repositories=Tous les dépôts teams.all_repositories=Tous les dépôts
@ -2689,6 +2786,7 @@ teams.invite.by=Invité par %s
teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre léquipe. teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre léquipe.
[admin] [admin]
maintenance=Maintenance
dashboard=Tableau de bord dashboard=Tableau de bord
self_check=Autodiagnostique self_check=Autodiagnostique
identity_access=Identité et accès identity_access=Identité et accès
@ -2712,6 +2810,7 @@ settings=Paramètres administrateur
dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails. dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">le blog</a> pour plus de détails.
dashboard.statistic=Résumé dashboard.statistic=Résumé
dashboard.maintenance_operations=Opérations de maintenance
dashboard.system_status=État du système dashboard.system_status=État du système
dashboard.operation_name=Nom de l'Opération dashboard.operation_name=Nom de l'Opération
dashboard.operation_switch=Basculer dashboard.operation_switch=Basculer
@ -2997,11 +3096,14 @@ auths.tips=Conseils
auths.tips.oauth2.general=Authentification OAuth2 auths.tips.oauth2.general=Authentification OAuth2
auths.tips.oauth2.general.tip=Lors de l'enregistrement d'une nouvelle authentification OAuth2, l'URL de rappel/redirection doit être : auths.tips.oauth2.general.tip=Lors de l'enregistrement d'une nouvelle authentification OAuth2, l'URL de rappel/redirection doit être :
auths.tip.oauth2_provider=Fournisseur OAuth2 auths.tip.oauth2_provider=Fournisseur OAuth2
auths.tip.bitbucket=Créez un nouveau jeton OAuth sur https://bitbucket.org/account/user/{your username}/oauth-consumers/new et ajoutez la permission “Compte” - “Lecture”.
auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instance en utilisant le menu "Paramètres -> Sécurité -> Client OAuth 2.0"` auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instance en utilisant le menu "Paramètres -> Sécurité -> Client OAuth 2.0"`
auths.tip.dropbox=Créez une nouvelle application sur https://www.dropbox.com/developers/apps auths.tip.dropbox=Créez une nouvelle application sur https://www.dropbox.com/developers/apps
auths.tip.facebook=`Enregistrez une nouvelle application sur https://developers.facebook.com/apps et ajoutez le produit "Facebook Login"` auths.tip.facebook=`Enregistrez une nouvelle application sur https://developers.facebook.com/apps et ajoutez le produit "Facebook Login"`
auths.tip.github=Créez une nouvelle application OAuth sur https://github.com/settings/applications/new auths.tip.github=Créez une nouvelle application OAuth sur https://github.com/settings/applications/new
auths.tip.gitlab_new=Enregistrez une nouvelle application sur https://gitlab.com/-/profile/applications
auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Google (https://console.developers.google.com/) auths.tip.google_plus=Obtenez des identifiants OAuth2 sur la console API de Google (https://console.developers.google.com/)
auths.tip.openid_connect=Utilisez lURL de découverte OpenID « https://{server}/.well-known/openid-configuration » pour spécifier les points d'accès.
auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée auths.tip.twitter=Rendez-vous sur https://dev.twitter.com/apps, créez une application et assurez-vous que l'option "Autoriser l'application à être utilisée avec Twitter Connect" est activée
auths.tip.discord=Enregistrer une nouvelle application sur https://discordapp.com/developers/applications/me auths.tip.discord=Enregistrer une nouvelle application sur https://discordapp.com/developers/applications/me
auths.tip.gitea=Enregistrez une nouvelle application OAuth2. Le guide peut être trouvé sur https://docs.gitea.com/development/oauth2-provider auths.tip.gitea=Enregistrez une nouvelle application OAuth2. Le guide peut être trouvé sur https://docs.gitea.com/development/oauth2-provider
@ -3135,6 +3237,7 @@ config.picture_config=Configuration de l'avatar
config.picture_service=Service d'Imagerie config.picture_service=Service d'Imagerie
config.disable_gravatar=Désactiver Gravatar config.disable_gravatar=Désactiver Gravatar
config.enable_federated_avatar=Activer les avatars unifiés config.enable_federated_avatar=Activer les avatars unifiés
config.open_with_editor_app_help=Les éditeurs disponibles via « Ouvrir avec ». Si laissé vide, la valeur par défaut sera utilisée. Développez pour voir la valeur par défaut.
config.git_config=Configuration de Git config.git_config=Configuration de Git
config.git_disable_diff_highlight=Désactiver la surbrillance syntaxique de Diff config.git_disable_diff_highlight=Désactiver la surbrillance syntaxique de Diff
@ -3214,11 +3317,13 @@ notices.op=Opération
notices.delete_success=Les informations systèmes ont été supprimées. notices.delete_success=Les informations systèmes ont été supprimées.
self_check.no_problem_found=Aucun problème trouvé pour linstant. self_check.no_problem_found=Aucun problème trouvé pour linstant.
self_check.startup_warnings=Avertissements au démarrage :
self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s. self_check.database_collation_mismatch=Exige que la base de données utilise la collation %s.
self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu. self_check.database_collation_case_insensitive=La base de données utilise la collation %s, insensible à la casse. Bien que Gitea soit compatible, il peut y avoir quelques rares cas qui ne fonctionnent pas comme prévu.
self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus. self_check.database_inconsistent_collation_columns=La base de données utilise la collation %s, mais ces colonnes utilisent des collations différentes. Cela peut causer des problèmes imprévus.
self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation. self_check.database_fix_mysql=Pour les utilisateurs de MySQL ou MariaDB, vous pouvez utiliser la commande « gitea doctor convert » dans un terminal ou exécuter une requête du type « ALTER … COLLATE ... » pour résoudre les problèmes de collation.
self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème quen exécutant une requête SQL du type « ALTER … COLLATE … ». self_check.database_fix_mssql=Pour les utilisateurs de MSSQL, vous ne pouvez résoudre le problème quen exécutant une requête SQL du type « ALTER … COLLATE … ».
self_check.location_origin_mismatch=LURL actuelle (%[1]s) ne correspond pas à lURL vue par Gitea (%[2]). Si vous utilisez un proxy inverse, assurez-vous que les en-têtes « Host » et « X-Forwarded-Proto » sont correctement définis.
[action] [action]
create_repo=a créé le dépôt <a href="%s">%s</a> create_repo=a créé le dépôt <a href="%s">%s</a>
@ -3246,6 +3351,7 @@ mirror_sync_create=a synchronisé la nouvelle référence <a href="%[2]s">%[3]s<
mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence <code>%[2]s</code> vers <a href="%[1]s">%[3]s</a> depuis le miroir mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence <code>%[2]s</code> vers <a href="%[1]s">%[3]s</a> depuis le miroir
approve_pull_request=`a approuvé <a href="%[1]s">%[3]s#%[2]s</a>` approve_pull_request=`a approuvé <a href="%[1]s">%[3]s#%[2]s</a>`
reject_pull_request=`a suggérés des changements pour <a href="%[1]s">%[3]s#%[2]s</a>` reject_pull_request=`a suggérés des changements pour <a href="%[1]s">%[3]s#%[2]s</a>`
publish_release=`a publié <a href="%[2]s"> "%[4]s" </a> dans <a href="%[1]s">%[3]s</a>`
review_dismissed=`a révoqué lévaluation de <b>%[4]s</b> dans <a href="%[1]s">%[3]s#%[2]s</a>` review_dismissed=`a révoqué lévaluation de <b>%[4]s</b> dans <a href="%[1]s">%[3]s#%[2]s</a>`
review_dismissed_reason=Raison : review_dismissed_reason=Raison :
create_branch=a créé la branche <a href="%[2]s">%[3]s</a> dans <a href="%[1]s">%[4]s</a> create_branch=a créé la branche <a href="%[2]s">%[3]s</a> dans <a href="%[1]s">%[4]s</a>
@ -3312,6 +3418,7 @@ error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section d
title=Paquets title=Paquets
desc=Gérer les paquets du dépôt. desc=Gérer les paquets du dépôt.
empty=Il n'y pas de paquet pour le moment. empty=Il n'y pas de paquet pour le moment.
no_metadata=Pas de métadonnées.
empty.documentation=Pour plus d'informations sur le registre de paquets, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>. empty.documentation=Pour plus d'informations sur le registre de paquets, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les <a href="%[1]s">paramètres du paquet</a> et liez le à ce dépôt. empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les <a href="%[1]s">paramètres du paquet</a> et liez le à ce dépôt.
registry.documentation=Pour plus dinformations sur le registre %s, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>. registry.documentation=Pour plus dinformations sur le registre %s, voir <a target="_blank" rel="noopener noreferrer" href="%s">la documentation</a>.
@ -3393,6 +3500,7 @@ npm.install=Pour installer le paquet en utilisant npm, exécutez la commande sui
npm.install2=ou ajoutez-le au fichier package.json : npm.install2=ou ajoutez-le au fichier package.json :
npm.dependencies=Dépendances npm.dependencies=Dépendances
npm.dependencies.development=Dépendances de développement npm.dependencies.development=Dépendances de développement
npm.dependencies.bundle=Dépendances emballées
npm.dependencies.peer=Dépendances de pairs npm.dependencies.peer=Dépendances de pairs
npm.dependencies.optional=Dépendances optionnelles npm.dependencies.optional=Dépendances optionnelles
npm.details.tag=Balise npm.details.tag=Balise
@ -3532,6 +3640,8 @@ runs.scheduled=Planifié
runs.pushed_by=soumis par runs.pushed_by=soumis par
runs.invalid_workflow_helper=La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s. runs.invalid_workflow_helper=La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s.
runs.no_matching_online_runner_helper=Aucun exécuteur en ligne correspondant au libellé %s runs.no_matching_online_runner_helper=Aucun exécuteur en ligne correspondant au libellé %s
runs.no_job_without_needs=Le flux de travail doit contenir au moins une tâche sans dépendance.
runs.no_job=Le flux de travail doit contenir au moins une tâche
runs.actor=Acteur runs.actor=Acteur
runs.status=Statut runs.status=Statut
runs.actors_no_select=Tous les acteurs runs.actors_no_select=Tous les acteurs

View File

@ -1238,6 +1238,7 @@ file_view_rendered=レンダリング表示
file_view_raw=Rawデータを見る file_view_raw=Rawデータを見る
file_permalink=パーマリンク file_permalink=パーマリンク
file_too_large=このファイルは大きすぎるため、表示できません。 file_too_large=このファイルは大きすぎるため、表示できません。
file_is_empty=ファイルは空です。
code_preview_line_from_to=%[1]d 行目から %[2]d 行目 in %[3]s code_preview_line_from_to=%[1]d 行目から %[2]d 行目 in %[3]s
code_preview_line_in=%[1]d 行目 in %[2]s code_preview_line_in=%[1]d 行目 in %[2]s
invisible_runes_header=このファイルには不可視のUnicode文字が含まれています invisible_runes_header=このファイルには不可視のUnicode文字が含まれています
@ -1378,6 +1379,7 @@ commitstatus.success=成功
ext_issues=外部イシューへのアクセス ext_issues=外部イシューへのアクセス
ext_issues.desc=外部のイシュートラッカーへのリンク。 ext_issues.desc=外部のイシュートラッカーへのリンク。
projects.desc=プロジェクトでイシューとプルリクエストを管理します。
projects.description=説明 (オプション) projects.description=説明 (オプション)
projects.description_placeholder=説明 projects.description_placeholder=説明
projects.create=プロジェクトを作成 projects.create=プロジェクトを作成
@ -1441,6 +1443,7 @@ issues.new.clear_assignees=担当者をクリア
issues.new.no_assignees=担当者なし issues.new.no_assignees=担当者なし
issues.new.no_reviewers=レビューアなし issues.new.no_reviewers=レビューアなし
issues.new.blocked_user=リポジトリのオーナーがあなたをブロックしているため、イシューを作成できません。 issues.new.blocked_user=リポジトリのオーナーがあなたをブロックしているため、イシューを作成できません。
issues.edit.already_changed=イシューの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください
issues.edit.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、内容を編集できません。 issues.edit.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、内容を編集できません。
issues.choose.get_started=始める issues.choose.get_started=始める
issues.choose.open_external_link=オープン issues.choose.open_external_link=オープン
@ -1552,7 +1555,9 @@ issues.no_content=説明はありません。
issues.close=イシューをクローズ issues.close=イシューをクローズ
issues.comment_pull_merged_at=がコミット %[1]s を %[2]s にマージ %[3]s issues.comment_pull_merged_at=がコミット %[1]s を %[2]s にマージ %[3]s
issues.comment_manually_pull_merged_at=がコミット %[1]s を %[2]s に手動マージ %[3]s issues.comment_manually_pull_merged_at=がコミット %[1]s を %[2]s に手動マージ %[3]s
issues.close_comment_issue=コメントしてクローズ
issues.reopen_issue=再オープンする issues.reopen_issue=再オープンする
issues.reopen_comment_issue=コメントして再オープン
issues.create_comment=コメントする issues.create_comment=コメントする
issues.comment.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。 issues.comment.blocked_user=投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。
issues.closed_at=`がイシューをクローズ <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`がイシューをクローズ <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -1754,6 +1759,7 @@ compare.compare_head=比較
pulls.desc=プルリクエストとコードレビューの有効化。 pulls.desc=プルリクエストとコードレビューの有効化。
pulls.new=新しいプルリクエスト pulls.new=新しいプルリクエスト
pulls.new.blocked_user=リポジトリのオーナーがあなたをブロックしているため、プルリクエストを作成できません。 pulls.new.blocked_user=リポジトリのオーナーがあなたをブロックしているため、プルリクエストを作成できません。
pulls.edit.already_changed=プルリクエストの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください
pulls.view=プルリクエストを表示 pulls.view=プルリクエストを表示
pulls.compare_changes=新規プルリクエスト pulls.compare_changes=新規プルリクエスト
pulls.allow_edits_from_maintainers=メンテナーからの編集を許可する pulls.allow_edits_from_maintainers=メンテナーからの編集を許可する
@ -1899,6 +1905,7 @@ pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1
pull.deleted_branch=(削除済み):%s pull.deleted_branch=(削除済み):%s
comments.edit.already_changed=コメントの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください
milestones.new=新しいマイルストーン milestones.new=新しいマイルストーン
milestones.closed=%s にクローズ milestones.closed=%s にクローズ
@ -3412,6 +3419,7 @@ error.unit_not_allowed=このセクションへのアクセスが許可されて
title=パッケージ title=パッケージ
desc=リポジトリ パッケージを管理します。 desc=リポジトリ パッケージを管理します。
empty=パッケージはまだありません。 empty=パッケージはまだありません。
no_metadata=メタデータがありません。
empty.documentation=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。 empty.documentation=パッケージレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。 empty.repo=パッケージはアップロード済みで、ここに表示されていないですか? <a href="%[1]s">パッケージ設定</a>を開いて、パッケージをこのリポジトリにリンクしてください。
registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。 registry.documentation=%sレジストリの詳細については、 <a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a> を参照してください。
@ -3634,6 +3642,7 @@ runs.pushed_by=pushed by
runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s runs.invalid_workflow_helper=ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s
runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s runs.no_matching_online_runner_helper=ラベルに一致するオンラインのランナーが見つかりません: %s
runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。 runs.no_job_without_needs=ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。
runs.no_job=ワークフローには少なくとも1つのジョブが含まれている必要があります
runs.actor=アクター runs.actor=アクター
runs.status=ステータス runs.status=ステータス
runs.actors_no_select=すべてのアクター runs.actors_no_select=すべてのアクター

View File

@ -1238,6 +1238,7 @@ file_view_rendered=Ver resultado processado
file_view_raw=Ver em bruto file_view_raw=Ver em bruto
file_permalink=Ligação permanente file_permalink=Ligação permanente
file_too_large=O ficheiro é demasiado grande para ser apresentado. file_too_large=O ficheiro é demasiado grande para ser apresentado.
file_is_empty=O ficheiro está vazio.
code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s code_preview_line_from_to=Linhas %[1]d até %[2]d em %[3]s
code_preview_line_in=Linha %[1]d em %[2]s code_preview_line_in=Linha %[1]d em %[2]s
invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis` invisible_runes_header=`Este ficheiro contém caracteres Unicode invisíveis`
@ -1554,7 +1555,9 @@ issues.no_content=Nenhuma descrição fornecida.
issues.close=Encerrar questão issues.close=Encerrar questão
issues.comment_pull_merged_at=cometimento %[1]s integrado em %[2]s %[3]s issues.comment_pull_merged_at=cometimento %[1]s integrado em %[2]s %[3]s
issues.comment_manually_pull_merged_at=cometimento %[1]s integrado manualmente em %[2]s %[3]s issues.comment_manually_pull_merged_at=cometimento %[1]s integrado manualmente em %[2]s %[3]s
issues.close_comment_issue=Fechar com comentário
issues.reopen_issue=Reabrir issues.reopen_issue=Reabrir
issues.reopen_comment_issue=Reabrir com comentário
issues.create_comment=Comentar issues.create_comment=Comentar
issues.comment.blocked_user=Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório. issues.comment.blocked_user=Não pode criar ou editar o comentário porque foi bloqueado/a pelo remetente ou pelo/a proprietário/a do repositório.
issues.closed_at=`encerrou esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`encerrou esta questão <a id="%[1]s" href="#%[1]s">%[2]s</a>`

58
package-lock.json generated
View File

@ -10,7 +10,7 @@
"@citation-js/plugin-csl": "0.7.11", "@citation-js/plugin-csl": "0.7.11",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3", "@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.1", "@github/relative-time-element": "4.4.2",
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
@ -68,7 +68,7 @@
"@stoplight/spectral-cli": "6.11.1", "@stoplight/spectral-cli": "6.11.1",
"@stylistic/eslint-plugin-js": "2.1.0", "@stylistic/eslint-plugin-js": "2.1.0",
"@stylistic/stylelint-plugin": "2.1.2", "@stylistic/stylelint-plugin": "2.1.2",
"@typescript-eslint/parser": "7.11.0", "@typescript-eslint/parser": "7.13.1",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
@ -1032,9 +1032,9 @@
"integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A==" "integrity": "sha512-AlquKGee+IWiAMYVB0xyHFZRMnu4n3X4HTvJHu79GiVJ1ojTukCWyxMlF5NMsecoLcBKsuBhx3QPv2vkE/zQ0A=="
}, },
"node_modules/@github/relative-time-element": { "node_modules/@github/relative-time-element": {
"version": "4.4.1", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.2.tgz",
"integrity": "sha512-E2vRcIgDj8AHv/iHpQMLJ/RqKOJ704OXkKw6+Zdhk3X+kVQhOf3Wj8KVz4DfCQ1eOJR8XxY6XVv73yd+pjMfXA==" "integrity": "sha512-wTXunu3hmuGljA5CHaaoUIKV0oI35wno0FKJl2yqKplTRnsCA5bPNj4bDeVIubkuskql6jwionWLlGM1Y6QLaw=="
}, },
"node_modules/@github/text-expander-element": { "node_modules/@github/text-expander-element": {
"version": "2.6.1", "version": "2.6.1",
@ -2400,15 +2400,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.11.0", "version": "7.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.13.1.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "integrity": "sha512-1ELDPlnLvDQ5ybTSrMhRTFDfOQEOXNM+eP+3HT/Yq7ruWpciQw+Avi73pdEbA4SooCawEWo3dtYbF68gN7Ed1A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/scope-manager": "7.13.1",
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.13.1",
"@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/typescript-estree": "7.13.1",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.13.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -2428,13 +2428,13 @@
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0", "version": "7.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.13.1.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "integrity": "sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.13.1",
"@typescript-eslint/visitor-keys": "7.11.0" "@typescript-eslint/visitor-keys": "7.13.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@ -2445,9 +2445,9 @@
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "7.11.0", "version": "7.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.13.1.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "integrity": "sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@ -2458,13 +2458,13 @@
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0", "version": "7.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.1.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "integrity": "sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.13.1",
"@typescript-eslint/visitor-keys": "7.11.0", "@typescript-eslint/visitor-keys": "7.13.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@ -2486,12 +2486,12 @@
} }
}, },
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0", "version": "7.13.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.1.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "integrity": "sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.11.0", "@typescript-eslint/types": "7.13.1",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {

View File

@ -9,7 +9,7 @@
"@citation-js/plugin-csl": "0.7.11", "@citation-js/plugin-csl": "0.7.11",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3", "@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.1", "@github/relative-time-element": "4.4.2",
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
@ -67,7 +67,7 @@
"@stoplight/spectral-cli": "6.11.1", "@stoplight/spectral-cli": "6.11.1",
"@stylistic/eslint-plugin-js": "2.1.0", "@stylistic/eslint-plugin-js": "2.1.0",
"@stylistic/stylelint-plugin": "2.1.2", "@stylistic/stylelint-plugin": "2.1.2",
"@typescript-eslint/parser": "7.11.0", "@typescript-eslint/parser": "7.13.1",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",

View File

@ -1,8 +1,5 @@
[tool.poetry] [tool.poetry]
name = "gitea" package-mode = false
version = "0.0.0"
description = ""
authors = []
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.10"

View File

@ -242,16 +242,12 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
} }
// get upload file size // get upload file size
fileRealTotalSize, contentLength, err := getUploadFileSize(ctx) fileRealTotalSize, contentLength := getUploadFileSize(ctx)
if err != nil {
log.Error("Error get upload file size: %v", err)
ctx.Error(http.StatusInternalServerError, "Error get upload file size")
return
}
// get artifact retention days // get artifact retention days
expiredDays := setting.Actions.ArtifactRetentionDays expiredDays := setting.Actions.ArtifactRetentionDays
if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" {
var err error
expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64)
if err != nil { if err != nil {
log.Error("Error parse retention days: %v", err) log.Error("Error parse retention days: %v", err)

View File

@ -39,7 +39,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
r = io.TeeReader(r, hasher) r = io.TeeReader(r, hasher)
} }
// save chunk to storage // save chunk to storage
writtenSize, err := st.Save(storagePath, r, -1) writtenSize, err := st.Save(storagePath, r, contentSize)
if err != nil { if err != nil {
return -1, fmt.Errorf("save chunk to storage error: %v", err) return -1, fmt.Errorf("save chunk to storage error: %v", err)
} }
@ -208,7 +208,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
// save merged file // save merged file
storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
written, err := st.Save(storagePath, mergedReader, -1) written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize)
if err != nil { if err != nil {
return fmt.Errorf("save merged file error: %v", err) return fmt.Errorf("save merged file error: %v", err)
} }

View File

@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true return task, runID, true
} }
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam
task := ctx.ActionTask task := ctx.ActionTask
runID, err := strconv.ParseInt(rawRunID, 10, 64) runID, err := strconv.ParseInt(rawRunID, 10, 64)
if err != nil || task.Job.RunID != runID { if err != nil || task.Job.RunID != runID {
@ -84,11 +84,11 @@ func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
// getUploadFileSize returns the size of the file to be uploaded. // getUploadFileSize returns the size of the file to be uploaded.
// The raw size is the size of the file as reported by the header X-TFS-FileLength. // The raw size is the size of the file as reported by the header X-TFS-FileLength.
func getUploadFileSize(ctx *ArtifactContext) (int64, int64, error) { func getUploadFileSize(ctx *ArtifactContext) (int64, int64) {
contentLength := ctx.Req.ContentLength contentLength := ctx.Req.ContentLength
xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64) xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
if xTfsLength > 0 { if xTfsLength > 0 {
return xTfsLength, contentLength, nil return xTfsLength, contentLength
} }
return contentLength, contentLength, nil return contentLength, contentLength
} }

View File

@ -588,6 +588,8 @@ func CommonRoutes() *web.Route {
r.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease) r.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease)
r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification) r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification)
r.Get("/gems/{filename}", rubygems.DownloadPackageFile) r.Get("/gems/{filename}", rubygems.DownloadPackageFile)
r.Get("/info/{packagename}", rubygems.GetPackageInfo)
r.Get("/versions", rubygems.GetAllPackagesVersions)
r.Group("/api/v1/gems", func() { r.Group("/api/v1/gems", func() {
r.Post("/", rubygems.UploadPackageFile) r.Post("/", rubygems.UploadPackageFile)
r.Delete("/yank", rubygems.DeletePackage) r.Delete("/yank", rubygems.DeletePackage)

View File

@ -26,7 +26,7 @@ var uploadVersionMutex sync.Mutex
// saveAsPackageBlob creates a package blob from an upload // saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image // The uploaded blob gets stored in a special upload version to link them to the package/image
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam
pb := packages_service.NewPackageBlob(hsr) pb := packages_service.NewPackageBlob(hsr)
exists := false exists := false

View File

@ -116,9 +116,9 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
} }
func apiUnauthorizedError(ctx *context.Context) { func apiUnauthorizedError(ctx *context.Context) {
// TODO: it doesn't seem quite right but it doesn't really cause problem at the moment. // container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed, ideally. realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`) ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
apiErrorDefined(ctx, errUnauthorized) apiErrorDefined(ctx, errUnauthorized)
} }

View File

@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) {
}) })
} }
func xmlResponse(ctx *context.Context, status int, obj any) { func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam
ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
ctx.Resp.WriteHeader(status) ctx.Resp.WriteHeader(status)
if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {

View File

@ -6,6 +6,7 @@ package rubygems
import ( import (
"compress/gzip" "compress/gzip"
"compress/zlib" "compress/zlib"
"crypto/md5"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -227,12 +228,7 @@ func UploadPackageFile(ctx *context.Context) {
return return
} }
var filename string filename := makeGemFullFileName(rp.Name, rp.Version, rp.Metadata.Platform)
if rp.Metadata.Platform == "" || rp.Metadata.Platform == "ruby" {
filename = strings.ToLower(fmt.Sprintf("%s-%s.gem", rp.Name, rp.Version))
} else {
filename = strings.ToLower(fmt.Sprintf("%s-%s-%s.gem", rp.Name, rp.Version, rp.Metadata.Platform))
}
_, _, err = packages_service.CreatePackageAndAddFile( _, _, err = packages_service.CreatePackageAndAddFile(
ctx, ctx,
@ -300,6 +296,136 @@ func DeletePackage(ctx *context.Context) {
} }
} }
// GetPackageInfo returns a custom text based format for the single rubygem with a line for each version of the rubygem
// ref: https://guides.rubygems.org/rubygems-org-compact-index-api/
func GetPackageInfo(ctx *context.Context) {
packageName := ctx.Params("packagename")
versions, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, packageName)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(versions) == 0 {
apiError(ctx, http.StatusNotFound, nil)
return
}
infoContent, err := makePackageInfo(ctx, versions)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.PlainText(http.StatusOK, infoContent)
}
// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems.
// ref: https://guides.rubygems.org/rubygems-org-compact-index-api/
func GetAllPackagesVersions(ctx *context.Context) {
packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
out := &strings.Builder{}
out.WriteString("---\n")
for _, pkg := range packages {
versions, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, pkg.Name)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(versions) == 0 {
continue
}
info, err := makePackageInfo(ctx, versions)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
// format: RUBYGEM [-]VERSION_PLATFORM[,VERSION_PLATFORM],...] MD5
_, _ = fmt.Fprintf(out, "%s ", pkg.Name)
for i, v := range versions {
sep := util.Iif(i == len(versions)-1, "", ",")
_, _ = fmt.Fprintf(out, "%s%s", v.Version, sep)
}
_, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info)))
}
ctx.PlainText(http.StatusOK, out.String())
}
func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) {
out.WriteString(prefix)
if len(reqs) == 0 {
reqs = []rubygems_module.VersionRequirement{{Restriction: ">=", Version: "0"}}
}
for i, req := range reqs {
sep := util.Iif(i == 0, "", "&")
_, _ = fmt.Fprintf(out, "%s%s %s", sep, req.Restriction, req.Version)
}
}
func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) {
// format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...]
// DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT]
// REQUIREMENT: KEY:VALUE (always contains "checksum")
pd, err := packages_model.GetPackageDescriptor(ctx, version)
if err != nil {
return "", err
}
metadata := pd.Metadata.(*rubygems_module.Metadata)
fullFilename := makeGemFullFileName(pd.Package.Name, version.Version, metadata.Platform)
file, err := packages_model.GetFileForVersionByName(ctx, version.ID, fullFilename, "")
if err != nil {
return "", err
}
blob, err := packages_model.GetBlobByID(ctx, file.BlobID)
if err != nil {
return "", err
}
buf := &strings.Builder{}
buf.WriteString(version.Version)
buf.WriteByte(' ')
for i, dep := range metadata.RuntimeDependencies {
sep := util.Iif(i == 0, "", ",")
writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf)
}
_, _ = fmt.Fprintf(buf, "|checksum:%s", blob.HashSHA256)
if len(metadata.RequiredRubyVersion) != 0 {
writePackageVersionRequirements(",ruby:", metadata.RequiredRubyVersion, buf)
}
if len(metadata.RequiredRubygemsVersion) != 0 {
writePackageVersionRequirements(",rubygems:", metadata.RequiredRubygemsVersion, buf)
}
return buf.String(), nil
}
func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) {
ret := "---\n"
for _, v := range versions {
dep, err := makePackageVersionDependency(ctx, v)
if err != nil {
return "", err
}
ret += dep + "\n"
}
return ret, nil
}
func makeGemFullFileName(gemName, version, platform string) string {
var basename string
if platform == "" || platform == "ruby" {
basename = fmt.Sprintf("%s-%s", gemName, version)
} else {
basename = fmt.Sprintf("%s-%s-%s", gemName, version, platform)
}
return strings.ToLower(basename) + ".gem"
}
func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) { func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) {
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID, OwnerID: ctx.Package.Owner.ID,

View File

@ -1168,6 +1168,15 @@ func Routes() *web.Route {
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tag_protections", func() {
m.Combo("").Get(repo.ListTagProtection).
Post(bind(api.CreateTagProtectionOption{}), mustNotBeArchived, repo.CreateTagProtection)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetTagProtection).
Patch(bind(api.EditTagProtectionOption{}), mustNotBeArchived, repo.EditTagProtection).
Delete(repo.DeleteTagProtection)
})
}, reqToken(), reqAdmin())
m.Group("/actions", func() { m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks) m.Get("/tasks", repo.ListActionTasks)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))

View File

@ -7,6 +7,7 @@ import (
go_context "context" go_context "context"
"io" "io"
"net/http" "net/http"
"path"
"strings" "strings"
"testing" "testing"
@ -19,36 +20,40 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const ( const AppURL = "http://localhost:3000/"
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
FullURL = AppURL + Repo + "/"
)
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) { func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
setting.AppURL = AppURL setting.AppURL = AppURL
context := "/gogits/gogs"
if !wiki {
context += path.Join("/src/branch/main", path.Dir(filePath))
}
options := api.MarkupOption{ options := api.MarkupOption{
Mode: mode, Mode: mode,
Text: text, Text: text,
Context: Repo, Context: context,
Wiki: true, Wiki: wiki,
FilePath: filePath, FilePath: filePath,
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
web.SetForm(ctx, &options) web.SetForm(ctx, &options)
Markup(ctx) Markup(ctx)
assert.Equal(t, responseBody, resp.Body.String()) assert.Equal(t, expectedBody, resp.Body.String())
assert.Equal(t, responseCode, resp.Code) assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset() resp.Body.Reset()
} }
func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) { func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
setting.AppURL = AppURL setting.AppURL = AppURL
context := "/gogits/gogs"
if !wiki {
context += "/src/branch/main"
}
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: mode, Mode: mode,
Text: text, Text: text,
Context: Repo, Context: context,
Wiki: true, Wiki: wiki,
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options) web.SetForm(ctx, &options)
@ -65,7 +70,7 @@ func TestAPI_RenderGFM(t *testing.T) {
}, },
}) })
testCasesCommon := []string{ testCasesWiki := []string{
// dear imgui wiki markdown extract: special wiki syntax // dear imgui wiki markdown extract: special wiki syntax
`Wiki! Enjoy :) `Wiki! Enjoy :)
- [[Links, Language bindings, Engine bindings|Links]] - [[Links, Language bindings, Engine bindings|Links]]
@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) {
// rendered // rendered
`<p>Wiki! Enjoy :)</p> `<p>Wiki! Enjoy :)</p>
<ul> <ul>
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> <li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li> <li><a href="http://localhost:3000/gogits/gogs/wiki/Tips" rel="nofollow">Tips</a></li>
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li> <li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
</ul> </ul>
`, `,
// Guard wiki sidebar: special syntax // Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered // rendered
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> `<p><a href="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
`, `,
// special syntax // special syntax
`[[Name|Link]]`, `[[Name|Link]]`,
// rendered // rendered
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p> `<p><a href="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p>
`, `,
// empty // empty
``, ``,
@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) {
``, ``,
} }
testCasesDocument := []string{ testCasesWikiDocument := []string{
// wine-staging wiki home extract: special wiki syntax, images // wine-staging wiki home extract: special wiki syntax, images
`## What is Wine Staging? `## What is Wine Staging?
**Wine Staging** on website [wine-staging.com](http://wine-staging.com). **Wine Staging** on website [wine-staging.com](http://wine-staging.com).
@ -111,31 +116,48 @@ Here are some links to the most important topics. You can find the full list of
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p> <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
<h2 id="user-content-quick-links">Quick Links</h2> <h2 id="user-content-quick-links">Quick Links</h2>
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a> <p><a href="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a>
<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> <a href="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`, `,
} }
for i := 0; i < len(testCasesCommon); i += 2 { for i := 0; i < len(testCasesWiki); i += 2 {
text := testCasesCommon[i] text := testCasesWiki[i]
response := testCasesCommon[i+1] response := testCasesWiki[i+1]
testRenderMarkdown(t, "gfm", text, response, http.StatusOK) testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkdown(t, "comment", text, response, http.StatusOK) testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
testRenderMarkup(t, "comment", "", text, response, http.StatusOK) testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
} }
for i := 0; i < len(testCasesDocument); i += 2 { for i := 0; i < len(testCasesWikiDocument); i += 2 {
text := testCasesDocument[i] text := testCasesWikiDocument[i]
response := testCasesDocument[i+1] response := testCasesWikiDocument[i+1]
testRenderMarkdown(t, "gfm", text, response, http.StatusOK) testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK) testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK) testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
} }
testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity) input := "[Link](test.md)\n![Image](image.png)"
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity) testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/path/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
} }
var simpleCases = []string{ var simpleCases = []string{
@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) {
options := api.MarkdownOption{ options := api.MarkdownOption{
Mode: "markdown", Mode: "markdown",
Text: "", Text: "",
Context: Repo, Context: "/gogits/gogs",
} }
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 { for i := 0; i < len(simpleCases); i += 2 {

View File

@ -64,7 +64,7 @@ func CompareDiff(ctx *context.APIContext) {
} }
} }
_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
Base: infos[0], Base: infos[0],
Head: infos[1], Head: infos[1],
}) })

View File

@ -408,7 +408,7 @@ func CreatePullRequest(ctx *context.APIContext) {
) )
// Get repo/branch information // Get repo/branch information
_, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -1054,7 +1054,7 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK) ctx.Status(http.StatusOK)
} }
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*user_model.User, *repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
baseRepo := ctx.Repo.Repository baseRepo := ctx.Repo.Repository
// Get compared branches information // Get compared branches information
@ -1087,14 +1087,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else { } else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err) ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
} }
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
headBranch = headInfos[1] headBranch = headInfos[1]
// The head repository can also point to the same repo // The head repository can also point to the same repo
isSameRepo = ctx.Repo.Owner.ID == headUser.ID isSameRepo = ctx.Repo.Owner.ID == headUser.ID
} else { } else {
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
ctx.Repo.PullRequest.SameRepo = isSameRepo ctx.Repo.PullRequest.SameRepo = isSameRepo
@ -1102,7 +1102,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
// Check if base branch is valid. // Check if base branch is valid.
if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) {
ctx.NotFound("BaseNotExist") ctx.NotFound("BaseNotExist")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// Check if current user has fork of repository or in the same repository. // Check if current user has fork of repository or in the same repository.
@ -1110,7 +1110,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if headRepo == nil && !isSameRepo { if headRepo == nil && !isSameRepo {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.NotFound("GetForkedRepo") ctx.NotFound("GetForkedRepo")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
var headGitRepo *git.Repository var headGitRepo *git.Repository
@ -1121,7 +1121,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
} }
@ -1130,7 +1130,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
if log.IsTrace() { if log.IsTrace() {
@ -1141,7 +1141,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound("Can't read pulls or can't read UnitTypeCode") ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// user should have permission to read headrepo's codes // user should have permission to read headrepo's codes
@ -1149,7 +1149,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
if !permHead.CanRead(unit.TypeCode) { if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() { if log.IsTrace() {
@ -1160,24 +1160,24 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound("Can't read headRepo UnitTypeCode") ctx.NotFound("Can't read headRepo UnitTypeCode")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// Check if head branch is valid. // Check if head branch is valid.
if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) {
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch return headRepo, headGitRepo, compareInfo, baseBranch, headBranch
} }
// UpdatePullRequest merge PR's baseBranch into headBranch // UpdatePullRequest merge PR's baseBranch into headBranch

View File

@ -107,7 +107,7 @@ func Search(ctx *context.APIContext) {
// - name: sort // - name: sort
// in: query // in: query
// description: sort repos by attribute. Supported values are // description: sort repos by attribute. Supported values are
// "alpha", "created", "updated", "size", and "id". // "alpha", "created", "updated", "size", "git_size", "lfs_size", "stars", "forks" and "id".
// Default is "alpha" // Default is "alpha"
// type: string // type: string
// - name: order // - name: order
@ -184,7 +184,7 @@ func Search(ctx *context.APIContext) {
if len(sortOrder) == 0 { if len(sortOrder) == 0 {
sortOrder = "asc" sortOrder = "asc"
} }
if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok { if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy opts.OrderBy = orderBy
} else { } else {

View File

@ -7,9 +7,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -287,3 +291,349 @@ func DeleteTag(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
// ListTagProtection lists tag protections for a repo
func ListTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection
// ---
// summary: List tag protections for a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtectionList"
repo := ctx.Repo.Repository
pts, err := git_model.GetProtectedTags(ctx, repo.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTags", err)
return
}
apiPts := make([]*api.TagProtection, len(pts))
for i := range pts {
apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo)
}
ctx.JSON(http.StatusOK, apiPts)
}
// GetTagProtection gets a tag protection
func GetTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection
// ---
// summary: Get a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protect to get
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || repo.ID != pt.RepoID {
ctx.NotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// CreateTagProtection creates a tag protection for a repo
func CreateTagProtection(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection
// ---
// summary: Create a tag protections for a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/TagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateTagProtectionOption)
repo := ctx.Repo.Repository
namePattern := strings.TrimSpace(form.NamePattern)
if namePattern == "" {
ctx.Error(http.StatusBadRequest, "name_pattern are empty", "name_pattern are empty")
return
}
if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 {
ctx.Error(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty", "both whitelist_usernames and whitelist_teams are empty")
return
}
pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectTagOfRepo", err)
return
} else if pt != nil {
ctx.Error(http.StatusForbidden, "Create tag protection", "Tag protection already exist")
return
}
var whitelistUsers, whitelistTeams []int64
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
protectTag := &git_model.ProtectedTag{
RepoID: repo.ID,
NamePattern: strings.TrimSpace(namePattern),
AllowlistUserIDs: whitelistUsers,
AllowlistTeamIDs: whitelistTeams,
}
if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil {
ctx.Error(http.StatusInternalServerError, "InsertProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", err)
return
}
ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo))
}
// EditTagProtection edits a tag protection for a repo
func EditTagProtection(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection
// ---
// summary: Edit a tag protections for a repository. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
repo := ctx.Repo.Repository
form := web.GetForm(ctx).(*api.EditTagProtectionOption)
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
if form.NamePattern != nil {
pt.NamePattern = *form.NamePattern
}
var whitelistUsers, whitelistTeams []int64
if form.WhitelistTeams != nil {
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
pt.AllowlistTeamIDs = whitelistTeams
}
if form.WhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
pt.AllowlistUserIDs = whitelistUsers
}
err = git_model.UpdateProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", "New tag protection not found")
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// DeleteTagProtection
func DeleteTagProtection(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection
// ---
// summary: Delete a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
err = git_model.DeleteProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteProtectedTag", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -170,6 +170,12 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreateTagOption api.CreateTagOption CreateTagOption api.CreateTagOption
// in:body
CreateTagProtectionOption api.CreateTagProtectionOption
// in:body
EditTagProtectionOption api.EditTagProtectionOption
// in:body // in:body
CreateAccessTokenOption api.CreateAccessTokenOption CreateAccessTokenOption api.CreateAccessTokenOption

View File

@ -70,6 +70,20 @@ type swaggerResponseAnnotatedTag struct {
Body api.AnnotatedTag `json:"body"` Body api.AnnotatedTag `json:"body"`
} }
// TagProtectionList
// swagger:response TagProtectionList
type swaggerResponseTagProtectionList struct {
// in:body
Body []api.TagProtection `json:"body"`
}
// TagProtection
// swagger:response TagProtection
type swaggerResponseTagProtection struct {
// in:body
Body api.TagProtection `json:"body"`
}
// Reference // Reference
// swagger:response Reference // swagger:response Reference
type swaggerResponseReference struct { type swaggerResponseReference struct {

View File

@ -7,63 +7,67 @@ package common
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"path"
"strings" "strings"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"mvdan.cc/xurls/v2"
) )
// RenderMarkup renders markup text for the /markup and /markdown endpoints // RenderMarkup renders markup text for the /markup and /markdown endpoints
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) { func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
var markupType string // urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
relativePath := "" // filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
// filePath will be used as RenderContext.RelativePath
if len(text) == 0 { // for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
_, _ = ctx.Write([]byte("")) // and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
return
var markupType, relativePath string
links := markup.Links{AbsolutePrefix: true}
if urlPathContext != "" {
links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
} }
switch mode { switch mode {
case "markdown": case "markdown":
// Raw markdown // Raw markdown
if err := markdown.RenderRaw(&markup.RenderContext{ if err := markdown.RenderRaw(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Links: markup.Links{ Links: links,
AbsolutePrefix: true,
Base: urlPrefix,
},
}, strings.NewReader(text), ctx.Resp); err != nil { }, strings.NewReader(text), ctx.Resp); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
} }
return return
case "comment": case "comment":
// Comment as markdown // Issue & comment content
markupType = markdown.MarkupName markupType = markdown.MarkupName
case "gfm": case "gfm":
// Github Flavored Markdown as document // GitHub Flavored Markdown
markupType = markdown.MarkupName markupType = markdown.MarkupName
case "file": case "file":
// File as document based on file extension markupType = "" // render the repo file content by its extension
markupType = ""
relativePath = filePath relativePath = filePath
default: default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode)) ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
return return
} }
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5)
// check if urlPrefix is already set to a URL if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
linkRegex, _ := xurls.StrictMatchingScheme("https?://") // absolute base prefix is something like "https://host/subpath/{user}/{repo}"
m := linkRegex.FindStringIndex(urlPrefix) absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1])
if m == nil {
urlPrefix = util.URLJoin(setting.AppURL, urlPrefix) fileDir := path.Dir(filePath) // it is "doc" if filePath is "doc/CHANGE.md"
} refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc"
refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12"
links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
} }
meta := map[string]string{} meta := map[string]string{}
@ -81,12 +85,9 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
} }
if err := markup.Render(&markup.RenderContext{ if err := markup.Render(&markup.RenderContext{
Ctx: ctx, Ctx: ctx,
Repo: repoCtx, Repo: repoCtx,
Links: markup.Links{ Links: links,
AbsolutePrefix: true,
Base: urlPrefix,
},
Metas: meta, Metas: meta,
IsWiki: wiki, IsWiki: wiki,
Type: markupType, Type: markupType,

View File

@ -25,7 +25,7 @@ import (
// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
func ProtocolMiddlewares() (handlers []any) { func ProtocolMiddlewares() (handlers []any) {
// first, normalize the URL path // first, normalize the URL path
handlers = append(handlers, stripSlashesMiddleware) handlers = append(handlers, normalizeRequestPathMiddleware)
// prepare the ContextData and panic recovery // prepare the ContextData and panic recovery
handlers = append(handlers, func(next http.Handler) http.Handler { handlers = append(handlers, func(next http.Handler) http.Handler {
@ -75,9 +75,9 @@ func ProtocolMiddlewares() (handlers []any) {
return handlers return handlers
} }
func stripSlashesMiddleware(next http.Handler) http.Handler { func normalizeRequestPathMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// First of all escape the URL RawPath to ensure that all routing is done using a correctly escaped URL // escape the URL RawPath to ensure that all routing is done using a correctly escaped URL
req.URL.RawPath = req.URL.EscapedPath() req.URL.RawPath = req.URL.EscapedPath()
urlPath := req.URL.RawPath urlPath := req.URL.RawPath
@ -86,19 +86,42 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
urlPath = rctx.RoutePath urlPath = rctx.RoutePath
} }
sanitizedPath := &strings.Builder{} normalizedPath := strings.TrimRight(urlPath, "/")
prevWasSlash := false // the following code block is a slow-path for replacing all repeated slashes "//" to one single "/"
for _, chr := range strings.TrimRight(urlPath, "/") { // if the path doesn't have repeated slashes, then no need to execute it
if chr != '/' || !prevWasSlash { if strings.Contains(normalizedPath, "//") {
sanitizedPath.WriteRune(chr) buf := &strings.Builder{}
prevWasSlash := false
for _, chr := range normalizedPath {
if chr != '/' || !prevWasSlash {
buf.WriteRune(chr)
}
prevWasSlash = chr == '/'
} }
prevWasSlash = chr == '/' normalizedPath = buf.String()
}
if setting.UseSubURLPath {
remainingPath, ok := strings.CutPrefix(normalizedPath, setting.AppSubURL+"/")
if ok {
normalizedPath = "/" + remainingPath
} else if normalizedPath == setting.AppSubURL {
normalizedPath = "/"
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") {
// do not respond to other requests, to simulate a real sub-path environment
http.Error(resp, "404 page not found, sub-path is: "+setting.AppSubURL, http.StatusNotFound)
return
}
// TODO: it's not quite clear about how req.URL and rctx.RoutePath work together.
// Fortunately, it is only used for debug purpose, we have enough time to figure it out in the future.
req.URL.RawPath = normalizedPath
req.URL.Path = normalizedPath
} }
if rctx == nil { if rctx == nil {
req.URL.Path = sanitizedPath.String() req.URL.Path = normalizedPath
} else { } else {
rctx.RoutePath = sanitizedPath.String() rctx.RoutePath = normalizedPath
} }
next.ServeHTTP(resp, req) next.ServeHTTP(resp, req)
}) })

Some files were not shown because too many files have changed in this diff Show More