Compare commits

..

52 Commits

Author SHA1 Message Date
Lunny Xiao 369830bada Release notes for 1.25.4 (#36385)
---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Thomas Beutlich <115483027+thbeu@users.noreply.github.com>
2026-01-22 00:57:25 +00:00
Tyrone Yeh d7d6533311 Fix markdown newline handling during IME composition (gitea#36421) (#36424)
Backport of #36421
2026-01-21 16:56:39 -08:00
Giteabot c326369f47 Allow foreachref parse max tokens from 4*64KB to 4MB (#36414) (#36429)
Backport #36414 by @lunny

Fix #36408

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-21 16:23:00 -08:00
Lunny Xiao 4cdb8a7f96 Fix missing repository id when migrating release attachments (#36389) (#36413)
This PR fixes missed repo_id on the migration of attachments to Gitea.
It also provides a doctor check to fix the dirty data on the database.

Backport #36389
2026-01-20 23:36:45 +02:00
wxiaoguang 38125a8d1d Fix git http service handling (#36396)
Partially backport #36391
2026-01-18 01:42:35 +08:00
Giteabot 175a425825 Fix bug on notification read (#36339) (#36387)
Backport #36339 by @lunny

When a user has been revoked permission to access a repository, the
related notification could still be visited. But the repository's
information should not be leaked any more.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-16 09:58:56 +02:00
Lunny Xiao 6132f639e7 Fix bug when compare in the pull request (#36363) (#36372)
The pull request comparison should not use `direct compare`.

Backport #36363
2026-01-15 01:58:42 +00:00
Giteabot dfe4055b92 Release attachments must belong to the intended repo (#36347) (#36375)
Backport #36347 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-15 02:30:18 +01:00
Giteabot 5fe9703586 Fix permission check on org project operations (#36318) (#36373)
Backport #36318 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-14 14:22:27 -08:00
Giteabot 53d67dae28 clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
Backport #36319 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-14 19:28:02 +01:00
Giteabot ef6ab681f7 Fix incorrect text content detection (#36364) (#36369)
Backport #36364 by wxiaoguang

Fix #36325

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-15 00:45:38 +08:00
Giteabot 812a3cffb3 Add more check for stopwatch read or list (#36340) (#36368)
Backport #36340 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-14 16:04:02 +00:00
Giteabot 669b22100b Fix openid setting check (#36346) (#36361)
Backport #36346 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-13 16:47:00 +02:00
Giteabot a0c77673ff fill missing has_code in repository api (#36338) (#36359)
Backport #36338 by @TheFox0x7

fixes: https://github.com/go-gitea/gitea/issues/36332

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-13 10:31:18 +00:00
Giteabot d96b68cbf5 Fix notifications pagination query parameters (#36351) (#36358)
Backport #36351 by @wxiaoguang

Fix #36350

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-13 04:37:20 +00:00
Giteabot f8ec5b3e43 Fix cancel auto merge bug (#36341) (#36356)
Backport #36341 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-12 18:55:41 -08:00
Lunny Xiao 11891c2dac Fix delete attachment check (#36320) (#36355)
Backport #36320
2026-01-12 13:48:07 -08:00
Giteabot 2c778ff067 LFS locks must belong to the intended repo (#36344) (#36349)
Backport #36344 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-12 09:09:16 +02:00
wxiaoguang 83ce45b186 Fix some trivial problems (#36336) (#36337)
Partially backport #36336

1. correctly parse git protocol's "OldCommit NewCommit RefName" line, it
should be explicitly split by space
2. trim space for the "commit status context name" to follow the same
behavior of git_model.NewCommitStatus
2026-01-09 20:59:03 +02:00
silverwind 39e83bd3fd Fix WebAuthn error checking (#36219) (#36235)
Backport of https://github.com/go-gitea/gitea/pull/36219
Fixes: https://github.com/go-gitea/gitea/issues/36216

Now `detectWebAuthnSupport` returns the error type and lets the caller
decide whether they call `webAuthnError` and show the error. It no
longer shows the error during page load when the user has not even
interacted with the feature.

The bug affects all users on HTTP, so I think a quick fix release for
this might be good.
2026-01-06 04:57:59 +00:00
Giteabot c2f9edd673 fix: prevent panic when GitLab release has more links than sources (#36295) (#36305)
Backport #36295 by argoyle

Fixes #36292

Co-authored-by: Joakim Olsson <joakim@unbound.se>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-05 17:29:28 +00:00
Giteabot aa575672ac Fix stats bug when syncing release (#36285) (#36294)
Backport #36285 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-01-04 12:14:00 +02:00
TheFox0x7 e9c14723b6 add more routes to the "expensive" list (#36290)
backport: https://github.com/go-gitea/gitea/pull/35547
closes https://github.com/go-gitea/gitea/issues/36229

Signed-off-by: Steven Noonan <steven@uplinklabs.net>
Co-authored-by: Steven Noonan <steven@uplinklabs.net>
2026-01-03 16:59:48 +02:00
Giteabot 76b6e94b5b Always honor user's choice for "delete branch after merge" (#36281) (#36286)
Backport #36281 by wxiaoguang

Fix #36280

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-03 03:06:10 +00:00
Giteabot 163113d173 Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
Backport #36264 by wxiaoguang

Fix #36253

Support slashes in `{ref}` (follow GitHub's behavior)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-01-01 03:04:03 +00:00
Lunny Xiao 7d010c6932 Fix panic when get editor config file (#36241) (#36247)
Fix #36239
Backport #36241
2025-12-27 20:29:22 -08:00
Giteabot b71e688634 Use the requested host for LFS links (#36242) (#36258)
Backport #36242 by sollyu

Co-authored-by: sollyu <43105186+sollyu@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-27 07:23:18 +00:00
Giteabot e147a8223a Fix regression in writing authorized principals (#36213) (#36218)
Backport #36213 by peterverraedt

Fixes: #36212

Signed-off-by: Peter Verraedt <peter.verraedt@kuleuven.be>
Co-authored-by: Peter Verraedt <peter.verraedt@kuleuven.be>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-20 01:57:26 +00:00
Lunny Xiao 9a7cfd8620 Add changelog for 1.25.3 (#36182) 2025-12-18 09:36:38 -08:00
Lunny Xiao 79f4cd754b Fix bugs when comparing and creating pull request (#36144)
Backport #36166
2025-12-17 16:17:33 -08:00
a1012112796 522cc25921 fix webAuthn insecure error view (#36165) (#36179)
backport #36165

Signed-off-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-17 08:49:29 -08:00
Giteabot a99ccfdf74 Fix OrgAssignment opts (#36174) (#36177)
Backport #36174 by @wxiaoguang

Fix #36084

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-17 18:56:58 +08:00
Giteabot d448ab9ad4 Check user visibility when redirecting to a renamed user (#36148) (#36159)
Backport #36148 by @lunny

Fix #34169

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-15 11:42:59 +02:00
wxiaoguang c97b89a662 Fix bug when viewing the commit diff page with non-ANSI files (#36149) (#36150)
Backport #36149
2025-12-14 01:14:06 +08:00
Giteabot 2dd8ef8368 Fix various bugs (#36139) (#36151)
Backport #36139

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-13 13:31:49 +00:00
Giteabot 432e128074 Hide RSS icon when viewing a file not under a branch (#36135) (#36141)
Backport #36135 by @lunny

Fix #35855

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-12 18:39:16 +01:00
silverwind 8d6442a43e Fix SVG size calulation, only use style attribute (#36133) (#36134)
Backport of https://github.com/go-gitea/gitea/pull/36133, only the
bugfix part.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-12 09:40:27 +02:00
wxiaoguang 3d66e75a47 Make Golang correctly delete temp files during uploading (#36128) (#36129)
Fix #36127

Partially backport #36128
And by the way partially backport  #36017
2025-12-11 20:10:59 +01:00
Giteabot e98d9bb93e Improve math rendering (#36124) (#36125)
Backport #36124 by @wxiaoguang

Fix #36108
Fix #36107

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-11 01:40:01 +08:00
Giteabot a601c09826 Changed a small typo in an error message and code comments. (#36117) (#36123)
Backport #36117 by @schinkelg

Changed a small typo in an English error message and code comments. Very
small PR.

Co-authored-by: Ger Schinkel <schinkelg@users.noreply.github.com>
2025-12-10 18:45:56 +08:00
Giteabot b5f50ff63b Add strikethrough button to markdown editor (#36087) (#36104)
Backport #36087 by silverwind

Co-authored-by: silverwind <me@silverwind.io>
2025-12-08 09:07:27 +08:00
Giteabot b1b35e934e Fix the bug when ssh clone with redirect user or repository (#36039) (#36090)
Backport #36039 by @lunny

Fix #36026 

The redirect should be checked when original user/repo doesn't exist.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-04 20:06:14 -08:00
Giteabot 544450a212 fix some file icon ui (#36078) (#36088)
Backport #36078 by @a1012112796

fix #36071

looks that's because if an svg in hiden env, it's color added by
`fill="url(#a)"` will become not usefull. by ai helping, I think moving
it out of page by position is a good solution. fell free creat a new
pull request if you have a better soluton. Thanks.
<img width="2198" height="1120" alt="image"
src="https://github.com/user-attachments/assets/bbf7c171-0b7f-412a-a1bc-aea3f1629636"
/>

Signed-off-by: a1012112796 <1012112796@qq.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-12-04 19:39:35 +00:00
Giteabot 0ab447005d Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
Backport #36055 by @lunny

Replace #36032
Fix #36030

This PR use `net/smtp` instead of gomail's smtp. Now
github.com/wneessen/go-mail will be used only for generating email
message body.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-04 18:58:01 +00:00
Giteabot 52902d4ece Fix edit user email bug in API (#36068) (#36081)
Backport #36068 by @lunny

Follow #36058 for API edit user bug when editing email.

- The Admin Edit User API includes a breaking change. Previously, when
updating a user with an email from an unallowed domain, the request
would succeed but return a warning in the response headers. Now, the
request will fail and return an error in the response body instead.
- Removed `AdminAddOrSetPrimaryEmailAddress` because it will not be used
any where.

Fix https://github.com/go-gitea/gitea/pull/36058#issuecomment-3600005186

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-04 18:25:52 +00:00
silverwind 0e91c8a068 Bump toolchain to go1.25.5, misc fixes (#36082)
Backport toolchain change into 1.25. This is needed because of the
govulncheck issue
[present](https://github.com/go-gitea/gitea/actions/runs/19921920886/job/57112316941)
in the branch.

---------

Signed-off-by: silverwind <me@silverwind.io>
2025-12-04 17:57:59 +00:00
Giteabot 45cdc5d8fd Fix bug when updating user email (#36058) (#36066)
Backport #36058 by @lunny

Fix #20390 

We should use `ReplacePrimaryEmailAddress` instead of
`AdminAddOrSetPrimaryEmailAddress` when modify user's email from admin
panel. And also we need a database transaction to keep deletion and
insertion succeed at the same time.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-12-02 02:57:21 +01:00
Zettat123 b276849cd8 Fix Actions pull_request.paths being triggered incorrectly by rebase (#36045) (#36054)
Backport #36045

Partially fix #34710

The bug described in #34710 can be divided into two parts: `push.paths`
and `pull_request.paths`. This PR fixes the issue related to
`pull_request.paths`. The root cause is that the check for whether the
workflow can be triggered happens **before** updating the PR’s merge
base. This causes the file-change detection to use the old merge base.
Therefore, we need to update the merge base first and then check whether
the workflow can be triggered.
2025-11-29 05:45:30 +00:00
Giteabot 46d1d154e8 Fix error handling in mailer and wiki services (#36041) (#36053)
Backport #36041 by @hamkido

- Updated error message in `incoming.go` to remove unnecessary wrapping
of the error.
- Corrected typo in error message in `wiki.go` for clarity.

Co-authored-by: hamkido <hamki.do2000@gmail.com>
2025-11-28 20:34:38 -08:00
Giteabot f164e38e04 Fix incorrect viewed files counter if file has changed (#36009) (#36047)
Backport #36009 by @bytedream

File changes since last review didn't decrease the viewed files counter

---
<img width="440" height="178" alt="image"
src="https://github.com/user-attachments/assets/da34fcf4-452f-4f71-8da2-97edbfc31fdd"
/>

Also reported here ->
https://github.com/go-gitea/gitea/issues/35803#issuecomment-3567850285

Co-authored-by: bytedream <me@bytedream.dev>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-11-28 11:33:37 +01:00
Giteabot d4d338f1c1 Fix container registry error handling (#36021) (#36037)
Backport #36021 by wxiaoguang

1. the `if` check in `handleCreateManifestResult` didn't handler err
correctly
2. add more error details for debugging

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-11-26 08:01:35 -08:00
Giteabot f6895f632e Add "site admin" back to profile menu (#36010) (#36013)
Backport #36010 by @wxiaoguang

Fix #35904

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-11-23 14:07:44 -08:00
151 changed files with 2416 additions and 1008 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
if: github.repository == 'go-gitea/gitea' if: github.repository == 'go-gitea/gitea'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+7 -7
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -101,7 +101,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -116,7 +116,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -145,7 +145,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -190,7 +190,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+5 -5
View File
@@ -39,7 +39,7 @@ jobs:
- "9000:9000" - "9000:9000"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -67,7 +67,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -125,7 +125,7 @@ jobs:
- 10000:10000 - 10000:10000
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -178,7 +178,7 @@ jobs:
- "993:993" - "993:993"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -218,7 +218,7 @@ jobs:
- 10000:10000 - 10000:10000
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -65,7 +65,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
@@ -107,7 +107,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
# fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force - run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
+58
View File
@@ -4,6 +4,64 @@ This changelog goes through the changes that have been made in each release
without substantial changes to our git log; to see the highlights of what has without substantial changes to our git log; to see the highlights of what has
been added to each release, please refer to the [blog](https://blog.gitea.com). been added to each release, please refer to the [blog](https://blog.gitea.com).
## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/1.25.4) - 2026-01-15
* SECURITY
* Release attachments must belong to the intended repo (#36347) (#36375)
* Fix permission check on org project operations (#36318) (#36373)
* Clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
* Add more check for stopwatch read or list (#36340) (#36368)
* Fix openid setting check (#36346) (#36361)
* Fix cancel auto merge bug (#36341) (#36356)
* Fix delete attachment check (#36320) (#36355)
* LFS locks must belong to the intended repo (#36344) (#36349)
* Fix bug on notification read (#36339) #36387
* ENHANCEMENTS
* Add more routes to the "expensive" list (#36290)
* Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
* BUGFIXES
* Fix markdown newline handling during IME composition (#36421) #36424
* Fix missing repository id when migrating release attachments (#36389)
* Fix bug when compare in the pull request (#36363) (#36372)
* Fix incorrect text content detection (#36364) (#36369)
* Fill missing `has_code` in repository api (#36338) (#36359)
* Fix notifications pagination query parameters (#36351) (#36358)
* Fix some trivial problems (#36336) (#36337)
* Prevent panic when GitLab release has more links than sources (#36295) (#36305)
* Fix stats bug when syncing release (#36285) (#36294)
* Always honor user's choice for "delete branch after merge" (#36281) (#36286)
* Use the requested host for LFS links (#36242) (#36258)
* Fix panic when get editor config file (#36241) (#36247)
* Fix regression in writing authorized principals (#36213) (#36218)
* Fix WebAuthn error checking (#36219) (#36235)
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
* SECURITY
* Bump toolchain to go1.25.5, misc fixes (#36082)
* ENHANCEMENTS
* Add strikethrough button to markdown editor (#36087) (#36104)
* Add "site admin" back to profile menu (#36010) (#36013)
* Improve math rendering (#36124) (#36125)
* BUGFIXES
* Check user visibility when redirecting to a renamed user (#36148) (#36159)
* Fix various bugs (#36139) (#36151)
* Fix bug when viewing the commit diff page with non-ANSI files (#36149) (#36150)
* Hide RSS icon when viewing a file not under a branch (#36135) (#36141)
* Fix SVG size calulation, only use `style` attribute (#36133) (#36134)
* Make Golang correctly delete temp files during uploading (#36128) (#36129)
* Fix the bug when ssh clone with redirect user or repository (#36039) (#36090)
* Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
* Fix edit user email bug in API (#36068) (#36081)
* Fix bug when updating user email (#36058) (#36066)
* Fix incorrect viewed files counter if file has changed (#36009) (#36047)
* Fix container registry error handling (#36021) (#36037)
* Fix webAuthn insecure error view (#36165) (#36179)
* Fix some file icon ui (#36078) (#36088)
* Fix Actions `pull_request.paths` being triggered incorrectly by rebase (#36045) (#36054)
* Fix error handling in mailer and wiki services (#36041) (#36053)
* Fix bugs when comparing and creating pull request (#36166) (#36144)
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23 ## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
* SECURITY * SECURITY
+1 -1
View File
@@ -35,7 +35,7 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf0
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest 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.7.8
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0 GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
+13 -11
View File
@@ -163,6 +163,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil return len(s), nil
} }
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
fields := strings.Split(line, " ")
if len(fields) != 3 {
return "", "", "", false
}
return fields[0], fields[1], git.RefName(fields[2]), true
}
func runHookPreReceive(ctx context.Context, c *cli.Command) error { func runHookPreReceive(ctx context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil return nil
@@ -228,14 +236,11 @@ Gitea or set your environment appropriately.`, "")
continue continue
} }
fields := bytes.Fields(scanner.Bytes()) oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
if len(fields) != 3 { if !ok {
continue continue
} }
oldCommitID := string(fields[0])
newCommitID := string(fields[1])
refFullName := git.RefName(fields[2])
total++ total++
lastline++ lastline++
@@ -378,16 +383,13 @@ Gitea or set your environment appropriately.`, "")
continue continue
} }
fields := bytes.Fields(scanner.Bytes()) var ok bool
if len(fields) != 3 { oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
if !ok {
continue continue
} }
fmt.Fprintf(out, ".") fmt.Fprintf(out, ".")
oldCommitIDs[count] = string(fields[0])
newCommitIDs[count] = string(fields[1])
refFullNames[count] = git.RefName(fields[2])
commitID, _ := git.NewIDFromString(newCommitIDs[count]) commitID, _ := git.NewIDFromString(newCommitIDs[count])
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total { if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
masterPushed = true masterPushed = true
+14
View File
@@ -39,3 +39,17 @@ func TestPktLine(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte("0007a\nb"), w.Bytes()) assert.Equal(t, []byte("0007a\nb"), w.Bytes())
} }
func TestParseGitHookCommitRefLine(t *testing.T) {
oldCommitID, newCommitID, refName, ok := parseGitHookCommitRefLine("a b c")
assert.True(t, ok)
assert.Equal(t, "a", oldCommitID)
assert.Equal(t, "b", newCommitID)
assert.Equal(t, "c", string(refName))
_, _, _, ok = parseGitHookCommitRefLine("a\tb\tc")
assert.False(t, ok)
_, _, _, ok = parseGitHookCommitRefLine("a b")
assert.False(t, ok)
}
+3 -1
View File
@@ -1,6 +1,8 @@
module code.gitea.io/gitea module code.gitea.io/gitea
go 1.25.4 go 1.25.0
toolchain go1.25.5
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related: // But some CAs use negative serial number, just relax the check. related:
+36 -7
View File
@@ -10,6 +10,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"sync" "sync"
@@ -50,12 +51,42 @@ func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
return err return err
} }
var globalVars = sync.OnceValue(func() (ret struct {
principalRegexp *regexp.Regexp
},
) {
// principalRegexp expresses whether a principal is considered valid.
// This reverse engineers how sshd parses the authorized keys file,
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
// Any newline or # comment will be stripped when parsing, so don't allow
// those. Also, if any space or tab is present in the principal, the part
// proceeding this would be parsed as an option, so just avoid any whitespace
// altogether.
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
return ret
})
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) { func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s %s` + "\n" const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil { var sshKey string
return false, err
if key.Type == KeyTypePrincipal {
// TODO: actually using PublicKey to store "principal" is an abuse
if !globalVars().principalRegexp.MatchString(key.Content) {
return false, fmt.Errorf("invalid principal key: %s", key.Content)
}
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
} else {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil {
return false, err
}
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
} }
// now the key is valid, the code below could only return template/IO related errors // now the key is valid, the code below could only return template/IO related errors
sbCmd := &strings.Builder{} sbCmd := &strings.Builder{}
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{ err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
@@ -69,9 +100,7 @@ func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, er
return true, err return true, err
} }
sshCommandEscaped := util.ShellEscape(sbCmd.String()) sshCommandEscaped := util.ShellEscape(sbCmd.String())
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey))) _, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
sshKeyComment := fmt.Sprintf("user-%d", key.OwnerID)
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKeyMarshalled, sshKeyComment)
return true, err return true, err
} }
@@ -0,0 +1,90 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package asymkey
import (
"strings"
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestWriteAuthorizedStringForKey(t *testing.T) {
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
sb := &strings.Builder{}
valid, err := writeAuthorizedStringForKey(key, sb)
return valid, sb.String(), err
}
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
testValid := func(t *testing.T, key *PublicKey, expected string) {
valid, content, err := writeKey(t, key)
assert.True(t, valid)
assert.Equal(t, expected, content)
assert.NoError(t, err)
}
testInvalid := func(t *testing.T, key *PublicKey) {
valid, content, err := writeKey(t, key)
assert.False(t, valid)
assert.Empty(t, content)
assert.Error(t, err)
}
t.Run("PublicKey", func(t *testing.T) {
testValid(t, &PublicKey{
OwnerID: 123,
Content: validKeyContent + " any-comment",
Type: KeyTypeUser,
}, `# gitea public key
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
`)
})
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
testValid(t, &PublicKey{
OwnerID: 123,
Content: validKeyContent + "\nany-more", // the new line should be ignored
Type: KeyTypeUser,
}, `# gitea public key
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
`)
})
t.Run("PublicKeyInvalid", func(t *testing.T) {
testInvalid(t, &PublicKey{
OwnerID: 123,
Content: validKeyContent + "any-more",
Type: KeyTypeUser,
})
})
t.Run("Principal", func(t *testing.T) {
testValid(t, &PublicKey{
OwnerID: 123,
Content: "any-content",
Type: KeyTypePrincipal,
}, `# gitea public key
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
`)
})
t.Run("PrincipalInvalid", func(t *testing.T) {
testInvalid(t, &PublicKey{
OwnerID: 123,
Content: "a b",
Type: KeyTypePrincipal,
})
testInvalid(t, &PublicKey{
OwnerID: 123,
Content: "a\nb",
Type: KeyTypePrincipal,
})
})
}
+4 -4
View File
@@ -108,10 +108,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
return rel, nil return rel, nil
} }
// GetLFSLockByID returns release by given id. // GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) { func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
lock := new(LFSLock) lock := new(LFSLock)
has, err := db.GetEngine(ctx).ID(id).Get(lock) has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
@@ -160,7 +160,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
// DeleteLFSLockByID deletes a lock by given ID. // DeleteLFSLockByID deletes a lock by given ID.
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) { func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
lock, err := GetLFSLockByID(ctx, id) lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"fmt"
"testing"
"time"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
t.Helper()
path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
OwnerID: owner.ID,
Path: path,
})
require.NoError(t, err)
return lock
}
func TestGetLFSLockByIDAndRepo(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
lockRepo1 := createTestLock(t, repo1, user2)
lockRepo3 := createTestLock(t, repo3, user4)
fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
require.NoError(t, err)
assert.Equal(t, lockRepo1.ID, fetched.ID)
assert.Equal(t, repo1.ID, fetched.RepoID)
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
assert.Error(t, err)
assert.True(t, IsErrLFSLockNotExist(err))
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
assert.Error(t, err)
assert.True(t, IsErrLFSLockNotExist(err))
}
func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
lockRepo1 := createTestLock(t, repo1, user2)
lockRepo3 := createTestLock(t, repo3, user4)
_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
assert.Error(t, err)
assert.True(t, IsErrLFSLockNotExist(err))
existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
require.NoError(t, err)
assert.Equal(t, lockRepo3.ID, existing.ID)
deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
require.NoError(t, err)
assert.Equal(t, lockRepo3.ID, deleted.ID)
deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
require.NoError(t, err)
assert.Equal(t, lockRepo1.ID, deleted.ID)
}
+13
View File
@@ -12,6 +12,8 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"xorm.io/builder"
) )
// Stopwatch represents a stopwatch for time tracking. // Stopwatch represents a stopwatch for time tracking.
@@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
}) })
return ok, err return ok, err
} }
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
// this function should be called before removing all the issues of the repository
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).
Where("`stopwatch`.user_id = ?", userID).
And(builder.In("`stopwatch`.issue_id",
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
Delete(new(Stopwatch))
return err
}
+12
View File
@@ -213,6 +213,18 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
return column, nil return column, nil
} }
func GetColumnByIDAndProjectID(ctx context.Context, columnID, projectID int64) (*Column, error) {
column := new(Column)
has, err := db.GetEngine(ctx).ID(columnID).And("project_id=?", projectID).Get(column)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
}
return column, nil
}
// UpdateColumn updates a project column // UpdateColumn updates a project column
func UpdateColumn(ctx context.Context, column *Column) error { func UpdateColumn(ctx context.Context, column *Column) error {
var fieldToUpdate []string var fieldToUpdate []string
+12
View File
@@ -302,6 +302,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
return p, nil return p, nil
} }
func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) {
p := new(Project)
has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p)
if err != nil {
return nil, err
} else if !has {
return nil, ErrProjectNotExist{ID: id}
}
return p, nil
}
// GetProjectForRepoByID returns the projects in a repository // GetProjectForRepoByID returns the projects in a repository
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) { func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
p := new(Project) p := new(Project)
+6 -6
View File
@@ -73,18 +73,18 @@ func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string)
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not // UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
// The given map of files with their viewed state will be merged with the previous review, if present // The given map of files with their viewed state will be merged with the previous review, if present
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error { func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) (*ReviewState, error) {
log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles) log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles)
review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA) review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA)
if err != nil { if err != nil {
return err return nil, err
} }
if exists { if exists {
review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles) review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles)
} else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil { } else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil {
return err return nil, err
// Overwrite the viewed files of the previous review if present // Overwrite the viewed files of the previous review if present
} else if previousReview != nil { } else if previousReview != nil {
@@ -98,11 +98,11 @@ func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA stri
if !exists { if !exists {
log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles) log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles)
_, err := engine.Insert(review) _, err := engine.Insert(review)
return err return nil, err
} }
log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles) log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles)
_, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles}) _, err = engine.ID(review.ID).Cols("updated_files").Update(review)
return err return review, err
} }
// mergeFiles merges the given maps of files with their viewing state into one map. // mergeFiles merges the given maps of files with their viewing state into one map.
+5
View File
@@ -166,6 +166,11 @@ func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, file
return attach, nil return attach, nil
} }
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
}
// DeleteAttachment deletes the given attachment and optionally the associated file. // DeleteAttachment deletes the given attachment and optionally the associated file.
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error { func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove) _, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
+16
View File
@@ -101,3 +101,19 @@ func TestGetAttachmentsByUUIDs(t *testing.T) {
assert.Equal(t, int64(1), attachList[0].IssueID) assert.Equal(t, int64(1), attachList[0].IssueID)
assert.Equal(t, int64(5), attachList[1].IssueID) assert.Equal(t, int64(5), attachList[1].IssueID)
} }
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
assert.NoError(t, err)
assert.Len(t, attachments, 1)
assert.Equal(t, int64(10), attachments[0].ID)
assert.Zero(t, attachments[0].IssueID)
assert.Zero(t, attachments[0].ReleaseID)
assert.Zero(t, attachments[0].CommentID)
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
assert.NoError(t, err)
assert.Empty(t, attachments)
}
+34 -8
View File
@@ -93,15 +93,25 @@ func init() {
db.RegisterModel(new(Release)) db.RegisterModel(new(Release))
} }
// LoadAttributes load repo and publisher attributes for a release // LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
func (r *Release) LoadAttributes(ctx context.Context) error { // (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
var err error const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
if r.Repo == nil {
r.Repo, err = GetRepositoryByID(ctx, r.RepoID) func (r *Release) LoadRepo(ctx context.Context) (err error) {
if err != nil { if r.Repo != nil {
return err return nil
}
} }
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
return err
}
// LoadAttributes load repo and publisher attributes for a release
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
if err := r.LoadRepo(ctx); err != nil {
return err
}
if r.Publisher == nil { if r.Publisher == nil {
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
if err != nil { if err != nil {
@@ -168,6 +178,11 @@ func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
// AddReleaseAttachments adds a release attachments // AddReleaseAttachments adds a release attachments
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) { func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
rel, err := GetReleaseByID(ctx, releaseID)
if err != nil {
return err
}
// Check attachments // Check attachments
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs) attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
if err != nil { if err != nil {
@@ -175,6 +190,17 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
} }
for i := range attachments { for i := range attachments {
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
attachments[i].RepoID = rel.RepoID
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
}
}
if attachments[i].RepoID != rel.RepoID {
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
}
if attachments[i].ReleaseID != 0 { if attachments[i].ReleaseID != 0 {
return util.NewPermissionDeniedErrorf("release permission denied") return util.NewPermissionDeniedErrorf("release permission denied")
} }
+53
View File
@@ -6,7 +6,9 @@ package repo
import ( import (
"testing" "testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -37,3 +39,54 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
assert.Equal(t, "delete-tag", rels[1].TagName) assert.Equal(t, "delete-tag", rels[1].TagName)
assert.Equal(t, "v1.0", rels[2].TagName) assert.Equal(t, "v1.0", rels[2].TagName)
} }
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrPermissionDenied)
attach, err := GetAttachmentByUUID(t.Context(), uuid)
assert.NoError(t, err)
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
}
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
assert.NoError(t, err)
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
assert.NoError(t, err)
assert.EqualValues(t, 1, attach.RepoID)
assert.EqualValues(t, 1, attach.ReleaseID)
}
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
attachment := &Attachment{
UUID: recentUUID,
RepoID: 0,
IssueID: 0,
ReleaseID: 0,
CommentID: 0,
Name: "recent-zero",
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
}
assert.NoError(t, db.Insert(t.Context(), attachment))
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
assert.Error(t, err)
assert.ErrorIs(t, err, util.ErrPermissionDenied)
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
assert.NoError(t, err)
assert.Zero(t, attach.ReleaseID)
assert.Zero(t, attach.RepoID)
}
+10
View File
@@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
} }
return watchRepoMode(ctx, watch, WatchModeAuto) return watchRepoMode(ctx, watch, WatchModeAuto)
} }
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
// Used when a repository is set to private.
func ClearRepoWatches(ctx context.Context, repoID int64) error {
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
return err
}
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
}
+19
View File
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestIsWatching(t *testing.T) { func TestIsWatching(t *testing.T) {
@@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, watchers, prevCount) assert.Len(t, watchers, prevCount)
} }
func TestClearRepoWatches(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
const repoID int64 = 1
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
require.NoError(t, err)
require.NotEmpty(t, watchers)
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
assert.NoError(t, err)
assert.Empty(t, watchers)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
assert.Zero(t, repo.NumWatches)
}
+9 -3
View File
@@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
} }
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user. // ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) { func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id) affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
return err if err != nil {
return err
}
if n, _ := affected.RowsAffected(); n != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
} }
+15 -2
View File
@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
func TestToggleUserOpenIDVisibility(t *testing.T) { func TestToggleUserOpenIDVisibility(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
user, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2)) oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, oids, 1) require.Len(t, oids, 1)
assert.True(t, oids[0].Show) assert.True(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID) err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err) require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2)) oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
@@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
require.Len(t, oids, 1) require.Len(t, oids, 1)
assert.False(t, oids[0].Show) assert.False(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID) err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err) require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2)) oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
@@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
assert.True(t, oids[0].Show) assert.True(t, oids[0].Show)
} }
} }
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
require.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
}
+12
View File
@@ -1444,3 +1444,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
} }
return &setting.Admin.UserDisabledFeatures return &setting.Admin.UserDisabledFeatures
} }
// GetUserOrOrgByName returns the user or org by name
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
var u User
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist{Name: name}
}
return &u, nil
}
+39 -31
View File
@@ -26,9 +26,11 @@ type ConvertOpts struct {
KeepBOM bool KeepBOM bool
} }
var ToUTF8WithFallbackReaderPrefetchSize = 16 * 1024
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible // ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader { func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
buf := make([]byte, 2048) buf := make([]byte, ToUTF8WithFallbackReaderPrefetchSize)
n, err := util.ReadAtMost(rd, buf) n, err := util.ReadAtMost(rd, buf)
if err != nil { if err != nil {
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd) return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
@@ -41,6 +43,7 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
encoding, _ := charset.Lookup(charsetLabel) encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil { if encoding == nil {
log.Error("Unknown encoding: %s", charsetLabel)
return io.MultiReader(bytes.NewReader(buf[:n]), rd) return io.MultiReader(bytes.NewReader(buf[:n]), rd)
} }
@@ -54,17 +57,18 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
} }
// ToUTF8 converts content to UTF8 encoding // ToUTF8 converts content to UTF8 encoding
func ToUTF8(content []byte, opts ConvertOpts) (string, error) { func ToUTF8(content []byte, opts ConvertOpts) ([]byte, error) {
charsetLabel, err := DetectEncoding(content) charsetLabel, err := DetectEncoding(content)
if err != nil { if err != nil {
return "", err return content, err
} else if charsetLabel == "UTF-8" { } else if charsetLabel == "UTF-8" {
return string(MaybeRemoveBOM(content, opts)), nil return MaybeRemoveBOM(content, opts), nil
} }
encoding, _ := charset.Lookup(charsetLabel) encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil { if encoding == nil {
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel) log.Error("Unknown encoding: %s", charsetLabel)
return content, fmt.Errorf("unknown encoding: %s", charsetLabel)
} }
// If there is an error, we concatenate the nicely decoded part and the // If there is an error, we concatenate the nicely decoded part and the
@@ -76,7 +80,7 @@ func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
result = MaybeRemoveBOM(result, opts) result = MaybeRemoveBOM(result, opts)
return string(result), err return result, err
} }
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible // ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
@@ -94,6 +98,7 @@ func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte {
encoding, _ := charset.Lookup(charsetLabel) encoding, _ := charset.Lookup(charsetLabel)
if encoding == nil { if encoding == nil {
log.Error("Unknown encoding: %s", charsetLabel)
return content return content
} }
@@ -130,28 +135,37 @@ func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
} }
// DetectEncoding detect the encoding of content // DetectEncoding detect the encoding of content
func DetectEncoding(content []byte) (string, error) { // it always returns a detected or guessed "encoding" string, no matter error happens or not
func DetectEncoding(content []byte) (encoding string, _ error) {
// First we check if the content represents valid utf8 content excepting a truncated character at the end. // First we check if the content represents valid utf8 content excepting a truncated character at the end.
// Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do // Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do
// instead we walk backwards from the end to trim off a the incomplete character // instead we walk backwards from the end to trim off the incomplete character
toValidate := content toValidate := content
end := len(toValidate) - 1 end := len(toValidate) - 1
if end < 0 { // U+0000 U+007F 0yyyzzzz
// no-op // U+0080 U+07FF 110xxxyy 10yyzzzz
} else if toValidate[end]>>5 == 0b110 { // U+0800 U+FFFF 1110wwww 10xxxxyy 10yyzzzz
// Incomplete 1 byte extension e.g. © <c2><a9> which has been truncated to <c2> // U+010000 U+10FFFF 11110uvv 10vvwwww 10xxxxyy 10yyzzzz
toValidate = toValidate[:end] cnt := 0
} else if end > 0 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>4 == 0b1110 { for end >= 0 && cnt < 4 {
// Incomplete 2 byte extension e.g. ⛔ <e2><9b><94> which has been truncated to <e2><9b> c := toValidate[end]
toValidate = toValidate[:end-1] if c>>5 == 0b110 || c>>4 == 0b1110 || c>>3 == 0b11110 {
} else if end > 1 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>6 == 0b10 && toValidate[end-2]>>3 == 0b11110 { // a leading byte
// Incomplete 3 byte extension e.g. 💩 <f0><9f><92><a9> which has been truncated to <f0><9f><92> toValidate = toValidate[:end]
toValidate = toValidate[:end-2] break
} else if c>>6 == 0b10 {
// a continuation byte
end--
} else {
// not an utf-8 byte
break
}
cnt++
} }
if utf8.Valid(toValidate) { if utf8.Valid(toValidate) {
log.Debug("Detected encoding: utf-8 (fast)")
return "UTF-8", nil return "UTF-8", nil
} }
@@ -160,7 +174,7 @@ func DetectEncoding(content []byte) (string, error) {
if len(content) < 1024 { if len(content) < 1024 {
// Check if original content is valid // Check if original content is valid
if _, err := textDetector.DetectBest(content); err != nil { if _, err := textDetector.DetectBest(content); err != nil {
return "", err return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
} }
times := 1024 / len(content) times := 1024 / len(content)
detectContent = make([]byte, 0, times*len(content)) detectContent = make([]byte, 0, times*len(content))
@@ -171,14 +185,10 @@ func DetectEncoding(content []byte) (string, error) {
detectContent = content detectContent = content
} }
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie break // Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie-break
results, err := textDetector.DetectAll(detectContent) results, err := textDetector.DetectAll(detectContent)
if err != nil { if err != nil {
if err == chardet.NotDetectedError && len(setting.Repository.AnsiCharset) > 0 { return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
return setting.Repository.AnsiCharset, nil
}
return "", err
} }
topConfidence := results[0].Confidence topConfidence := results[0].Confidence
@@ -201,11 +211,9 @@ func DetectEncoding(content []byte) (string, error) {
} }
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument // FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
if topResult.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 { if topResult.Charset != "UTF-8" && setting.Repository.AnsiCharset != "" {
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
return setting.Repository.AnsiCharset, err return setting.Repository.AnsiCharset, err
} }
log.Debug("Detected encoding: %s", topResult.Charset) return topResult.Charset, nil
return topResult.Charset, err
} }
+35 -146
View File
@@ -4,12 +4,12 @@
package charset package charset
import ( import (
"bytes"
"io" "io"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -47,12 +47,12 @@ func TestToUTF8(t *testing.T) {
res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "ABC", res) assert.Equal(t, "ABC", string(res))
// "áéíóú" // "áéíóú"
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res)) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
// "áéíóú" // "áéíóú"
res, err = ToUTF8([]byte{ res, err = ToUTF8([]byte{
@@ -60,7 +60,7 @@ func TestToUTF8(t *testing.T) {
0xc3, 0xba, 0xc3, 0xba,
}, ConvertOpts{}) }, ConvertOpts{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res)) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
res, err = ToUTF8([]byte{ res, err = ToUTF8([]byte{
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
@@ -96,12 +96,11 @@ func TestToUTF8(t *testing.T) {
assert.Equal(t, []byte{ assert.Equal(t, []byte{
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3, 0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82, 0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
}, }, res)
[]byte(res))
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{}) res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res)) assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
} }
func TestToUTF8WithFallback(t *testing.T) { func TestToUTF8WithFallback(t *testing.T) {
@@ -231,152 +230,42 @@ func TestDetectEncoding(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func stringMustStartWith(t *testing.T, expected, value string) { func stringMustStartWith(t *testing.T, expected string, value []byte) {
assert.Equal(t, expected, value[:len(expected)]) assert.Equal(t, expected, string(value[:len(expected)]))
} }
func stringMustEndWith(t *testing.T, expected, value string) { func stringMustEndWith(t *testing.T, expected string, value []byte) {
assert.Equal(t, expected, value[len(value)-len(expected):]) assert.Equal(t, expected, string(value[len(value)-len(expected):]))
} }
func TestToUTF8WithFallbackReader(t *testing.T) { func TestToUTF8WithFallbackReader(t *testing.T) {
resetDefaultCharsetsOrder() resetDefaultCharsetsOrder()
test.MockVariableValue(&ToUTF8WithFallbackReaderPrefetchSize)
for testLen := range 2048 { block := "aá啊🤔"
pattern := " test { () }\n" runes := []rune(block)
input := "" assert.Len(t, string(runes[0]), 1)
for len(input) < testLen { assert.Len(t, string(runes[1]), 2)
input += pattern assert.Len(t, string(runes[2]), 3)
} assert.Len(t, string(runes[3]), 4)
input = input[:testLen]
input += "// Выключаем" content := strings.Repeat(block, 2)
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)), ConvertOpts{}) for i := 1; i < len(content); i++ {
encoding, err := DetectEncoding([]byte(content[:i]))
assert.NoError(t, err)
assert.Equal(t, "UTF-8", encoding)
ToUTF8WithFallbackReaderPrefetchSize = i
rd := ToUTF8WithFallbackReader(strings.NewReader(content), ConvertOpts{})
r, _ := io.ReadAll(rd) r, _ := io.ReadAll(rd)
assert.Equalf(t, input, string(r), "testing string len=%d", testLen) assert.Equal(t, content, string(r))
}
for _, r := range runes {
content = "abc abc " + string(r) + string(r) + string(r)
for i := 0; i < len(content); i++ {
encoding, err := DetectEncoding([]byte(content[:i]))
assert.NoError(t, err)
assert.Equal(t, "UTF-8", encoding)
}
} }
truncatedOneByteExtension := failFastBytes
encoding, _ := DetectEncoding(truncatedOneByteExtension)
assert.Equal(t, "UTF-8", encoding)
truncatedTwoByteExtension := failFastBytes
truncatedTwoByteExtension[len(failFastBytes)-1] = 0x9b
truncatedTwoByteExtension[len(failFastBytes)-2] = 0xe2
encoding, _ = DetectEncoding(truncatedTwoByteExtension)
assert.Equal(t, "UTF-8", encoding)
truncatedThreeByteExtension := failFastBytes
truncatedThreeByteExtension[len(failFastBytes)-1] = 0x92
truncatedThreeByteExtension[len(failFastBytes)-2] = 0x9f
truncatedThreeByteExtension[len(failFastBytes)-3] = 0xf0
encoding, _ = DetectEncoding(truncatedThreeByteExtension)
assert.Equal(t, "UTF-8", encoding)
}
var failFastBytes = []byte{
0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67, 0x2e, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x74, 0x6f,
0x6f, 0x6c, 0x73, 0x2e, 0x61, 0x6e, 0x74, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x64, 0x65, 0x66, 0x73, 0x2e, 0x63, 0x6f, 0x6e,
0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x73, 0x0a, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67,
0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f,
0x74, 0x2e, 0x67, 0x72, 0x61, 0x64, 0x6c, 0x65, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x75, 0x6e, 0x2e, 0x42,
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x0a, 0x0a, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x64, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d,
0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65,
0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x2d, 0x64, 0x6f, 0x63, 0x73, 0x22, 0x29,
0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x64, 0x62,
0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x66,
0x73, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6d, 0x71, 0x22, 0x29, 0x29, 0x0a, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22,
0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
0x2d, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74,
0x65, 0x72, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63,
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x68, 0x61, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e,
0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x29, 0x0a,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28,
0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b,
0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74,
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x77, 0x65, 0x62, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69,
0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72,
0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x6f, 0x70,
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f,
0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d,
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x29, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f,
0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74,
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
0x72, 0x74, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2d, 0x61, 0x6c, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
0x72, 0x74, 0x65, 0x72, 0x2d, 0x73, 0x6c, 0x65, 0x75, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70,
0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x72, 0x65, 0x74, 0x72, 0x79, 0x3a,
0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x72, 0x65, 0x74, 0x72, 0x79, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x63, 0x68, 0x2e, 0x71,
0x6f, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x3a, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x2d, 0x63,
0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d,
0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x69, 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65,
0x74, 0x65, 0x72, 0x3a, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x67, 0x69, 0x73,
0x74, 0x72, 0x79, 0x2d, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x6b, 0x6f, 0x74,
0x6c, 0x69, 0x6e, 0x28, 0x22, 0x73, 0x74, 0x64, 0x6c, 0x69, 0x62, 0x22, 0x29, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
0x2f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64,
0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
0x65, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a,
0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d,
0x74, 0x65, 0x73, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a,
0x61, 0x72, 0x20, 0x62, 0x79, 0x20, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x69, 0x6e, 0x67, 0x28, 0x4a, 0x61, 0x72, 0x3a, 0x3a, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e,
0x73, 0x65, 0x74, 0x28, 0x22, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x76, 0x61, 0x6c, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68,
0x20, 0x62, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x67,
0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
0x73, 0x28, 0x22, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x2d, 0x50, 0x61, 0x74, 0x68, 0x22, 0x20, 0x74, 0x6f, 0x20, 0x6f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70,
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x20, 0x3d,
0x20, 0x22, 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x2b, 0x22, 0x2e, 0x74, 0x6f, 0x52, 0x65, 0x67, 0x65, 0x78, 0x28, 0x29,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64,
0x65, 0x20, 0x66, 0x75, 0x6e, 0x20, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x3a, 0x20, 0x53, 0x74,
0x72, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70,
0x61, 0x74, 0x68, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x6f, 0x53, 0x74, 0x72, 0x69,
0x6e, 0x67, 0x28, 0x22, 0x20, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x74, 0x2e, 0x74, 0x6f, 0x55, 0x52, 0x49, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x55,
0x52, 0x4c, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x2e, 0x72, 0x65, 0x70, 0x6c,
0x61, 0x63, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x28, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x2c, 0x20, 0x22, 0x2f,
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x7d, 0x0a, 0x0a, 0x74, 0x61, 0x73,
0x6b, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x3c, 0x42, 0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x3e, 0x28, 0x22, 0x62,
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x4f,
0x73, 0x2e, 0x69, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x28, 0x4f, 0x73, 0x2e, 0x46, 0x41, 0x4d, 0x49, 0x4c, 0x59,
0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x29, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68, 0x20, 0x3d, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x28, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x28, 0x22, 0x6d, 0x61, 0x69,
0x6e, 0x22, 0x29, 0x2e, 0x6d, 0x61, 0x70, 0x20, 0x7b, 0x20, 0x69, 0x74, 0x2e, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20,
0x7d, 0x2c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a, 0x61, 0x72, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0xd0,
} }
+8 -1
View File
@@ -9,6 +9,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -91,7 +92,13 @@ loop:
} }
for _, userStopwatches := range usersStopwatches { for _, userStopwatches := range usersStopwatches {
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches) u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
if err != nil {
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
continue
}
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
if err != nil { if err != nil {
if !issues_model.IsErrIssueNotExist(err) { if !issues_model.IsErrIssueNotExist(err) {
log.Error("Unable to APIFormat stopwatches: %v", err) log.Error("Unable to APIFormat stopwatches: %v", err)
+1 -1
View File
@@ -76,7 +76,7 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
if p.IconSVGs[svgID] == "" { if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = svgHTML p.IconSVGs[svgID] = svgHTML
} }
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) return template.HTML(`<svg ` + svgCommonAttrs + `><use href="#` + svgID + `"></use></svg>`)
} }
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML { func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
+1 -1
View File
@@ -25,7 +25,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return "" return ""
} }
sb := &strings.Builder{} sb := &strings.Builder{}
sb.WriteString(`<div class=tw-hidden>`) sb.WriteString(`<div class="svg-icon-container">`)
for _, icon := range p.IconSVGs { for _, icon := range p.IconSVGs {
sb.WriteString(string(icon)) sb.WriteString(string(icon))
} }
+19 -5
View File
@@ -9,24 +9,38 @@ package git
import ( import (
"io" "io"
"code.gitea.io/gitea/modules/log"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
// Blob represents a Git object. // Blob represents a Git object.
type Blob struct { type Blob struct {
ID ObjectID ID ObjectID
repo *Repository
name string
}
gogitEncodedObj plumbing.EncodedObject func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
name string return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
} }
// DataAsync gets a ReadCloser for the contents of a blob without reading it all. // DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output. // Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) { func (b *Blob) DataAsync() (io.ReadCloser, error) {
return b.gogitEncodedObj.Reader() obj, err := b.gogitEncodedObj()
if err != nil {
return nil, err
}
return obj.Reader()
} }
// Size returns the uncompressed size of the blob // Size returns the uncompressed size of the blob
func (b *Blob) Size() int64 { func (b *Blob) Size() int64 {
return b.gogitEncodedObj.Size() obj, err := b.gogitEncodedObj()
if err != nil {
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
return 0
}
return obj.Size()
} }
+5 -3
View File
@@ -30,9 +30,11 @@ type Parser struct {
func NewParser(r io.Reader, format Format) *Parser { func NewParser(r io.Reader, format Format) *Parser {
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
// default MaxScanTokenSize = 64 kiB may be too small for some references, // default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
// so allow the buffer to grow up to 4x if needed // so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
scanner.Buffer(nil, 4*bufio.MaxScanTokenSize) // as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
scanner.Buffer(nil, 4*1024*1024)
// in addition to the reference delimiter we specified in the --format, // in addition to the reference delimiter we specified in the --format,
// `git for-each-ref` will always add a newline after every reference. // `git for-each-ref` will always add a newline after every reference.
+7 -1
View File
@@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return repo.getBlob(id) if id.IsZero() {
return nil, ErrNotExist{id.String(), ""}
}
return &Blob{
ID: id,
repo: repo,
}, nil
} }
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"github.com/go-git/go-git/v5/plumbing"
)
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
encodedObj, err := repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(id.RawValue()))
if err != nil {
return nil, ErrNotExist{id.String(), ""}
}
return &Blob{
ID: id,
gogitEncodedObj: encodedObj,
}, nil
}
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
func (repo *Repository) getBlob(id ObjectID) (*Blob, error) {
if id.IsZero() {
return nil, ErrNotExist{id.String(), ""}
}
return &Blob{
ID: id,
repo: repo,
}, nil
}
+3 -9
View File
@@ -7,7 +7,6 @@
package git package git
import ( import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
@@ -83,14 +82,9 @@ func (te *TreeEntry) IsExecutable() bool {
// Blob returns the blob object the entry // Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob { func (te *TreeEntry) Blob() *Blob {
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
if err != nil {
return nil
}
return &Blob{ return &Blob{
ID: ParseGogitHash(te.gogitTreeEntry.Hash), ID: ParseGogitHash(te.gogitTreeEntry.Hash),
gogitEncodedObj: encodedObj, repo: te.ptree.repo,
name: te.Name(), name: te.Name(),
} }
} }
@@ -30,6 +30,10 @@ func TestMathRender(t *testing.T) {
"$ a $", "$ a $",
`<p><code class="language-math">a</code></p>` + nl, `<p><code class="language-math">a</code></p>` + nl,
}, },
{
"$a$$b$",
`<p><code class="language-math">a</code><code class="language-math">b</code></p>` + nl,
},
{ {
"$a$ $b$", "$a$ $b$",
`<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl, `<p><code class="language-math">a</code> <code class="language-math">b</code></p>` + nl,
@@ -59,7 +63,7 @@ func TestMathRender(t *testing.T) {
`<p>a$b $a a$b b$</p>` + nl, `<p>a$b $a a$b b$</p>` + nl,
}, },
{ {
"a$x$", "a$x$", // Pattern: "word$other$" The real world example is: "Price is between US$1 and US$2.", so don't parse this.
`<p>a$x$</p>` + nl, `<p>a$x$</p>` + nl,
}, },
{ {
@@ -70,6 +74,10 @@ func TestMathRender(t *testing.T) {
"$a$ ($b$) [$c$] {$d$}", "$a$ ($b$) [$c$] {$d$}",
`<p><code class="language-math">a</code> (<code class="language-math">b</code>) [$c$] {$d$}</p>` + nl, `<p><code class="language-math">a</code> (<code class="language-math">b</code>) [$c$] {$d$}</p>` + nl,
}, },
{
"[$a$](link)",
`<p><a href="/link" rel="nofollow"><code class="language-math">a</code></a></p>` + nl,
},
{ {
"$$a$$", "$$a$$",
`<p><code class="language-math">a</code></p>` + nl, `<p><code class="language-math">a</code></p>` + nl,
@@ -54,6 +54,10 @@ func isAlphanumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
} }
func isInMarkdownLinkText(block text.Reader, lineAfter []byte) bool {
return block.PrecendingCharacter() == '[' && bytes.HasPrefix(lineAfter, []byte("]("))
}
// Parse parses the current line and returns a result of parsing. // Parse parses the current line and returns a result of parsing.
func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine() line, _ := block.PeekLine()
@@ -115,7 +119,9 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
} }
// check valid ending character // check valid ending character
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) || isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 ||
succeedingCharacter == '$' ||
isInMarkdownLinkText(block, line[i+len(stopMark):])
if checkSurrounding && !isValidEndingChar { if checkSurrounding && !isValidEndingChar {
break break
} }
+6 -3
View File
@@ -10,9 +10,12 @@ import (
// ReleaseAsset represents a release asset // ReleaseAsset represents a release asset
type ReleaseAsset struct { type ReleaseAsset struct {
ID int64 ID int64
Name string Name string
ContentType *string `yaml:"content_type"`
// There was a field "ContentType (content_type)" because Some forges can provide that for assets,
// but we don't need it when migrating, so the field is omitted here.
Size *int Size *int
DownloadCount *int `yaml:"download_count"` DownloadCount *int `yaml:"download_count"`
Created time.Time Created time.Time
+23 -2
View File
@@ -62,7 +62,28 @@ type PackageMetadata struct {
Author User `json:"author"` Author User `json:"author"`
ReadmeFilename string `json:"readmeFilename,omitempty"` ReadmeFilename string `json:"readmeFilename,omitempty"`
Users map[string]bool `json:"users,omitempty"` Users map[string]bool `json:"users,omitempty"`
License string `json:"license,omitempty"` License License `json:"license,omitempty"`
}
type License string
func (l *License) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*l = License(value)
case '{':
var values map[string]any
if err := json.Unmarshal(data, &values); err != nil {
return err
}
value, _ := values["type"].(string)
*l = License(value)
}
return nil
} }
// PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version // PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
@@ -74,7 +95,7 @@ type PackageMetadataVersion struct {
Description string `json:"description"` Description string `json:"description"`
Author User `json:"author"` Author User `json:"author"`
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
License string `json:"license,omitempty"` License License `json:"license,omitempty"`
Repository Repository `json:"repository"` Repository Repository `json:"repository"`
Keywords []string `json:"keywords,omitempty"` Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"`
+27 -1
View File
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParsePackage(t *testing.T) { func TestParsePackage(t *testing.T) {
@@ -291,11 +292,36 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, packageDescription, p.Metadata.Readme) assert.Equal(t, packageDescription, p.Metadata.Readme)
assert.Equal(t, packageAuthor, p.Metadata.Author) assert.Equal(t, packageAuthor, p.Metadata.Author)
assert.Equal(t, packageBin, p.Metadata.Bin["bin"]) assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
assert.Equal(t, "MIT", p.Metadata.License) assert.Equal(t, "MIT", string(p.Metadata.License))
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL) assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
assert.Contains(t, p.Metadata.Dependencies, "package") assert.Contains(t, p.Metadata.Dependencies, "package")
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"]) assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
assert.Equal(t, repository.Type, p.Metadata.Repository.Type) assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
assert.Equal(t, repository.URL, p.Metadata.Repository.URL) assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
}) })
t.Run("ValidLicenseMap", func(t *testing.T) {
packageJSON := `{
"versions": {
"0.1.1": {
"name": "dev-null",
"version": "0.1.1",
"license": {
"type": "MIT"
},
"dist": {
"integrity": "sha256-"
}
}
},
"_attachments": {
"foo": {
"data": "AAAA"
}
}
}`
p, err := ParsePackage(strings.NewReader(packageJSON))
require.NoError(t, err)
require.Equal(t, "MIT", string(p.Metadata.License))
})
} }
+1 -1
View File
@@ -12,7 +12,7 @@ type Metadata struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"` Author string `json:"author,omitempty"`
License string `json:"license,omitempty"` License License `json:"license,omitempty"`
ProjectURL string `json:"project_url,omitempty"` ProjectURL string `json:"project_url,omitempty"`
Keywords []string `json:"keywords,omitempty"` Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"`
+1 -1
View File
@@ -233,7 +233,7 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR
return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
} }
} }
added, deleted, updated = len(deletes), len(updates), len(inserts) added, deleted, updated = len(inserts), len(deletes), len(updates)
return nil return nil
}) })
if err != nil { if err != nil {
+2 -2
View File
@@ -327,14 +327,14 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
if rootCfg.Section(oldSection).HasKey(oldKey) { if rootCfg.Section(oldSection).HasKey(oldKey) {
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
} }
} }
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
if rootCfg.Section(oldSection).HasKey(oldKey) { if rootCfg.Section(oldSection).HasKey(oldKey) {
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
} }
} }
+23
View File
@@ -107,6 +107,17 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) {
return brands, true return brands, true
} }
func isEmbeddedOpenType(data []byte) bool {
// https://www.w3.org/submissions/EOT
if len(data) < 80 {
return false
}
version := binary.LittleEndian.Uint32(data[8:]) // Actually this standard is abandoned (for IE6-IE11 only), there are only 3 versions defined
magic := binary.LittleEndian.Uint16(data[34:36]) // MagicNumber: 0x504C ("LP")
reserved := data[64:80] // Reserved 1-4 (each: unsigned long)
return (version == 0x00010000 || version == 0x00020001 || version == 0x00020002) && magic == 0x504C && bytes.Count(reserved, []byte{0}) == len(reserved)
}
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty. // DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty.
func DetectContentType(data []byte) SniffedType { func DetectContentType(data []byte) SniffedType {
if len(data) == 0 { if len(data) == 0 {
@@ -119,6 +130,18 @@ func DetectContentType(data []byte) SniffedType {
data = data[:SniffContentSize] data = data[:SniffContentSize]
} }
const typeMsFontObject = "application/vnd.ms-fontobject"
if ct == typeMsFontObject {
// Stupid Golang blindly detects any content with 34th-35th bytes being "LP" as font.
// If it is not really for ".eot" content, we try to detect it again by hiding the "LP", see the test for more details.
if isEmbeddedOpenType(data) {
return SniffedType{typeMsFontObject}
}
data = slices.Clone(data)
data[34] = 'l'
ct = http.DetectContentType(data)
}
vars := globalVars() vars := globalVars()
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888 // SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html") detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
+23
View File
@@ -6,6 +6,7 @@ package typesniffer
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"net/http"
"strings" "strings"
"testing" "testing"
@@ -154,3 +155,25 @@ func TestDetectContentTypeAvif(t *testing.T) {
st := DetectContentType(buf) st := DetectContentType(buf)
assert.Equal(t, MimeTypeImageAvif, st.contentType) assert.Equal(t, MimeTypeImageAvif, st.contentType)
} }
func TestDetectContentTypeIncorrectFont(t *testing.T) {
s := "Stupid Golang keep detecting 34th LP as font"
// They don't want to have any improvement to it: https://github.com/golang/go/issues/77172
golangDetected := http.DetectContentType([]byte(s))
assert.Equal(t, "application/vnd.ms-fontobject", golangDetected)
// We have to make our patch to make it work correctly
ourDetected := DetectContentType([]byte(s))
assert.Equal(t, "text/plain; charset=utf-8", ourDetected.contentType)
// For binary content, ensure it still detects as font. The content is from "opensans-regular.eot"
b := []byte{
0x3d, 0x30, 0x00, 0x00, 0x6b, 0x2f, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00,
0x02, 0x0b, 0x06, 0x06, 0x03, 0x05, 0x04, 0x02, 0x02, 0x04, 0x01, 0x00, 0x90, 0x01, 0x00, 0x00,
0x04, 0x00, 0x4c, 0x50, 0xef, 0x02, 0x00, 0xe0, 0x5b, 0x20, 0x00, 0x40, 0x28, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x9f, 0x01, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf4, 0x17, 0x14,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x12, 0x00, 0x4f, 0x00, 0x70, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x20, 0x00, 0x53, 0x00,
}
assert.Equal(t, "application/vnd.ms-fontobject", http.DetectContentType(b))
assert.Equal(t, "application/vnd.ms-fontobject", DetectContentType(b).contentType)
}
+5 -1
View File
@@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
// //
// Then the mock function will be executed as a middleware at the mock point. // Then the mock function will be executed as a middleware at the mock point.
// It only takes effect in testing mode (setting.IsInTesting == true). // It only takes effect in testing mode (setting.IsInTesting == true).
func RouteMock(pointName string, h any) { func RouteMock(pointName string, h any) func() {
if _, ok := routeMockPoints[pointName]; !ok { if _, ok := routeMockPoints[pointName]; !ok {
panic("route mock point not found: " + pointName) panic("route mock point not found: " + pointName)
} }
old := routeMockPoints[pointName]
routeMockPoints[pointName] = toHandlerProvider(h) routeMockPoints[pointName] = toHandlerProvider(h)
return func() {
routeMockPoints[pointName] = old
}
} }
// RouteMockReset resets all mock points (no mock anymore) // RouteMockReset resets all mock points (no mock anymore)
+1 -1
View File
@@ -55,7 +55,7 @@ func NewRouter() *Router {
// Use supports two middlewares // Use supports two middlewares
func (r *Router) Use(middlewares ...any) { func (r *Router) Use(middlewares ...any) {
for _, m := range middlewares { for _, m := range middlewares {
if m != nil { if !isNilOrFuncNil(m) {
r.chiRouter.Use(toHandlerProvider(m)) r.chiRouter.Use(toHandlerProvider(m))
} }
} }
+2
View File
@@ -214,6 +214,7 @@ more = More
buttons.heading.tooltip = Add heading buttons.heading.tooltip = Add heading
buttons.bold.tooltip = Add bold text buttons.bold.tooltip = Add bold text
buttons.italic.tooltip = Add italic text buttons.italic.tooltip = Add italic text
buttons.strikethrough.tooltip = Add strikethrough text
buttons.quote.tooltip = Quote text buttons.quote.tooltip = Quote text
buttons.code.tooltip = Add code buttons.code.tooltip = Add code
buttons.link.tooltip = Add a link buttons.link.tooltip = Add a link
@@ -1857,6 +1858,7 @@ pulls.desc = Enable pull requests and code reviews.
pulls.new = New Pull Request pulls.new = New Pull Request
pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner.
pulls.new.must_collaborator = You must be a collaborator to create pull request. pulls.new.must_collaborator = You must be a collaborator to create pull request.
pulls.new.already_existed = A pull request between these branches already exists
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes. pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes.
pulls.view = View Pull Request pulls.view = View Pull Request
pulls.compare_changes = New Pull Request pulls.compare_changes = New Pull Request
+7 -10
View File
@@ -290,8 +290,8 @@ func PostBlobsUploads(ctx *context.Context) {
Creator: ctx.Doer, Creator: ctx.Doer,
}, },
); err != nil { ); err != nil {
switch err { switch {
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
apiError(ctx, http.StatusForbidden, err) apiError(ctx, http.StatusForbidden, err)
default: default:
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@@ -439,8 +439,8 @@ func PutBlobsUpload(ctx *context.Context) {
Creator: ctx.Doer, Creator: ctx.Doer,
}, },
); err != nil { ); err != nil {
switch err { switch {
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize: case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
apiError(ctx, http.StatusForbidden, err) apiError(ctx, http.StatusForbidden, err)
default: default:
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
@@ -592,13 +592,10 @@ func PutManifest(ctx *context.Context) {
apiErrorDefined(ctx, namedError) apiErrorDefined(ctx, namedError)
} else if errors.Is(err, container_model.ErrContainerBlobNotExist) { } else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
apiErrorDefined(ctx, errBlobUnknown) apiErrorDefined(ctx, errBlobUnknown)
} else if errors.Is(err, packages_service.ErrQuotaTotalCount) || errors.Is(err, packages_service.ErrQuotaTypeSize) || errors.Is(err, packages_service.ErrQuotaTotalSize) {
apiError(ctx, http.StatusForbidden, err)
} else { } else {
switch err { apiError(ctx, http.StatusInternalServerError, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
} }
return return
} }
+21 -19
View File
@@ -82,9 +82,11 @@ type processManifestTxRet struct {
} }
func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) { func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) {
if err != nil && txRet.created && txRet.pb != nil { if err != nil {
if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil { if txRet.created && txRet.pb != nil {
log.Error("Error deleting package blob from content store: %v", err) if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil {
log.Error("Error deleting package blob from content store: %v", err)
}
} }
return "", err return "", err
} }
@@ -198,14 +200,14 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
if errors.Is(err, container_model.ErrContainerBlobNotExist) { if errors.Is(err, container_model.ErrContainerBlobNotExist) {
return errManifestBlobUnknown return errManifestBlobUnknown
} }
return err return fmt.Errorf("GetContainerBlob: %w", err)
} }
size, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{ size, err := packages_model.CalculateFileSize(ctx, &packages_model.PackageFileSearchOptions{
VersionID: pfd.File.VersionID, VersionID: pfd.File.VersionID,
}) })
if err != nil { if err != nil {
return err return fmt.Errorf("CalculateFileSize: %w", err)
} }
metadata.Manifests = append(metadata.Manifests, &container_module.Manifest{ metadata.Manifests = append(metadata.Manifests, &container_module.Manifest{
@@ -217,7 +219,7 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
pv, err := createPackageAndVersion(ctx, mci, metadata) pv, err := createPackageAndVersion(ctx, mci, metadata)
if err != nil { if err != nil {
return err return fmt.Errorf("createPackageAndVersion: %w", err)
} }
txRet.pv = pv txRet.pv = pv
@@ -240,7 +242,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackage) { if !errors.Is(err, packages_model.ErrDuplicatePackage) {
log.Error("Error inserting package: %v", err) log.Error("Error inserting package: %v", err)
return nil, err return nil, fmt.Errorf("TryInsertPackage: %w", err)
} }
created = false created = false
} }
@@ -248,7 +250,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
if created { if created {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(mci.Owner.LowerName+"/"+mci.Image)); err != nil {
log.Error("Error setting package property: %v", err) log.Error("Error setting package property: %v", err)
return nil, err return nil, fmt.Errorf("InsertProperty(PropertyRepository): %w", err)
} }
} }
@@ -256,7 +258,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
metadataJSON, err := json.Marshal(metadata) metadataJSON, err := json.Marshal(metadata)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("json.Marshal(metadata): %w", err)
} }
// "docker buildx imagetools create" multi-arch operations: // "docker buildx imagetools create" multi-arch operations:
@@ -276,43 +278,43 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
pv, err := packages_model.GetOrInsertVersion(ctx, _pv) pv, err := packages_model.GetOrInsertVersion(ctx, _pv)
if err != nil { if err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
log.Error("Error inserting package: %v", err) log.Error("Error GetOrInsertVersion (first try) package: %v", err)
return nil, err return nil, fmt.Errorf("GetOrInsertVersion: first try: %w", err)
} }
if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
return nil, err return nil, fmt.Errorf("DeletePackageVersionAndReferences: %w", err)
} }
// keep download count on overwriting // keep download count on overwriting
_pv.DownloadCount = pv.DownloadCount _pv.DownloadCount = pv.DownloadCount
pv, err = packages_model.GetOrInsertVersion(ctx, _pv) pv, err = packages_model.GetOrInsertVersion(ctx, _pv)
if err != nil { if err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
log.Error("Error inserting package: %v", err) log.Error("Error GetOrInsertVersion (second try) package: %v", err)
return nil, err return nil, fmt.Errorf("GetOrInsertVersion: second try: %w", err)
} }
} }
} }
if err := packages_service.CheckCountQuotaExceeded(ctx, mci.Creator, mci.Owner); err != nil { if err := packages_service.CheckCountQuotaExceeded(ctx, mci.Creator, mci.Owner); err != nil {
return nil, err return nil, fmt.Errorf("CheckCountQuotaExceeded: %w", err)
} }
if mci.IsTagged { if mci.IsTagged {
if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
return nil, err return nil, fmt.Errorf("InsertOrUpdateProperty(ManifestTagged): %w", err)
} }
} else { } else {
if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil { if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil {
return nil, err return nil, fmt.Errorf("DeletePropertiesByName(ManifestTagged): %w", err)
} }
} }
if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil { if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil {
return nil, err return nil, fmt.Errorf("DeletePropertiesByName(ManifestReference): %w", err)
} }
for _, manifest := range metadata.Manifests { for _, manifest := range metadata.Manifests {
if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
return nil, err return nil, fmt.Errorf("InsertProperty(ManifestReference): %w", err)
} }
} }
+4 -5
View File
@@ -216,9 +216,12 @@ func EditUser(ctx *context.APIContext) {
} }
if form.Email != nil { if form.Email != nil {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch { switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
case user_model.IsErrEmailAlreadyUsed(err): case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err) ctx.APIError(http.StatusBadRequest, err)
@@ -227,10 +230,6 @@ func EditUser(ctx *context.APIContext) {
} }
return return
} }
if !user_model.IsEmailDomainAllowed(*form.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
}
} }
opts := &user_service.UpdateOptions{ opts := &user_service.UpdateOptions{
+10 -10
View File
@@ -153,7 +153,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
context.RedirectToUser(ctx.Base, userName, redirectUserID) context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) { } else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetUserByName", err) ctx.APIErrorNotFound("GetUserByName", err)
} else { } else {
@@ -629,7 +629,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org")) redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org"))
if err == nil { if err == nil {
context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID) context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) { } else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetOrgByName", err) ctx.APIErrorNotFound("GetOrgByName", err)
} else { } else {
@@ -1400,19 +1400,19 @@ func Routes() *web.Router {
}) })
m.Get("/{base}/*", repo.GetPullRequestByBaseHead) m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) }, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
m.Group("/statuses", func() { m.Group("/statuses", func() { // "/statuses/{sha}" only accepts commit ID
m.Combo("/{sha}").Get(repo.GetCommitStatuses). m.Combo("/{sha}").Get(repo.GetCommitStatuses).
Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus) Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(unit.TypeCode)) }, reqRepoReader(unit.TypeCode))
m.Group("/commits", func() { m.Group("/commits", func() {
m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits) m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
m.Group("/{ref}", func() { m.PathGroup("/*", func(g *web.RouterPathGroup) {
m.Get("/status", repo.GetCombinedCommitStatusByRef) // Mis-configured reverse proxy might decode the `%2F` to slash ahead, so we need to support both formats (escaped, unescaped) here.
m.Get("/statuses", repo.GetCommitStatusesByRef) // It also matches GitHub's behavior
}, context.ReferencesGitRepo()) g.MatchPath("GET", "/<ref:*>/status", repo.GetCombinedCommitStatusByRef)
m.Group("/{sha}", func() { g.MatchPath("GET", "/<ref:*>/statuses", repo.GetCommitStatusesByRef)
m.Get("/pull", repo.GetCommitPullRequest) g.MatchPath("GET", "/<sha>/pull", repo.GetCommitPullRequest)
}, context.ReferencesGitRepo()) })
}, reqRepoReader(unit.TypeCode)) }, reqRepoReader(unit.TypeCode))
m.Group("/git", func() { m.Group("/git", func() {
m.Group("/commits", func() { m.Group("/commits", func() {
+1 -1
View File
@@ -224,7 +224,7 @@ func GetStopwatches(ctx *context.APIContext) {
return return
} }
apiSWs, err := convert.ToStopWatches(ctx, sws) apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
+13 -17
View File
@@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"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"
"code.gitea.io/gitea/routers/common"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@@ -1076,7 +1077,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo) // There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1] headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserByName(ctx, headInfos[0]) headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("GetUserByName") ctx.APIErrorNotFound("GetUserByName")
@@ -1092,28 +1093,23 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
isSameRepo := ctx.Repo.Owner.ID == headUser.ID isSameRepo := ctx.Repo.Owner.ID == headUser.ID
// Check if current user has fork of repository or in the same repository. var headRepo *repo_model.Repository
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) if isSameRepo {
if headRepo == nil && !isSameRepo { headRepo = baseRepo
err = baseRepo.GetBaseRepo(ctx) } else {
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return nil, nil return nil, nil
} }
if headRepo == nil {
// Check if baseRepo's base repository is the same as headUser's repository. ctx.APIErrorNotFound("head repository not found")
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.APIErrorNotFound("GetBaseRepo")
return nil, nil return nil, nil
} }
// Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo
} }
var headGitRepo *git.Repository var headGitRepo *git.Repository
if isSameRepo { if isSameRepo {
headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo closer = func() {} // no need to close the head repo because it shares the base repo
} else { } else {
@@ -1137,7 +1133,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil return nil, nil
} }
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { if !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode") ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil return nil, nil
@@ -1341,7 +1337,7 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) {
} }
if ctx.Doer.ID != autoMerge.DoerID { if ctx.Doer.ID != autoMerge.DoerID {
allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer) allowed, err := pull_service.IsUserAllowedToMerge(ctx, pull, ctx.Repo.Permission, ctx.Doer)
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
return return
@@ -1552,9 +1548,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
var prInfo *pull_service.CompareInfo var prInfo *pull_service.CompareInfo
if pr.HasMerged { if pr.HasMerged {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), true, false) prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
} else { } else {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), true, false) prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
} }
if err != nil { if err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
@@ -398,7 +398,6 @@ func DeleteReleaseAttachment(ctx *context.APIContext) {
ctx.APIErrorNotFound() ctx.APIErrorNotFound()
return return
} }
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil { if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
ctx.APIErrorInternal(err) ctx.APIErrorInternal(err)
+1 -1
View File
@@ -16,7 +16,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User {
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil { if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil {
context.RedirectToUser(ctx.Base, username, redirectUserID) context.RedirectToUser(ctx.Base, ctx.Doer, username, redirectUserID)
} else { } else {
ctx.APIErrorNotFound("GetUserByName", err) ctx.APIErrorNotFound("GetUserByName", err)
} }
+2
View File
@@ -44,9 +44,11 @@ func isRoutePathExpensive(routePattern string) bool {
"/{username}/{reponame}/blame/", "/{username}/{reponame}/blame/",
"/{username}/{reponame}/commit/", "/{username}/{reponame}/commit/",
"/{username}/{reponame}/commits/", "/{username}/{reponame}/commits/",
"/{username}/{reponame}/compare/",
"/{username}/{reponame}/graph", "/{username}/{reponame}/graph",
"/{username}/{reponame}/media/", "/{username}/{reponame}/media/",
"/{username}/{reponame}/raw/", "/{username}/{reponame}/raw/",
"/{username}/{reponame}/rss/branch/",
"/{username}/{reponame}/src/", "/{username}/{reponame}/src/",
// issue & PR related (no trailing slash) // issue & PR related (no trailing slash)
+53
View File
@@ -4,6 +4,8 @@
package common package common
import ( import (
"context"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@@ -20,3 +22,54 @@ type CompareInfo struct {
HeadBranch string HeadBranch string
DirectComparison bool DirectComparison bool
} }
// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository.
const maxForkTraverseLevel = 10
// FindHeadRepo tries to find the head repository based on the base repository and head user ID.
func FindHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
if baseRepo.IsFork {
curRepo := baseRepo
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
if err := curRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
if curRepo.BaseRepo == nil {
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, maxForkTraverseLevel)
}
curRepo = curRepo.BaseRepo
}
return curRepo, nil
}
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel)
}
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
if traverseLevel == 0 {
return nil, nil
}
// test if we are lucky
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
if err != nil {
return nil, err
}
if repo != nil {
return repo, nil
}
firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
if err != nil {
return nil, err
}
for _, repo := range firstLevelForkedRepos {
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
if err != nil {
return nil, err
}
if forked != nil {
return forked, nil
}
}
return nil, nil
}
+5 -2
View File
@@ -71,8 +71,11 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
req = req.WithContext(cache.WithCacheContext(ctx)) req = req.WithContext(cache.WithCacheContext(ctx))
ds.SetContextValue(httplib.RequestContextKey, req) ds.SetContextValue(httplib.RequestContextKey, req)
ds.AddCleanUp(func() { ds.AddCleanUp(func() {
if req.MultipartForm != nil { // The req in context might have changed due to the new req.WithContext calls
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory // For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
if ctxReq.MultipartForm != nil {
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
} }
}) })
next.ServeHTTP(respWriter, req) next.ServeHTTP(respWriter, req)
+48 -39
View File
@@ -108,21 +108,19 @@ func ServCommand(ctx *context.PrivateContext) {
results.RepoName = repoName[:len(repoName)-5] results.RepoName = repoName[:len(repoName)-5]
} }
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err == nil {
owner, err := user_model.GetUserByID(ctx, redirectedUserID)
if err == nil {
log.Info("User %s has been redirected to %s", results.OwnerName, owner.Name)
results.OwnerName = owner.Name
} else {
log.Warn("User %s has a redirect to user with ID %d, but no user with this ID could be found. Trying without redirect...", results.OwnerName, redirectedUserID)
}
}
owner, err := user_model.GetUserByName(ctx, results.OwnerName) owner, err := user_model.GetUserByName(ctx, results.OwnerName)
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if !user_model.IsErrUserNotExist(err) {
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
// Check if there is a user redirect for the requested owner
redirectedUserID, err := user_model.LookupUserRedirect(ctx, results.OwnerName)
if err != nil {
// User is fetching/cloning a non-existent repository // User is fetching/cloning a non-existent repository
log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{ ctx.JSON(http.StatusNotFound, private.Response{
@@ -130,11 +128,20 @@ func ServCommand(ctx *context.PrivateContext) {
}) })
return return
} }
log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusForbidden, private.Response{ redirectUser, err := user_model.GetUserByID(ctx, redirectedUserID)
UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), if err != nil {
}) // User is fetching/cloning a non-existent repository
return log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr())
ctx.JSON(http.StatusNotFound, private.Response{
UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName),
})
return
}
log.Info("User %s has been redirected to %s", results.OwnerName, redirectUser.Name)
results.OwnerName = redirectUser.Name
owner = redirectUser
} }
if !owner.IsOrganization() && !owner.IsActive { if !owner.IsOrganization() && !owner.IsActive {
ctx.JSON(http.StatusForbidden, private.Response{ ctx.JSON(http.StatusForbidden, private.Response{
@@ -143,24 +150,33 @@ func ServCommand(ctx *context.PrivateContext) {
return return
} }
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
// Now get the Repository and set the results section // Now get the Repository and set the results section
repoExist := true repoExist := true
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName) repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, results.RepoName)
if err != nil { if err != nil {
if repo_model.IsErrRepoNotExist(err) { if !repo_model.IsErrRepoNotExist(err) {
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
}
redirectedRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, results.RepoName)
if err == nil {
redirectedRepo, err := repo_model.GetRepositoryByID(ctx, redirectedRepoID)
if err == nil {
log.Info("Repository %s/%s has been redirected to %s/%s", results.OwnerName, results.RepoName, redirectedRepo.OwnerName, redirectedRepo.Name)
results.RepoName = redirectedRepo.Name
results.OwnerName = redirectedRepo.OwnerName
repo = redirectedRepo
owner.ID = redirectedRepo.OwnerID
} else {
log.Warn("Repo %s/%s has a redirect to repo with ID %d, but no repo with this ID could be found. Trying without redirect...", results.OwnerName, results.RepoName, redirectedRepoID)
}
}
if repo == nil {
repoExist = false repoExist = false
if mode == perm.AccessModeRead { if mode == perm.AccessModeRead {
// User is fetching/cloning a non-existent repository // User is fetching/cloning a non-existent repository
@@ -170,13 +186,6 @@ func ServCommand(ctx *context.PrivateContext) {
}) })
return return
} }
// else fallthrough (push-to-create may kick in below)
} else {
log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err),
})
return
} }
} }
+1 -1
View File
@@ -409,7 +409,7 @@ func EditUserPost(ctx *context.Context) {
} }
if form.Email != "" { if form.Email != "" {
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { if err := user_service.ReplacePrimaryEmailAddress(ctx, u, form.Email); err != nil {
switch { switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
ctx.Data["Err_Email"] = true ctx.Data["Err_Email"] = true
+4 -2
View File
@@ -277,8 +277,11 @@ type LinkAccountData struct {
GothUser goth.User GothUser goth.User
} }
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData { func init() {
gob.Register(LinkAccountData{}) gob.Register(LinkAccountData{})
}
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData) v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData)
if !ok { if !ok {
return nil return nil
@@ -287,7 +290,6 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
} }
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error { func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
gob.Register(LinkAccountData{})
return updateSession(ctx, nil, map[string]any{ return updateSession(ctx, nil, map[string]any{
"linkAccountData": linkAccountData, "linkAccountData": linkAccountData,
}) })
+28 -71
View File
@@ -205,22 +205,24 @@ func ChangeProjectStatus(ctx *context.Context) {
} }
id := ctx.PathParamInt64("id") id := ctx.PathParamInt64("id")
if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, id, toClose); err != nil { project, err := project_model.GetProjectByIDAndOwner(ctx, id, ctx.ContextUser.ID)
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
return
}
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id))
}
// DeleteProject delete a project
func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
if p.OwnerID != ctx.ContextUser.ID {
ctx.NotFound(nil) if err := project_model.ChangeProjectStatusByRepoIDAndID(ctx, 0, project.ID, toClose); err != nil {
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
return
}
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, project.ID))
}
// DeleteProject delete a project
func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
@@ -246,15 +248,11 @@ func RenderEditProject(ctx *context.Context) {
return return
} }
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) p, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
if p.OwnerID != ctx.ContextUser.ID {
ctx.NotFound(nil)
return
}
ctx.Data["projectID"] = p.ID ctx.Data["projectID"] = p.ID
ctx.Data["title"] = p.Title ctx.Data["title"] = p.Title
@@ -288,15 +286,11 @@ func EditProjectPost(ctx *context.Context) {
return return
} }
p, err := project_model.GetProjectByID(ctx, projectID) p, err := project_model.GetProjectByIDAndOwner(ctx, projectID, ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
if p.OwnerID != ctx.ContextUser.ID {
ctx.NotFound(nil)
return
}
p.Title = form.Title p.Title = form.Title
p.Description = form.Content p.Description = form.Content
@@ -316,15 +310,12 @@ func EditProjectPost(ctx *context.Context) {
// ViewProject renders the project with board view for a project // ViewProject renders the project with board view for a project
func ViewProject(ctx *context.Context) { func ViewProject(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
if project.OwnerID != ctx.ContextUser.ID {
ctx.NotFound(nil)
return
}
if err := project.LoadOwner(ctx); err != nil { if err := project.LoadOwner(ctx); err != nil {
ctx.ServerError("LoadOwner", err) ctx.ServerError("LoadOwner", err)
return return
@@ -455,28 +446,15 @@ func DeleteProjectColumn(ctx *context.Context) {
return return
} }
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
pb, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) _, err = project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
if err != nil { if err != nil {
ctx.ServerError("GetProjectColumn", err) ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
return
}
if pb.ProjectID != ctx.PathParamInt64("id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", pb.ID, project.ID),
})
return
}
if project.OwnerID != ctx.ContextUser.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectColumn[%d] is not in Owner[%d] as expected", pb.ID, ctx.ContextUser.ID),
})
return return
} }
@@ -492,7 +470,7 @@ func DeleteProjectColumn(ctx *context.Context) {
func AddColumnToProjectPost(ctx *context.Context) { func AddColumnToProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.EditProjectColumnForm) form := web.GetForm(ctx).(*forms.EditProjectColumnForm)
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
@@ -520,30 +498,18 @@ func CheckProjectColumnChangePermissions(ctx *context.Context) (*project_model.P
return nil, nil return nil, nil
} }
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return nil, nil return nil, nil
} }
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
if err != nil { if err != nil {
ctx.ServerError("GetProjectColumn", err) ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
return nil, nil
}
if column.ProjectID != ctx.PathParamInt64("id") {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectColumn[%d] is not in Project[%d] as expected", column.ID, project.ID),
})
return nil, nil return nil, nil
} }
if project.OwnerID != ctx.ContextUser.ID {
ctx.JSON(http.StatusUnprocessableEntity, map[string]string{
"message": fmt.Sprintf("ProjectColumn[%d] is not in Repository[%d] as expected", column.ID, project.ID),
})
return nil, nil
}
return project, column return project, column
} }
@@ -595,24 +561,15 @@ func MoveIssues(ctx *context.Context) {
return return
} }
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id")) project, err := project_model.GetProjectByIDAndOwner(ctx, ctx.PathParamInt64("id"), ctx.ContextUser.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err) ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
return return
} }
if project.OwnerID != ctx.ContextUser.ID {
ctx.NotFound(nil)
return
}
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID")) column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("columnID"), project.ID)
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetProjectColumn", project_model.IsErrProjectColumnNotExist, err) ctx.NotFoundOrServerError("GetColumnByIDAndProjectID", project_model.IsErrProjectColumnNotExist, err)
return
}
if column.ProjectID != project.ID {
ctx.NotFound(nil)
return return
} }
+30
View File
@@ -4,11 +4,14 @@
package org_test package org_test
import ( import (
"net/http"
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/org" "code.gitea.io/gitea/routers/web/org"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"code.gitea.io/gitea/services/forms"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -26,3 +29,30 @@ func TestCheckProjectColumnChangePermissions(t *testing.T) {
assert.NotNil(t, column) assert.NotNil(t, column)
assert.False(t, ctx.Written()) assert.False(t, ctx.Written())
} }
func TestChangeProjectStatusRejectsForeignProjects(t *testing.T) {
unittest.PrepareTestEnv(t)
// project 4 is owned by user2 not user1
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/close")
contexttest.LoadUser(t, ctx, 1)
ctx.ContextUser = ctx.Doer
ctx.SetPathParam("action", "close")
ctx.SetPathParam("id", "4")
org.ChangeProjectStatus(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
func TestAddColumnToProjectPostRejectsForeignProjects(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user1/-/projects/4/columns/new")
contexttest.LoadUser(t, ctx, 1)
ctx.ContextUser = ctx.Doer
ctx.SetPathParam("id", "4")
web.SetForm(ctx, &forms.EditProjectColumnForm{Title: "foreign"})
org.AddColumnToProjectPost(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
+64 -14
View File
@@ -4,11 +4,12 @@
package repo package repo
import ( import (
"fmt"
"net/http" "net/http"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@@ -40,7 +41,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
file, header, err := ctx.Req.FormFile("file") file, header, err := ctx.Req.FormFile("file")
if err != nil { if err != nil {
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) ctx.ServerError("FormFile", err)
return return
} }
defer file.Close() defer file.Close()
@@ -56,7 +57,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
ctx.HTTPError(http.StatusBadRequest, err.Error()) ctx.HTTPError(http.StatusBadRequest, err.Error())
return return
} }
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err)) ctx.ServerError("UploadAttachmentGeneralSizeLimit", err)
return return
} }
@@ -74,13 +75,44 @@ func DeleteAttachment(ctx *context.Context) {
ctx.HTTPError(http.StatusBadRequest, err.Error()) ctx.HTTPError(http.StatusBadRequest, err.Error())
return return
} }
if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) {
if !ctx.IsSigned {
ctx.HTTPError(http.StatusForbidden) ctx.HTTPError(http.StatusForbidden)
return return
} }
if attach.RepoID != ctx.Repo.Repository.ID {
ctx.HTTPError(http.StatusBadRequest, "attachment does not belong to this repository")
return
}
if ctx.Doer.ID != attach.UploaderID {
if attach.IssueID > 0 {
issue, err := issues_model.GetIssueByID(ctx, attach.IssueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.HTTPError(http.StatusForbidden)
return
}
} else if attach.ReleaseID > 0 {
if !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
ctx.HTTPError(http.StatusForbidden)
return
}
} else {
if !ctx.Repo.Permission.IsAdmin() && !ctx.Repo.Permission.IsOwner() {
ctx.HTTPError(http.StatusForbidden)
return
}
}
}
err = repo_model.DeleteAttachment(ctx, attach, true) err = repo_model.DeleteAttachment(ctx, attach, true)
if err != nil { if err != nil {
ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err)) ctx.ServerError("DeleteAttachment", err)
return return
} }
ctx.JSON(http.StatusOK, map[string]string{ ctx.JSON(http.StatusOK, map[string]string{
@@ -100,23 +132,41 @@ func ServeAttachment(ctx *context.Context, uuid string) {
return return
} }
repository, unitType, err := repo_service.LinkedRepository(ctx, attach) // prevent visiting attachment from other repository directly
if err != nil { // The check will be ignored before this code merged.
ctx.ServerError("LinkedRepository", err) if attach.CreatedUnix > repo_model.LegacyAttachmentMissingRepoIDCutoff && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID != attach.RepoID {
ctx.HTTPError(http.StatusNotFound)
return return
} }
if repository == nil { // If not linked unitType, repoID, err := repo_service.GetAttachmentLinkedTypeAndRepoID(ctx, attach)
if err != nil {
ctx.ServerError("GetAttachmentLinkedTypeAndRepoID", err)
return
}
if unitType == unit.TypeInvalid { // unlinked attachment can only be accessed by the uploader
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
ctx.HTTPError(http.StatusNotFound) ctx.HTTPError(http.StatusNotFound)
return return
} }
} else { // If we have the repository we check access } else { // If we have the linked type, we need to check access
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer) var perm access_model.Permission
if err != nil { if ctx.Repo.Repository == nil {
ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error()) repo, err := repo_model.GetRepositoryByID(ctx, repoID)
return if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}
perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
} else {
perm = ctx.Repo.Permission
} }
if !perm.CanRead(unitType) { if !perm.CanRead(unitType) {
ctx.HTTPError(http.StatusNotFound) ctx.HTTPError(http.StatusNotFound)
return return
+1 -1
View File
@@ -258,7 +258,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
} else if len(headInfos) == 2 { } else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/") headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 { if len(headInfosSplit) == 1 {
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0]) ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil) ctx.NotFound(nil)
+4 -2
View File
@@ -312,11 +312,13 @@ func EditFile(ctx *context.Context) {
ctx.ServerError("ReadAll", err) ctx.ServerError("ReadAll", err)
return return
} }
var fileContent string
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
ctx.Data["FileContent"] = string(buf) fileContent = string(buf)
} else { } else {
ctx.Data["FileContent"] = content fileContent = string(content)
} }
ctx.Data["FileContent"] = fileContent
} }
} }
+1 -3
View File
@@ -35,9 +35,7 @@ func CherryPick(ctx *context.Context) {
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
} else { } else {
ctx.Data["CherryPickType"] = "cherry-pick" ctx.Data["CherryPickType"] = "cherry-pick"
splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n")
ctx.Data["commit_summary"] = splits[0]
ctx.Data["commit_message"] = splits[1]
} }
ctx.HTML(http.StatusOK, tplCherryPick) ctx.HTML(http.StatusOK, tplCherryPick)
+18 -29
View File
@@ -31,6 +31,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
@@ -57,7 +58,7 @@ func CorsHandler() func(next http.Handler) http.Handler {
} }
// httpBase implementation git smart HTTP protocol // httpBase implementation git smart HTTP protocol
func httpBase(ctx *context.Context) *serviceHandler { func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
username := ctx.PathParam("username") username := ctx.PathParam("username")
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
@@ -67,18 +68,14 @@ func httpBase(ctx *context.Context) *serviceHandler {
} }
var isPull, receivePack bool var isPull, receivePack bool
service := ctx.FormString("service") switch util.OptionalArg(optGitService) {
if service == "git-receive-pack" || case "git-receive-pack":
strings.HasSuffix(ctx.Req.URL.Path, "git-receive-pack") {
isPull = false
receivePack = true receivePack = true
} else if service == "git-upload-pack" || case "git-upload-pack":
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") {
isPull = true isPull = true
} else if service == "git-upload-archive" || case "git-upload-archive":
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true isPull = true
} else { default:
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
} }
@@ -411,13 +408,19 @@ func prepareGitCmdWithAllowedService(service string) (*gitcmd.Command, error) {
return nil, fmt.Errorf("service %q is not allowed", service) return nil, fmt.Errorf("service %q is not allowed", service)
} }
func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { func serviceRPC(ctx *context.Context, service string) {
defer func() { defer func() {
if err := ctx.Req.Body.Close(); err != nil { if err := ctx.Req.Body.Close(); err != nil {
log.Error("serviceRPC: Close: %v", err) log.Error("serviceRPC: Close: %v", err)
} }
}() }()
h := httpBase(ctx, "git-"+service)
if h == nil {
ctx.Resp.WriteHeader(http.StatusBadRequest)
return
}
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
if ctx.Req.Header.Get("Content-Type") != expectedContentType { if ctx.Req.Header.Get("Content-Type") != expectedContentType {
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType) log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
@@ -472,26 +475,12 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
// ServiceUploadPack implements Git Smart HTTP protocol // ServiceUploadPack implements Git Smart HTTP protocol
func ServiceUploadPack(ctx *context.Context) { func ServiceUploadPack(ctx *context.Context) {
h := httpBase(ctx) serviceRPC(ctx, "upload-pack")
if h != nil {
serviceRPC(ctx, h, "upload-pack")
}
} }
// ServiceReceivePack implements Git Smart HTTP protocol // ServiceReceivePack implements Git Smart HTTP protocol
func ServiceReceivePack(ctx *context.Context) { func ServiceReceivePack(ctx *context.Context) {
h := httpBase(ctx) serviceRPC(ctx, "receive-pack")
if h != nil {
serviceRPC(ctx, h, "receive-pack")
}
}
func getServiceType(ctx *context.Context) string {
serviceType := ctx.Req.FormValue("service")
if !strings.HasPrefix(serviceType, "git-") {
return ""
}
return strings.TrimPrefix(serviceType, "git-")
} }
func updateServerInfo(ctx gocontext.Context, dir string) []byte { func updateServerInfo(ctx gocontext.Context, dir string) []byte {
@@ -512,12 +501,12 @@ func packetWrite(str string) []byte {
// GetInfoRefs implements Git dumb HTTP // GetInfoRefs implements Git dumb HTTP
func GetInfoRefs(ctx *context.Context) { func GetInfoRefs(ctx *context.Context) {
h := httpBase(ctx) service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
h := httpBase(ctx, "git-"+service)
if h == nil { if h == nil {
return return
} }
setHeaderNoCache(ctx) setHeaderNoCache(ctx)
service := getServiceType(ctx)
cmd, err := prepareGitCmdWithAllowedService(service) cmd, err := prepareGitCmdWithAllowedService(service)
if err == nil { if err == nil {
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) { if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
+37 -5
View File
@@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
issue_template "code.gitea.io/gitea/modules/issue/template" issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@@ -1100,11 +1101,9 @@ func MergePullRequest(ctx *context.Context) {
message += "\n\n" + form.MergeMessageField message += "\n\n" + form.MergeMessageField
} }
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr) // There is always a checkbox on the UI (the DeleteBranchAfterMerge is nil if the checkbox is not checked),
if err != nil { // just use the user's choice, don't use pull_service.ShouldDeleteBranchAfterMerge to decide
ctx.ServerError("ShouldDeleteBranchAfterMerge", err) deleteBranchAfterMerge := optional.FromPtr(form.DeleteBranchAfterMerge).Value()
return
}
if form.MergeWhenChecksSucceed { if form.MergeWhenChecksSucceed {
// delete all scheduled auto merges // delete all scheduled auto merges
@@ -1230,6 +1229,28 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
return return
} }
exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, issue.PullRequest.ID)
if err != nil {
ctx.ServerError("GetScheduledMergeByPullID", err)
return
}
if !exist {
ctx.NotFound(nil)
return
}
if ctx.Doer.ID != autoMerge.DoerID {
allowed, err := pull_service.IsUserAllowedToMerge(ctx, issue.PullRequest, ctx.Repo.Permission, ctx.Doer)
if err != nil {
ctx.ServerError("IsUserAllowedToMerge", err)
return
}
if !allowed {
ctx.HTTPError(http.StatusForbidden, "user has no permission to cancel the scheduled auto merge")
return
}
}
if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil { if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil {
if db.IsErrNotExist(err) { if db.IsErrNotExist(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled")) ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled"))
@@ -1305,6 +1326,17 @@ func CompareAndPullRequestPost(ctx *context.Context) {
return return
} }
// Check if a pull request already exists with the same head and base branch.
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
return
}
if pr != nil {
ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed"))
return
}
content := form.Content content := form.Content
if filename := ctx.Req.Form.Get("template-file"); filename != "" { if filename := ctx.Req.Form.Get("template-file"); filename != "" {
if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
+1 -1
View File
@@ -331,7 +331,7 @@ func UpdateViewedFiles(ctx *context.Context) {
updatedFiles[file] = state updatedFiles[file] = state
} }
if err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil { if _, err := pull_model.UpdateReviewState(ctx, ctx.Doer.ID, pull.ID, data.HeadCommitSHA, updatedFiles); err != nil {
ctx.ServerError("UpdateReview", err) ctx.ServerError("UpdateReview", err)
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
}) })
mockIconForFile := func(id string) template.HTML { mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
} }
assert.Equal(t, WebDiffFileTree{ assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{ TreeRoot: WebDiffFileItem{
+3
View File
@@ -15,6 +15,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@@ -128,7 +129,9 @@ func prepareUserNotificationsData(ctx *context.Context) {
ctx.Data["Notifications"] = notifications ctx.Data["Notifications"] = notifications
ctx.Data["Link"] = setting.AppSubURL + "/notifications" ctx.Data["Link"] = setting.AppSubURL + "/notifications"
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number") ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
pager.AddParamFromRequest(ctx.Req) pager.AddParamFromRequest(ctx.Req)
pager.RemoveParam(container.SetOf("div-only", "sequence-number"))
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
} }
@@ -0,0 +1,14 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"testing"
"code.gitea.io/gitea/models/unittest"
)
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
+13 -3
View File
@@ -4,12 +4,14 @@
package security package security
import ( import (
"errors"
"net/http" "net/http"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/openid" "code.gitea.io/gitea/modules/auth/openid"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@@ -116,7 +118,11 @@ func DeleteOpenID(ctx *context.Context) {
} }
if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil { if err := user_model.DeleteUserOpenID(ctx, &user_model.UserOpenID{ID: ctx.FormInt64("id"), UID: ctx.Doer.ID}); err != nil {
ctx.ServerError("DeleteUserOpenID", err) if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("DeleteUserOpenID", err)
}
return return
} }
log.Trace("OpenID address deleted: %s", ctx.Doer.Name) log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
@@ -132,8 +138,12 @@ func ToggleOpenIDVisibility(ctx *context.Context) {
return return
} }
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id")); err != nil { if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id"), ctx.Doer); err != nil {
ctx.ServerError("ToggleUserOpenIDVisibility", err) if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
}
return return
} }
@@ -0,0 +1,36 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"net/http"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
DeleteOpenID(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security")
contexttest.LoadUser(t, ctx, 2)
ctx.SetFormString("id", "1")
ToggleOpenIDVisibility(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
+1 -1
View File
@@ -29,7 +29,7 @@ func GetStopwatches(ctx *context.Context) {
return return
} }
apiSWs, err := convert.ToStopWatches(ctx, sws) apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
if err != nil { if err != nil {
ctx.HTTPError(http.StatusInternalServerError, err.Error()) ctx.HTTPError(http.StatusInternalServerError, err.Error())
return return
+3 -1
View File
@@ -227,6 +227,8 @@ func ctxDataSet(args ...any) func(ctx *context.Context) {
} }
} }
const RouterMockPointBeforeWebRoutes = "before-web-routes"
// Routes returns all web routes // Routes returns all web routes
func Routes() *web.Router { func Routes() *web.Router {
routes := web.NewRouter() routes := web.NewRouter()
@@ -285,7 +287,7 @@ func Routes() *web.Router {
webRoutes := web.NewRouter() webRoutes := web.NewRouter()
webRoutes.Use(mid...) webRoutes.Use(mid...)
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS()) webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
routes.Mount("", webRoutes) routes.Mount("", webRoutes)
return routes return routes
} }
+2
View File
@@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"path" "path"
"strings"
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@@ -91,6 +92,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
runName = wfs[0].Name runName = wfs[0].Name
} }
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
ctxname = strings.TrimSpace(ctxname) // git_model.NewCommitStatus also trims spaces
state := toCommitStatus(job.Status) state := toCommitStatus(job.Status)
if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil { if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
for _, v := range statuses { for _, v := range statuses {
+3 -1
View File
@@ -43,8 +43,10 @@ type Base struct {
Locale translation.Locale Locale translation.Locale
} }
var ParseMultipartFormMaxMemory = int64(32 << 20)
func (b *Base) ParseMultipartForm() bool { func (b *Base) ParseMultipartForm() bool {
err := b.Req.ParseMultipartForm(32 << 20) err := b.Req.ParseMultipartForm(ParseMultipartFormMaxMemory)
if err != nil { if err != nil {
// TODO: all errors caused by client side should be ignored (connection closed). // TODO: all errors caused by client side should be ignored (connection closed).
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) { if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
+14 -2
View File
@@ -20,15 +20,27 @@ import (
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
) )
// RedirectToUser redirect to a differently-named user // RedirectToUser redirect to a differently-named user
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) { func RedirectToUser(ctx *Base, doer *user_model.User, userName string, redirectUserID int64) {
user, err := user_model.GetUserByID(ctx, redirectUserID) user, err := user_model.GetUserByID(ctx, redirectUserID)
if err != nil { if err != nil {
ctx.HTTPError(http.StatusInternalServerError, "unable to get user") if user_model.IsErrUserNotExist(err) {
ctx.HTTPError(http.StatusNotFound, "user does not exist")
} else {
ctx.HTTPError(http.StatusInternalServerError, "unable to get user")
}
return
}
// Handle Visibility
if user.Visibility != structs.VisibleTypePublic && doer == nil {
// We must be signed in to see limited or private organizations
ctx.HTTPError(http.StatusNotFound, "user does not exist")
return return
} }
+1 -1
View File
@@ -118,7 +118,7 @@ func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
if uidChanged { if uidChanged {
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id) _ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
} else if cookieToken != "" { } else if cookieToken != "" {
// If cookie token presents, re-use existing unexpired token, else generate a new one. // If cookie token present, re-use existing unexpired token, else generate a new one.
if issueTime, ok := ParseCsrfToken(cookieToken); ok { if issueTime, ok := ParseCsrfToken(cookieToken); ok {
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
+3 -2
View File
@@ -49,7 +49,7 @@ func GetOrganizationByParams(ctx *Context) {
if organization.IsErrOrgNotExist(err) { if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName) redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
if err == nil { if err == nil {
RedirectToUser(ctx.Base, orgName, redirectUserID) RedirectToUser(ctx.Base, ctx.Doer, orgName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) { } else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
@@ -70,8 +70,9 @@ type OrgAssignmentOptions struct {
} }
// OrgAssignment returns a middleware to handle organization assignment // OrgAssignment returns a middleware to handle organization assignment
func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) { func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
opts := orgAssignmentOpts // it must be a copy, because the values will be changed
var err error var err error
if ctx.ContextUser == nil { if ctx.ContextUser == nil {
// if Organization is not defined, get it from params // if Organization is not defined, get it from params
+10
View File
@@ -8,8 +8,10 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
"slices"
"strings" "strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/paginator" "code.gitea.io/gitea/modules/paginator"
) )
@@ -49,6 +51,14 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
p.AddParamFromQuery(req.URL.Query()) p.AddParamFromQuery(req.URL.Query())
} }
func (p *Pagination) RemoveParam(keys container.Set[string]) {
p.urlParams = slices.DeleteFunc(p.urlParams, func(s string) bool {
k, _, _ := strings.Cut(s, "=")
k, _ = url.QueryUnescape(k)
return keys.Contains(k)
})
}
// GetParams returns the configured URL params // GetParams returns the configured URL params
func (p *Pagination) GetParams() template.URL { func (p *Pagination) GetParams() template.URL {
return template.URL(strings.Join(p.urlParams, "&")) return template.URL(strings.Join(p.urlParams, "&"))
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/container"
"github.com/stretchr/testify/assert"
)
func TestPagination(t *testing.T) {
p := NewPagination(1, 1, 1, 1)
params := url.Values{}
params.Add("k1", "11")
params.Add("k1", "12")
params.Add("k", "a")
params.Add("k", "b")
params.Add("k2", "21")
params.Add("k2", "22")
params.Add("foo", "bar")
p.AddParamFromQuery(params)
v, _ := url.ParseQuery(string(p.GetParams()))
assert.Equal(t, params, v)
p.RemoveParam(container.SetOf("k", "foo"))
params.Del("k")
params.Del("foo")
v, _ = url.ParseQuery(string(p.GetParams()))
assert.Equal(t, params, v)
}
+1 -1
View File
@@ -444,7 +444,7 @@ func RepoAssignment(ctx *Context) {
} }
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
RedirectToUser(ctx.Base, userName, redirectUserID) RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) { } else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound(nil) ctx.NotFound(nil)
} else { } else {
+1 -1
View File
@@ -69,7 +69,7 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con
if err != nil { if err != nil {
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil { if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
RedirectToUser(ctx, username, redirectUserID) RedirectToUser(ctx, doer, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) { } else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, err) errCb(http.StatusNotFound, err)
} else { } else {
+21 -2
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/label"
@@ -163,11 +164,12 @@ func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.T
} }
// ToStopWatches convert Stopwatch list to api.StopWatches // ToStopWatches convert Stopwatch list to api.StopWatches
func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.StopWatches, error) { func ToStopWatches(ctx context.Context, doer *user_model.User, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
result := api.StopWatches(make([]api.StopWatch, 0, len(sws))) result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
issueCache := make(map[int64]*issues_model.Issue) issueCache := make(map[int64]*issues_model.Issue)
repoCache := make(map[int64]*repo_model.Repository) repoCache := make(map[int64]*repo_model.Repository)
permCache := make(map[int64]access_model.Permission)
var ( var (
issue *issues_model.Issue issue *issues_model.Issue
repo *repo_model.Repository repo *repo_model.Repository
@@ -182,13 +184,30 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
if err != nil { if err != nil {
return nil, err return nil, err
} }
issueCache[sw.IssueID] = issue
} }
repo, ok = repoCache[issue.RepoID] repo, ok = repoCache[issue.RepoID]
if !ok { if !ok {
repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID) repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
if err != nil { if err != nil {
return nil, err log.Error("GetRepositoryByID(%d): %v", issue.RepoID, err)
continue
} }
repoCache[issue.RepoID] = repo
}
// ADD: Check user permissions
perm, ok := permCache[repo.ID]
if !ok {
perm, err = access_model.GetUserRepoPermission(ctx, repo, doer)
if err != nil {
continue
}
permCache[repo.ID] = perm
}
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
continue
} }
result = append(result, api.StopWatch{ result = append(result, api.StopWatch{
+28
View File
@@ -8,9 +8,11 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@@ -55,3 +57,29 @@ func TestMilestone_APIFormat(t *testing.T) {
Deadline: milestone.DeadlineUnix.AsTimePtr(), Deadline: milestone.DeadlineUnix.AsTimePtr(),
}, *ToAPIMilestone(milestone)) }, *ToAPIMilestone(milestone))
} }
func TestToStopWatchesRespectsPermissions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
publicSW := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{ID: 1})
privateIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 3})
privateSW := &issues_model.Stopwatch{IssueID: privateIssue.ID, UserID: 5}
assert.NoError(t, db.Insert(ctx, privateSW))
assert.NotZero(t, privateSW.ID)
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
sws := []*issues_model.Stopwatch{publicSW, privateSW}
visible, err := ToStopWatches(ctx, regularUser, sws)
assert.NoError(t, err)
assert.Len(t, visible, 1)
assert.Equal(t, "repo1", visible[0].RepoName)
visibleAdmin, err := ToStopWatches(ctx, adminUser, sws)
assert.NoError(t, err)
assert.Len(t, visibleAdmin, 2)
assert.ElementsMatch(t, []string{"repo1", "repo3"}, []string{visibleAdmin[0].RepoName, visibleAdmin[1].RepoName})
}
+12 -6
View File
@@ -8,8 +8,8 @@ import (
"net/url" "net/url"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
) )
@@ -25,11 +25,17 @@ func ToNotificationThread(ctx context.Context, n *activities_model.Notification)
// since user only get notifications when he has access to use minimal access mode // since user only get notifications when he has access to use minimal access mode
if n.Repository != nil { if n.Repository != nil {
result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead}) perm, err := access_model.GetUserRepoPermission(ctx, n.Repository, n.User)
if err != nil {
// This permission is not correct and we should not be reporting it log.Error("GetUserRepoPermission failed: %v", err)
for repository := result.Repository; repository != nil; repository = repository.Parent { return result
repository.Permissions = nil }
if perm.HasAnyUnitAccessOrPublicAccess() { // if user has been revoked access to repo, do not show repo info
result.Repository = ToRepo(ctx, n.Repository, perm)
// This permission is not correct and we should not be reporting it
for repository := result.Repository; repository != nil; repository = repository.Parent {
repository.Permissions = nil
}
} }
} }
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"testing"
activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
)
func TestToNotificationThreadIncludesRepoForAccessibleUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
n := newRepoNotification(t, 1, 4)
thread := ToNotificationThread(t.Context(), n)
if assert.NotNil(t, thread.Repository) {
assert.Equal(t, n.Repository.FullName(), thread.Repository.FullName)
assert.Nil(t, thread.Repository.Permissions)
}
}
func TestToNotificationThreadOmitsRepoWhenAccessRevoked(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
n := newRepoNotification(t, 2, 4)
thread := ToNotificationThread(t.Context(), n)
assert.Nil(t, thread.Repository)
}
func newRepoNotification(t *testing.T, repoID, userID int64) *activities_model.Notification {
t.Helper()
ctx := t.Context()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
assert.NoError(t, repo.LoadOwner(ctx))
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
return &activities_model.Notification{
ID: repoID*1000 + userID,
UserID: user.ID,
RepoID: repo.ID,
Status: activities_model.NotificationStatusUnread,
Source: activities_model.NotificationSourceRepository,
UpdatedUnix: timeutil.TimeStampNow(),
Repository: repo,
User: user,
}
}
+5 -14
View File
@@ -127,20 +127,10 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
projectsMode = config.ProjectsMode projectsMode = config.ProjectsMode
} }
hasReleases := false hasCode := repo.UnitEnabled(ctx, unit_model.TypeCode)
if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil { hasReleases := repo.UnitEnabled(ctx, unit_model.TypeReleases)
hasReleases = true hasPackages := repo.UnitEnabled(ctx, unit_model.TypePackages)
} hasActions := repo.UnitEnabled(ctx, unit_model.TypeActions)
hasPackages := false
if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil {
hasPackages = true
}
hasActions := false
if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil {
hasActions = true
}
if err := repo.LoadOwner(ctx); err != nil { if err := repo.LoadOwner(ctx); err != nil {
return nil return nil
@@ -221,6 +211,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
Updated: repo.UpdatedUnix.AsTime(), Updated: repo.UpdatedUnix.AsTime(),
ArchivedAt: repo.ArchivedUnix.AsTime(), ArchivedAt: repo.ArchivedUnix.AsTime(),
Permissions: permission, Permissions: permission,
HasCode: hasCode,
HasIssues: hasIssues, HasIssues: hasIssues,
ExternalTracker: externalTracker, ExternalTracker: externalTracker,
InternalTracker: internalTracker, InternalTracker: internalTracker,
+36 -8
View File
@@ -1233,10 +1233,10 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == "" shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
if shouldFullFileHighlight { if shouldFullFileHighlight {
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize { if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String()) diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
} }
if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize { if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.String()) diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
} }
} }
} }
@@ -1244,9 +1244,35 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
return diff, nil return diff, nil
} }
func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int]template.HTML { func splitHighlightLines(buf []byte) (ret [][]byte) {
lineCount := bytes.Count(buf, []byte("\n")) + 1
ret = make([][]byte, 0, lineCount)
nlTagClose := []byte("\n</")
for {
pos := bytes.IndexByte(buf, '\n')
if pos == -1 {
ret = append(ret, buf)
return ret
}
// Chroma highlighting output sometimes have "</span>" right after \n, sometimes before.
// * "<span>text\n</span>"
// * "<span>text</span>\n"
if bytes.HasPrefix(buf[pos:], nlTagClose) {
pos1 := bytes.IndexByte(buf[pos:], '>')
if pos1 != -1 {
pos += pos1
}
}
ret = append(ret, buf[:pos+1])
buf = buf[pos+1:]
}
}
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
contentUTF8, _ := charset.ToUTF8(rawContent, charset.ConvertOpts{})
content := util.UnsafeBytesToString(contentUTF8)
highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content) highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
splitLines := strings.Split(string(highlightedNewContent), "\n") splitLines := splitHighlightLines([]byte(highlightedNewContent))
lines := make(map[int]template.HTML, len(splitLines)) lines := make(map[int]template.HTML, len(splitLines))
// only save the highlighted lines we need, but not the whole file, to save memory // only save the highlighted lines we need, but not the whole file, to save memory
for _, sec := range diffFile.Sections { for _, sec := range diffFile.Sections {
@@ -1344,15 +1370,17 @@ outer:
} }
} }
// Explicitly store files that have changed in the database, if any is present at all.
// This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
// On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
if len(filesChangedSinceLastDiff) > 0 { if len(filesChangedSinceLastDiff) > 0 {
err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff) // Explicitly store files that have changed in the database, if any is present at all.
// This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
// On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
updatedReview, err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
if err != nil { if err != nil {
log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err) log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
return nil, err return nil, err
} }
// Update the local review to reflect the changes immediately
review = updatedReview
} }
return review, nil return review, nil
+39
View File
@@ -5,6 +5,7 @@
package gitdiff package gitdiff
import ( import (
"html/template"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@@ -640,3 +641,41 @@ func TestNoCrashes(t *testing.T) {
ParsePatch(t.Context(), setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "") ParsePatch(t.Context(), setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "")
} }
} }
func TestHighlightCodeLines(t *testing.T) {
t.Run("CharsetDetecting", func(t *testing.T) {
diffFile := &DiffFile{
Name: "a.c",
Language: "c",
Sections: []*DiffSection{
{
Lines: []*DiffLine{{LeftIdx: 1}},
},
},
}
ret := highlightCodeLines(diffFile, true, []byte("// abc\xcc def\xcd")) // ISO-8859-1 bytes
assert.Equal(t, "<span class=\"c1\">// abcÌ defÍ\n</span>", string(ret[0]))
})
t.Run("LeftLines", func(t *testing.T) {
diffFile := &DiffFile{
Name: "a.c",
Language: "c",
Sections: []*DiffSection{
{
Lines: []*DiffLine{
{LeftIdx: 1},
{LeftIdx: 2},
{LeftIdx: 3},
},
},
},
}
const nl = "\n"
ret := highlightCodeLines(diffFile, true, []byte("a\nb\n"))
assert.Equal(t, map[int]template.HTML{
0: `<span class="n">a</span>` + nl,
1: `<span class="n">b</span>`,
}, ret)
})
}
+1 -1
View File
@@ -90,7 +90,7 @@ func GetListLockHandler(ctx *context.Context) {
}) })
return return
} }
lock, err := git_model.GetLFSLockByID(ctx, v) lock, err := git_model.GetLFSLockByIDAndRepo(ctx, v, repository.ID)
if err != nil && !git_model.IsErrLFSLockNotExist(err) { if err != nil && !git_model.IsErrLFSLockNotExist(err) {
log.Error("Unable to get lock with ID[%s]: Error: %v", v, err) log.Error("Unable to get lock with ID[%s]: Error: %v", v, err)
} }

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