Compare commits

...

51 Commits

Author SHA1 Message Date
Lunny Xiao
136ec9ef81 Add changelog for 1.24.5 (#35261) 2025-08-13 08:58:05 -07:00
Giteabot
79018ae726 modules/setting/actions.go: fixed typo: ì->i (#35253) (#35254)
Backport #35253 by @TimB87

Hello,

I spotted this minor typo.

Co-authored-by: Tim Biermann <tbier@posteo.de>
2025-08-12 11:21:56 +00:00
Giteabot
e11176192a Fix a bug where lfs gc never worked. (#35198) (#35255)
Backport #35198 by @lunny

Fix #31113

After #22385 introduced LFS GC, it never worked due to a bug in the INI
library: fields in structs embedded more than one level deep are not
populated from the INI file.

This PR fixes the issue by replacing the multi-level embedded struct
with a single-level struct for parsing the cron.gc_lfs configuration.

Added a new test for retrieving cron settings to demonstrate the bug in
the INI package.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-12 10:57:50 +03:00
Giteabot
4e0269e890 Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
Backport #35243 by @lunny

Fix #35229

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 10:12:47 -07:00
Lunny Xiao
04114c637a Fix bug when review pull request commits (#35192) (#35246)
The commit range in the UI follows a half-open, half-closed convention:
(,]. When reviewing a range of commits, the beforeCommitID should be set
to the commit immediately preceding the first selected commit. For
single-commit reviews, we must identify and use the previous commit of
that specific commit.

The endpoint ViewPullFilesStartingFromCommit is currently unused and can
be safely removed.

Fix #35157
Replace #35184
Partially extract from #35077
Backport #35192
2025-08-11 13:18:03 +02:00
6543
e5540bfa81 Nix flake build static with sqlite support (#35149) (#35225)
Backport #35149

with `nix develop -c $SHELL` you can enter the dev environment. now with
`make clean-all generate build -j1` you will get a static linked binary
that has sqlite support

outside of an nix dev shell if you set `STATIC=true` you also will get a
static binary
2025-08-07 23:37:46 +03:00
Giteabot
d22d6ca0d8 Vertically center "Show Resolved" (#35211) (#35218)
Backport #35211 by @silverwind

Before, "Show Resolved" slightly off-center:

<img width="174" height="60" alt="Screenshot 2025-08-04 at 15 07 13"
src="https://github.com/user-attachments/assets/a165f721-4749-4ea3-bde0-141ad43a6142"
/>

After: centered:

<img width="176" height="63" alt="Screenshot 2025-08-04 at 15 07 22"
src="https://github.com/user-attachments/assets/f87e16bc-d067-4040-b940-9adb4cf182c1"
/>

Co-authored-by: silverwind <me@silverwind.io>
2025-08-06 10:55:48 +02:00
Lunny Xiao
d49feab428 release notes for 1.24.4 (#35208) 2025-08-04 11:13:27 -07:00
wxiaoguang
9162f4403a Fix various bugs (1.24) (#35186)
Backport #35177, #35183
2025-07-31 12:04:47 +08:00
Giteabot
d05cf08fad Fix migrate input box bug (#35166) (#35171)
Backport #35166 by @lunny

Fix #35162

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-07-27 19:57:47 -07:00
Giteabot
f4b4b0bf98 Only hide dropzone when no files have been uploaded (#35156) (#35167)
Backport #35156 by @bartvdbraak

Instead of always hiding the dropzone when it's not active:
- hide it when when there are no uploaded files and it becomes inactive 
- don't hide it when there are uploaded files

Fixes #35125

Co-authored-by: Bart van der Braak <bartvdbraak@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-27 11:20:48 +03:00
wxiaoguang
99596044d7 Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
Fix #35106
2025-07-18 13:52:41 +00:00
Giteabot
693d26914f Fix submodule parsing when the gitmodules is missing (#35109) (#35118)
Backport #35109

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-18 20:22:18 +08:00
Giteabot
315f197790 Increase gap on latest commit (#35104) (#35113)
Backport #35104 by @silverwind

Before:

<img width="964" height="101" alt="Screenshot 2025-07-17 at 02 31 05"
src="https://github.com/user-attachments/assets/e02181f3-f730-40bb-8cfa-ecfea4ed4aec"
/>

After:

<img width="967" height="104" alt="Screenshot 2025-07-17 at 02 42 13"
src="https://github.com/user-attachments/assets/7ca7b9a8-1f59-4dc0-9bb0-c72346fd792a"
/>

Co-authored-by: silverwind <me@silverwind.io>
2025-07-18 08:25:51 +03:00
Giteabot
76b8f0c3a7 Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099) 2025-07-17 00:04:05 +08:00
Giteabot
f99bbd7f3f Fix submodule nil check (#35096) (#35098)
Backport #35096 by wxiaoguang

Fix  #35095

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-16 13:02:18 +00:00
Giteabot
f7ef657b5a nix flake update (#35085) (#35090)
Backport #35085 by @techknowlogick

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
2025-07-15 14:37:44 -07:00
Lunny Xiao
486d274be6 Add missing changelog (#35079)
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-15 12:08:13 +08:00
Giteabot
ab3d2a944c Fix form property assignment edge case (#35073) (#35078)
Backport #35073 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-15 09:40:50 +08:00
wxiaoguang
12bfa9e83d Improve submodule relative path handling (#35056) (#35075)
Backport #35056
2025-07-14 17:26:16 +00:00
Giteabot
dd661e92df Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
Backport #35046 by lunny

* Fix missing the first char when parsing diff hunk header
* Fix #35040
* Fix #35049

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-12 15:12:02 +08:00
Giteabot
0b31272c7e Fix updating user visibility (#35036) (#35044)
Backport #35036 by @lunny

Fix #35030

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-11 02:44:06 +00:00
Giteabot
ec0c418719 Support base64-encoded agit push options (#35037) (#35041)
Backport #35037 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-10 18:56:43 +00:00
Giteabot
6dc19fc29a Make submodule link work with relative path (#35034) (#35038)
Backport #35034 by wxiaoguang

Fix #35033

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-10 18:20:14 +00:00
Giteabot
9f1baa7d18 Fix bug when displaying git user avatar in commits list (#35006)
A quick fix for #34991

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-10 08:46:44 -07:00
wxiaoguang
e13deb7a16 Fix API response for swagger spec (#35029)
Co-authored-by: Scion <scion@studiowhy.net>
2025-07-10 15:27:34 +08:00
Lunny Xiao
e5c1b8b632 Add changelog for 1.24.3 (#34975) 2025-07-10 03:21:01 +00:00
Lunny Xiao
e931b62f33 Start automerge check again after the conflict check and the schedule (#35002)
Fix #34988
Backport #34989 

Co-authored-by: posativ

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-10 02:37:15 +00:00
wxiaoguang
81ee93e5bc Fix repo settings and protocol log problems (#35012) (#35013)
Backport #35012
2025-07-09 17:20:15 +00:00
ChristopherHX
053f9186bc Fix the response format for actions/workflows. (#35009) (#35016)
Backport #35009

This PR fixes the response format for the OpenAPI Spec of
`ActionsListRepositoryWorkflows`.
It was specified in the OpenAPI spec as returning a `[]*ActionWorkflow`,
but it actually should return a `api.ActionWorkflowResponse`.

The test already expects an `api.ActionWorkflowResponse` like expected.

Co-authored-by: Scion <Filiecs2@gmail.com>
2025-07-09 18:18:40 +02:00
wxiaoguang
68fcdb6122 Fix project images scroll (#34971) (#34972) 2025-07-07 00:30:43 +08:00
Giteabot
14ca309c39 Mark old reviews as stale on agit pr updates (#34933) (#34965)
Backport #34933 by @dcermak

Fixes: https://github.com/go-gitea/gitea/issues/34134

Co-authored-by: Dan Čermák <dan.cermak@posteo.net>
2025-07-05 20:33:26 -07:00
wxiaoguang
4aba42519d Fix git graph page (#34948) (#34949) 2025-07-04 15:33:17 +00:00
Giteabot
9adf175df0 Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
Backport #34928 by lunny

Fix #18846 
Fix #34924

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-07-03 03:01:17 +00:00
wxiaoguang
c3fa2a8729 Fix issue filter (#34914) (#34915)
Backport #34914
2025-07-03 09:45:17 +08:00
Giteabot
89dfed32e0 support the open-icon of folder (#34168) (#34896)
Backport #34168 by @kerwin612

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-30 13:55:24 +02:00
Giteabot
d5062d0c27 docs: fix typo in pull request merge warning message text (#34899) (#34903)
Backport #34899 by @Pavanipogula

### Description

This PR fixes two typos in the pull request merge command warning
message.

- "can not" → "cannot"
- "was not enable" → "is not enabled."

### File Updated
- `options/locale/locale_en-US.ini` (line 1972)

### Related Discussion
https://github.com/go-gitea/gitea/issues/34893

Co-authored-by: Pavanipogula <51442511+Pavanipogula@users.noreply.github.com>
2025-06-29 18:26:15 -07:00
Giteabot
90e9e79232 Optimize flex layout of release attachment area (#34885) (#34886)
Backport #34885 by kerwin612

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-06-28 09:10:41 +08:00
Giteabot
c6467edcb1 Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
Backport #34791 by @kerwin612

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-06-26 23:18:02 -07:00
wxiaoguang
5d5b695527 Skip updating timestamp when sync branch (#34875) 2025-06-26 17:59:06 -07:00
Giteabot
0af7a7b79f Fix some log and UI problems (#34863) (#34868)
Backport #34863 by @wxiaoguang

Remove the misleading error log, fix #34738

Make the "search" input auto-focused, fix #34807

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-25 22:08:30 +03:00
Giteabot
9339661078 Fix archive API (#34853) (#34857)
Backport #34853 by wxiaoguang

Fix #34852

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-25 07:20:42 +00:00
Giteabot
1e69f085d6 Ignore force pushes for changed files in a PR review (#34837) (#34843)
Backport #34837 by delvh

Fixes #34832

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-24 16:41:07 +00:00
Giteabot
0bfccd8ecf Fix SSH LFS timeout (#34838) (#34842)
Backport #34838 by wxiaoguang

Fix #34834

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-24 16:15:03 +00:00
Giteabot
534b9b35dd Fix job status aggregation logic (#34823) (#34835)
Backport #34823 by nienjiuntai

Co-authored-by: JIUN-TAI NIEN <44364165+nienjiuntai@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-24 23:23:16 +08:00
Giteabot
dbadc59b56 Fix team permissions (#34827) (#34836)
Backport #34827 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-24 14:17:14 +00:00
Zettat123
a57e2c4bc3 Fix required contexts and commit status matching bug (#34815) (#34829)
Backport #34815

Fix #34504

Since one required context can match more than one commit statuses, we
should not directly compare the lengths of `requiredCommitStatuses` and
`requiredContexts`
2025-06-24 07:15:25 +08:00
Lunny Xiao
acd4e10990 release of 1.24.2 (#34800) 2025-06-20 12:41:59 -07:00
Lunny Xiao
0a1df294c8 upgrade chi (#34799)
Backport #34798
2025-06-20 18:30:49 +00:00
wxiaoguang
52a964d1fc Fix container range bug (#34795) (#34796)
Backport #34795
2025-06-20 17:35:36 +00:00
Giteabot
d3dbe0d9ce Bump poetry feature to new url for dev container (#34787) (#34790)
Backport #34787 by @yp05327

Why:
I got this error when build dev container

![4be14223f83e4d19ff04b0b31e38b8fc](https://github.com/user-attachments/assets/6e14630f-e505-4a8c-819f-229efee67116)

It seems that the url of poetry feature was changed:
https://containers.dev/features

![image](https://github.com/user-attachments/assets/8d3d4beb-9344-494a-8176-80b6f52ad6af)

After change to the new one, it worked.

Co-authored-by: yp05327 <576951401@qq.com>
2025-06-20 10:08:38 -07:00
134 changed files with 1624 additions and 698 deletions

View File

@@ -7,7 +7,7 @@
"version": "20" "version": "20"
}, },
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {}, "ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
"ghcr.io/devcontainers-contrib/features/poetry:2": {}, "ghcr.io/devcontainers-extra/features/poetry:2": {},
"ghcr.io/devcontainers/features/python:1": { "ghcr.io/devcontainers/features/python:1": {
"version": "3.12" "version": "3.12"
}, },

View File

@@ -4,7 +4,68 @@ 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.24.1](https://github.com/go-gitea/gitea/releases/tag/1.24.1) - 2025-06-18 ## [1.24.5](https://github.com/go-gitea/gitea/releases/tag/v1.24.5) - 2025-08-12
* BUGFIXES
* Fix a bug where lfs gc never worked. (#35198) (#35255)
* Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
* Fix bug when review pull request commits (#35192) (#35246)
* MISC
* Vertically center "Show Resolved" (#35211) (#35218)
## [1.24.4](https://github.com/go-gitea/gitea/releases/tag/v1.24.4) - 2025-08-03
* BUGFIXES
* Fix various bugs (1.24) (#35186)
* Fix migrate input box bug (#35166) (#35171)
* Only hide dropzone when no files have been uploaded (#35156) (#35167)
* Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099)
* Fix submodule nil check (#35096) (#35098)
* MISC
* Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
* Increase gap on latest commit (#35104) (#35113)
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/v1.24.3) - 2025-07-15
* BUGFIXES
* Fix form property assignment edge case (#35073) (#35078)
* Improve submodule relative path handling (#35056) (#35075)
* Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
* Fix updating user visibility (#35036) (#35044)
* Support base64-encoded agit push options (#35037) (#35041)
* Make submodule link work with relative path (#35034) (#35038)
* Fix bug when displaying git user avatar in commits list (#35006)
* Fix API response for swagger spec (#35029)
* Start automerge check again after the conflict check and the schedule (#34988) (#35002)
* Fix the response format for actions/workflows (#35009) (#35016)
* Fix repo settings and protocol log problems (#35012) (#35013)
* Fix project images scroll (#34971) (#34972)
* Mark old reviews as stale on agit pr updates (#34933) (#34965)
* Fix git graph page (#34948) (#34949)
* Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
* Fix some log and UI problems (#34863) (#34868)
* Fix archive API (#34853) (#34857)
* Ignore force pushes for changed files in a PR review (#34837) (#34843)
* Fix SSH LFS timeout (#34838) (#34842)
* Fix team permissions (#34827) (#34836)
* Fix job status aggregation logic (#34823) (#34835)
* Fix issue filter (#34914) (#34915)
* Fix typo in pull request merge warning message text (#34899) (#34903)
* Support the open-icon of folder (#34168) (#34896)
* Optimize flex layout of release attachment area (#34885) (#34886)
* Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
* Skip updating timestamp when sync branch (#34875)
* Fix required contexts and commit status matching bug (#34815) (#34829)
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/v1.24.2) - 2025-06-20
* BUGFIXES
* Fix container range bug (#34795) (#34796)
* Upgrade chi to v5.2.2 (#34798) (#34799)
* BUILD
* Bump poetry feature to new url for dev container (#34787) (#34790)
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/v1.24.1) - 2025-06-18
* ENHANCEMENTS * ENHANCEMENTS
* Improve alignment of commit status icon on commit page (#34750) (#34757) * Improve alignment of commit status icon on commit page (#34750) (#34757)
@@ -24,7 +85,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Hide href attribute of a tag if there is no target_url (#34556) (#34684) * Hide href attribute of a tag if there is no target_url (#34556) (#34684)
* Fix tag target (#34781) #34783 * Fix tag target (#34781) #34783
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26 ## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
* BREAKING * BREAKING
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076) * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
@@ -394,7 +455,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Bump x/net (#32896) (#32900) * Bump x/net (#32896) (#32900)
* Only activity tab needs heatmap data loading (#34652) * Only activity tab needs heatmap data loading (#34652)
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11 ## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
* SECURITY * SECURITY
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411) * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
@@ -421,7 +482,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* Bump go version in go.mod (#34160) * Bump go version in go.mod (#34160)
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158) * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07 ## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
* Enhancements * Enhancements
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071) * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
@@ -519,7 +580,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
* BUGFIXES * BUGFIXES
* Fix a bug caused by status webhook template #33512 * Fix a bug caused by status webhook template #33512
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04 ## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
* BREAKING * BREAKING
* Add tests for webhook and fix some webhook bugs (#33396) (#33442) * Add tests for webhook and fix some webhook bugs (#33396) (#33442)
@@ -3049,7 +3110,7 @@ Key highlights of this release encompass significant changes categorized under `
* Improve decryption failure message (#24573) (#24575) * Improve decryption failure message (#24573) (#24575)
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572) * Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03 ## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
* SECURITY * SECURITY
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400 * Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
@@ -3062,7 +3123,7 @@ Key highlights of this release encompass significant changes categorized under `
* Fix incorrect CurrentUser check for docker rootless (#24435) * Fix incorrect CurrentUser check for docker rootless (#24435)
* Getting the tag list does not require being signed in (#24413) (#24416) * Getting the tag list does not require being signed in (#24413) (#24416)
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26 ## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
* SECURITY * SECURITY
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364) * Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
@@ -3561,7 +3622,7 @@ Key highlights of this release encompass significant changes categorized under `
* Display attachments of review comment when comment content is blank (#23035) (#23046) * Display attachments of review comment when comment content is blank (#23035) (#23046)
* Return empty url for submodule tree entries (#23043) (#23048) * Return empty url for submodule tree entries (#23043) (#23048)
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20 ## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
* SECURITY * SECURITY
* Provide the ability to set password hash algorithm parameters (#22942) (#22943) * Provide the ability to set password hash algorithm parameters (#22942) (#22943)
@@ -3988,7 +4049,7 @@ Key highlights of this release encompass significant changes categorized under `
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867) * Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
* Fix UI mis-align for PR commit history (#20845) (#20859) * Fix UI mis-align for PR commit history (#20845) (#20859)
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17 ## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
* SECURITY * SECURITY
* Correctly escape within tribute.js (#20831) (#20832) * Correctly escape within tribute.js (#20831) (#20832)

View File

@@ -47,6 +47,17 @@ ifeq ($(HAS_GO), yes)
CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS) CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
endif endif
CGO_ENABLED ?= 0
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
CGO_ENABLED = 1
endif
STATIC ?=
EXTLDFLAGS ?=
ifneq ($(STATIC),)
EXTLDFLAGS = -extldflags "-static"
endif
ifeq ($(GOOS),windows) ifeq ($(GOOS),windows)
IS_WINDOWS := yes IS_WINDOWS := yes
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows) else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
@@ -740,7 +751,10 @@ security-check:
go run $(GOVULNCHECK_PACKAGE) -show color ./... go run $(GOVULNCHECK_PACKAGE) -show color ./...
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
$(error pam support set via TAGS doesn't support static builds)
endif
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
.PHONY: release .PHONY: release
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1747179050, "lastModified": 1752480373,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -11,8 +11,16 @@
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in in
{ {
devShells.default = pkgs.mkShell { devShells.default =
buildInputs = with pkgs; [ with pkgs;
let
# only bump toolchain versions here
go = go_1_24;
nodejs = nodejs_24;
python3 = python312;
in
pkgs.mkShell {
buildInputs = [
# generic # generic
git git
git-lfs git-lfs
@@ -22,21 +30,25 @@
gzip gzip
# frontend # frontend
nodejs_22 nodejs
# linting # linting
python312 python3
poetry poetry
# backend # backend
go_1_24 go
glibc.static
gofumpt gofumpt
sqlite sqlite
]; ];
shellHook = '' CFLAGS = "-I${glibc.static.dev}/include";
export GO="${pkgs.go_1_24}/bin/go" LDFLAGS = "-L ${glibc.static}/lib";
export GOROOT="${pkgs.go_1_24}/share/go" GO = "${go}/bin/go";
''; GOROOT = "${go}/share/go";
TAGS = "sqlite sqlite_unlock_notify";
STATIC = "true";
}; };
} }
); );

2
go.mod
View File

@@ -51,7 +51,7 @@ require (
github.com/gliderlabs/ssh v0.3.8 github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-enry/go-enry/v2 v2.9.2

4
go.sum
View File

@@ -301,8 +301,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=

View File

@@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
return StatusSuccess return StatusSuccess
case hasCancelled: case hasCancelled:
return StatusCancelled return StatusCancelled
case hasFailure:
return StatusFailure
case hasRunning: case hasRunning:
return StatusRunning return StatusRunning
case hasFailure:
return StatusFailure
case hasWaiting: case hasWaiting:
return StatusWaiting return StatusWaiting
case hasBlocked: case hasBlocked:

View File

@@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled}, {[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled}, {[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
// failure with other status, fail fast // failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast. // another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
{[]Status{StatusFailure}, StatusFailure}, {[]Status{StatusFailure}, StatusFailure},
{[]Status{StatusFailure, StatusSuccess}, StatusFailure}, {[]Status{StatusFailure, StatusSuccess}, StatusFailure},
{[]Status{StatusFailure, StatusSkipped}, StatusFailure}, {[]Status{StatusFailure, StatusSkipped}, StatusFailure},
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, {[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
{[]Status{StatusFailure, StatusWaiting}, StatusFailure}, {[]Status{StatusFailure, StatusWaiting}, StatusFailure},
{[]Status{StatusFailure, StatusRunning}, StatusFailure}, {[]Status{StatusFailure, StatusRunning}, StatusRunning},
{[]Status{StatusFailure, StatusBlocked}, StatusFailure}, {[]Status{StatusFailure, StatusBlocked}, StatusFailure},
// skipped with other status // skipped with other status

View File

@@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil) signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
} }
if err != nil { if err != nil {
log.Error("Unable to validate token signature. Error: %v", err) log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
return nil, ErrGPGInvalidTokenSignature{ return nil, ErrGPGInvalidTokenSignature{
ID: ekeys[0].PrimaryKey.KeyIdString(), ID: ekeys[0].PrimaryKey.KeyIdString(),
Wrapped: err, Wrapped: err,

View File

@@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
} }
if signer == nil { if signer == nil {
log.Error("Unable to validate token signature. Error: %v", err) log.Debug("VerifyGPGKey failed: no signer")
return "", ErrGPGInvalidTokenSignature{ return "", ErrGPGInvalidTokenSignature{
ID: key.KeyID, ID: key.KeyID,
} }

View File

@@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command // edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
// see https://github.com/PowerShell/PowerShell/issues/5974 // see https://github.com/PowerShell/PowerShell/issues/5974
if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil { if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
log.Error("Unable to validate token signature. Error: %v", err) log.Debug("VerifySSHKey sshsig.Verify failed: %v", err)
return "", ErrSSHInvalidTokenSignature{ return "", ErrSSHInvalidTokenSignature{
Fingerprint: key.Fingerprint, Fingerprint: key.Fingerprint,
} }

View File

@@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
return currentWhitelist, nil return currentWhitelist, nil
} }
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil { if err != nil {
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
} }

View File

@@ -719,7 +719,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
return nil return nil
} }
func (c *Comment) loadReview(ctx context.Context) (err error) { // LoadReview loads the associated review
func (c *Comment) LoadReview(ctx context.Context) (err error) {
if c.ReviewID == 0 { if c.ReviewID == 0 {
return nil return nil
} }
@@ -736,11 +737,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
return nil return nil
} }
// LoadReview loads the associated review
func (c *Comment) LoadReview(ctx context.Context) error {
return c.loadReview(ctx)
}
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes. // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
func (c *Comment) DiffSide() string { func (c *Comment) DiffSide() string {
if c.Line < 0 { if c.Line < 0 {
@@ -860,7 +856,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
} }
if comment.ReviewID != 0 { if comment.ReviewID != 0 {
if comment.Review == nil { if comment.Review == nil {
if err := comment.loadReview(ctx); err != nil { if err := comment.LoadReview(ctx); err != nil {
return err return err
} }
} }

View File

@@ -235,7 +235,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
// AddCrossReferences add cross references // AddCrossReferences add cross references
func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error { func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
if c.Type != CommentTypeCode && c.Type != CommentTypeComment { if !c.Type.HasContentSupport() {
return nil return nil
} }
if err := c.LoadIssue(stdCtx); err != nil { if err := c.LoadIssue(stdCtx); err != nil {

View File

@@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
"team_user.uid": userID, "team_user.uid": userID,
}) })
} }
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
}

View File

@@ -9,6 +9,8 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"xorm.io/builder"
) )
// TeamRepo represents an team-repository relation. // TeamRepo represents an team-repository relation.
@@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
return err return err
} }
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository. // GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) { // This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
teams := make([]*Team, 0, 5) teams := make([]*Team, 0, 5)
return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode).
Join("INNER", "team_repo", "team_repo.team_id = team.id").
And("team_repo.org_id = ?", orgID).
And("team_repo.repo_id = ?", repoID).
OrderBy("name").
Find(&teams)
}
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit. sub := builder.Select("team_id").From("team_unit").
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { Where(builder.Expr("team_unit.team_id = team.id")).
teams := make([]*Team, 0, 5) And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode). And(builder.Expr("team_unit.access_mode >= ?", mode))
err := db.GetEngine(ctx).
Join("INNER", "team_repo", "team_repo.team_id = team.id"). Join("INNER", "team_repo", "team_repo.team_id = team.id").
Join("INNER", "team_unit", "team_unit.team_id = team.id").
And("team_repo.org_id = ?", orgID). And("team_repo.org_id = ?", orgID).
And("team_repo.repo_id = ?", repoID). And("team_repo.repo_id = ?", repoID).
And("team_unit.type = ?", unitType). And(builder.Or(
builder.Expr("team.authorize >= ?", mode),
builder.In("team.id", sub),
)).
OrderBy("name"). OrderBy("name").
Find(&teams) Find(&teams)
return teams, err
} }

View File

@@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41}) org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61}) repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, teams, 2) { if assert.Len(t, teams, 2) {
assert.EqualValues(t, 21, teams[0].ID) assert.EqualValues(t, 21, teams[0].ID)

View File

@@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool {
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
// It doesn't count the "public(anonymous/everyone) access mode". // It doesn't count the "public(anonymous/everyone) access mode".
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
func (p *Permission) HasAnyUnitAccess() bool { func (p *Permission) HasAnyUnitAccess() bool {
for _, v := range p.unitsMode { for _, v := range p.unitsMode {
if v >= perm_model.AccessModeRead { if v >= perm_model.AccessModeRead {
@@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
perm.units = repo.Units perm.units = repo.Units
// anonymous user visit private repo. // anonymous user visit private repo.
// TODO: anonymous user visit public unit of private repo???
if user == nil && repo.IsPrivate { if user == nil && repo.IsPrivate {
perm.AccessMode = perm_model.AccessModeNone perm.AccessMode = perm_model.AccessModeNone
return perm, nil return perm, nil
@@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
} }
// Prevent strangers from checking out public repo of private organization/users // Prevent strangers from checking out public repo of private organization/users
// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself // Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
// TODO: rename it to "IsOwnerVisibleToDoer"
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator { if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
perm.AccessMode = perm_model.AccessModeNone perm.AccessMode = perm_model.AccessModeNone
return perm, nil return perm, nil
@@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, nil return perm, nil
} }
// plain user // plain user TODO: this check should be replaced, only need to check collaborator access mode
perm.AccessMode, err = accessLevel(ctx, user, repo) perm.AccessMode, err = accessLevel(ctx, user, repo)
if err != nil { if err != nil {
return perm, err return perm, err
@@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return perm, nil return perm, nil
} }
// now: the owner is visible to doer, if the repo is public, then the min access mode is read
minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
perm.AccessMode = max(perm.AccessMode, minAccessMode)
// get units mode from teams
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
if err != nil {
return perm, err
}
if len(teams) == 0 {
return perm, nil
}
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
// Collaborators on organization // Collaborators on organization
@@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
} }
} }
// get units mode from teams
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
if err != nil {
return perm, err
}
// if user in an owner team // if user in an owner team
for _, team := range teams { for _, team := range teams {
if team.HasAdminAccess() { if team.HasAdminAccess() {
@@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
} }
for _, u := range repo.Units { for _, u := range repo.Units {
var found bool
for _, team := range teams { for _, team := range teams {
unitAccessMode := minAccessMode
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
found = true
}
}
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
if !found && !repo.IsPrivate && !user.IsRestricted {
if _, ok := perm.unitsMode[u.Type]; !ok {
perm.unitsMode[u.Type] = perm_model.AccessModeRead
} }
perm.unitsMode[u.Type] = unitAccessMode
} }
} }

View File

@@ -6,12 +6,16 @@ package access
import ( import (
"testing" "testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
perm_model "code.gitea.io/gitea/models/perm" perm_model "code.gitea.io/gitea/models/perm"
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/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestHasAnyUnitAccess(t *testing.T) { func TestHasAnyUnitAccess(t *testing.T) {
@@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) {
} }
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
} }
func TestGetUserRepoPermission(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
require.NoError(t, repo32.LoadOwner(ctx))
require.True(t, repo32.Owner.IsOrganization())
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
org := repo32.Owner
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
require.NoError(t, db.Insert(ctx, team))
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
perm, err := GetUserRepoPermission(ctx, repo32, user)
require.NoError(t, err)
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
})
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
perm, err := GetUserRepoPermission(ctx, repo32, user)
require.NoError(t, err)
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
})
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
perm, err := GetUserRepoPermission(ctx, repo32, user)
require.NoError(t, err)
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
})
}

View File

@@ -5,12 +5,14 @@ package pull
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
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/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
) )
// AutoMerge represents a pull request scheduled for merging when checks succeed // AutoMerge represents a pull request scheduled for merging when checks succeed
@@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return false, nil, err return false, nil, err
} }
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID) doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
if errors.Is(err, util.ErrNotExist) {
doer, err = user_model.NewGhostUser(), nil
}
if err != nil { if err != nil {
return false, nil, err return false, nil, err
} }

View File

@@ -1176,12 +1176,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
needCheckEmails := make(container.Set[string]) needCheckEmails := make(container.Set[string])
needCheckUserNames := make(container.Set[string]) needCheckUserNames := make(container.Set[string])
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
for _, email := range emails { for _, email := range emails {
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { emailLower := strings.ToLower(email)
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
needCheckUserNames.Add(strings.ToLower(username)) needCheckUserNames.Add(noReplyUserNameLower)
needCheckEmails.Add(emailLower)
} else { } else {
needCheckEmails.Add(strings.ToLower(email)) needCheckEmails.Add(emailLower)
} }
} }

View File

@@ -85,6 +85,10 @@ func TestUserEmails(t *testing.T) {
testGetUserByEmail(t, c.Email, c.UID) testGetUserByEmail(t, c.Email, c.UID)
}) })
} }
t.Run("NoReplyConflict", func(t *testing.T) {
setting.Service.NoReplyAddress = "example.com"
testGetUserByEmail(t, "user1-2@example.COM", 1)
})
}) })
} }

View File

@@ -6,22 +6,26 @@ package fileicon
import ( import (
"html/template" "html/template"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
) )
func BasicThemeIcon(entry *git.TreeEntry) template.HTML { func BasicEntryIconName(entry *EntryInfo) string {
svgName := "octicon-file" svgName := "octicon-file"
switch { switch {
case entry.IsLink(): case entry.EntryMode.IsLink():
svgName = "octicon-file-symlink-file" svgName = "octicon-file-symlink-file"
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.SymlinkToMode.IsDir() {
svgName = "octicon-file-directory-symlink" svgName = "octicon-file-directory-symlink"
} }
case entry.IsDir(): case entry.EntryMode.IsDir():
svgName = "octicon-file-directory-fill" svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
case entry.IsSubModule(): case entry.EntryMode.IsSubModule():
svgName = "octicon-file-submodule" svgName = "octicon-file-submodule"
} }
return svg.RenderHTML(svgName) return svgName
}
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
return svg.RenderHTML(BasicEntryIconName(entry))
} }

31
modules/fileicon/entry.go Normal file
View File

@@ -0,0 +1,31 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package fileicon
import "code.gitea.io/gitea/modules/git"
type EntryInfo struct {
FullName string
EntryMode git.EntryMode
SymlinkToMode git.EntryMode
IsOpen bool
}
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
if gitEntry.IsLink() {
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
ret.SymlinkToMode = te.Mode()
}
}
return ret
}
func EntryInfoFolder() *EntryInfo {
return &EntryInfo{EntryMode: git.EntryModeTree}
}
func EntryInfoFolderOpen() *EntryInfo {
return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
}

View File

@@ -9,11 +9,12 @@ import (
"strings" "strings"
"sync" "sync"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
) )
type materialIconRulesData struct { type materialIconRulesData struct {
@@ -69,42 +70,52 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
} }
svgID := "svg-mfi-" + name svgID := "svg-mfi-" + name
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
if p == nil {
return svgHTML
}
if p.IconSVGs[svgID] == "" { if p.IconSVGs[svgID] == "" {
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:]) p.IconSVGs[svgID] = svgHTML
} }
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`) return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
} }
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
if m.rules == nil { if m.rules == nil {
return BasicThemeIcon(entry) return BasicEntryIconHTML(entry)
} }
if entry.IsLink() { if entry.EntryMode.IsLink() {
if te, err := entry.FollowLink(); err == nil && te.IsDir() { if entry.SymlinkToMode.IsDir() {
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink") return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
} }
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
} }
name := m.findIconNameByGit(entry) name := m.FindIconName(entry)
// the material icon pack's "folder" icon doesn't look good, so use our built-in one iconSVG := m.svgs[name]
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work if iconSVG == "" {
if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" { name = "file"
if entry.EntryMode.IsDir() {
name = util.Iif(entry.IsOpen, "folder-open", "folder")
}
iconSVG = m.svgs[name]
if iconSVG == "" {
setting.PanicInDevOrTesting("missing file icon for %s", name)
}
}
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
extraClass := "octicon-file" extraClass := "octicon-file"
switch { switch {
case entry.IsDir(): case entry.EntryMode.IsDir():
extraClass = "octicon-file-directory-fill" extraClass = BasicEntryIconName(entry)
case entry.IsSubModule(): case entry.EntryMode.IsSubModule():
extraClass = "octicon-file-submodule" extraClass = "octicon-file-submodule"
} }
return m.renderFileIconSVG(p, name, iconSVG, extraClass) return m.renderFileIconSVG(p, name, iconSVG, extraClass)
} }
// TODO: use an interface or wrapper for git.Entry to make the code testable.
return BasicThemeIcon(entry)
}
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
if _, ok := m.svgs[s]; ok { if _, ok := m.svgs[s]; ok {
@@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
return "" return ""
} }
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
fileNameLower := strings.ToLower(path.Base(name)) if entry.EntryMode.IsSubModule() {
if isDir { return "folder-git"
}
fileNameLower := strings.ToLower(path.Base(entry.FullName))
if entry.EntryMode.IsDir() {
if s, ok := m.rules.FolderNames[fileNameLower]; ok { if s, ok := m.rules.FolderNames[fileNameLower]; ok {
return s return s
} }
return "folder" return util.Iif(entry.IsOpen, "folder-open", "folder")
} }
if s, ok := m.rules.FileNames[fileNameLower]; ok { if s, ok := m.rules.FileNames[fileNameLower]; ok {
@@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
return "file" return "file"
} }
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
if entry.IsSubModule() {
return "folder-git"
}
return m.FindIconName(entry.Name(), entry.IsDir())
}

View File

@@ -8,6 +8,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
func TestFindIconName(t *testing.T) { func TestFindIconName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
p := fileicon.DefaultMaterialIconProvider() p := fileicon.DefaultMaterialIconProvider()
assert.Equal(t, "php", p.FindIconName("foo.php", false)) assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
} }

View File

@@ -7,7 +7,6 @@ import (
"html/template" "html/template"
"strings" "strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
@@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
return template.HTML(sb.String()) return template.HTML(sb.String())
} }
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
if setting.UI.FileIconTheme == "material" { if setting.UI.FileIconTheme == "material" {
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
} }
return BasicThemeIcon(entry) return BasicEntryIconHTML(entry)
}
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
// TODO: add "open icon" support
if setting.UI.FileIconTheme == "material" {
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
}
return BasicThemeIcon(entry)
} }

View File

@@ -20,6 +20,7 @@ const (
GitlabLanguage = "gitlab-language" GitlabLanguage = "gitlab-language"
Lockable = "lockable" Lockable = "lockable"
Filter = "filter" Filter = "filter"
Diff = "diff"
) )
var LinguistAttributes = []string{ var LinguistAttributes = []string{

View File

@@ -9,3 +9,15 @@ type CommitInfo struct {
Commit *Commit Commit *Commit
SubmoduleFile *CommitSubmoduleFile SubmoduleFile *CommitSubmoduleFile
} }
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
submodule, err := commit.GetSubModule(fullPath)
if err != nil {
return nil, err
}
if submodule == nil {
// unable to find submodule from ".gitmodules" file
return NewCommitSubmoduleFile(repoLink, fullPath, "", refCommitID.String()), nil
}
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, refCommitID.String()), nil
}

View File

@@ -16,7 +16,7 @@ import (
) )
// GetCommitsInfo gets information of all commits that are corresponding to these entries // GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1) entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself // Get the commit for the treePath itself
entryPaths[0] = "" entryPaths[0] = ""
@@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
commitsInfo[i].Commit = entryCommit commitsInfo[i].Commit = entryCommit
} }
// If the entry is a submodule add a submodule file for this // If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() { if entry.IsSubModule() {
subModuleURL := "" commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
var fullPath string if err != nil {
if len(treePath) > 0 {
fullPath = treePath + "/" + entry.Name()
} else {
fullPath = entry.Name()
}
if subModule, err := commit.GetSubModule(fullPath); err != nil {
return nil, nil, err return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
} }
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubmoduleFile = subModuleFile
} }
} }

View File

@@ -16,7 +16,7 @@ import (
) )
// GetCommitsInfo gets information of all commits that are corresponding to these entries // GetCommitsInfo gets information of all commits that are corresponding to these entries
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
entryPaths := make([]string, len(tes)+1) entryPaths := make([]string, len(tes)+1)
// Get the commit for the treePath itself // Get the commit for the treePath itself
entryPaths[0] = "" entryPaths[0] = ""
@@ -65,22 +65,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
log.Debug("missing commit for %s", entry.Name()) log.Debug("missing commit for %s", entry.Name())
} }
// If the entry is a submodule add a submodule file for this // If the entry is a submodule, add a submodule file for this
if entry.IsSubModule() { if entry.IsSubModule() {
subModuleURL := "" commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
var fullPath string if err != nil {
if len(treePath) > 0 {
fullPath = treePath + "/" + entry.Name()
} else {
fullPath = entry.Name()
}
if subModule, err := commit.GetSubModule(fullPath); err != nil {
return nil, nil, err return nil, nil, err
} else if subModule != nil {
subModuleURL = subModule.URL
} }
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
commitsInfo[i].SubmoduleFile = subModuleFile
} }
} }

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@@ -82,7 +83,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
} }
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path) commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
if err != nil { if err != nil {
t.FailNow() t.FailNow()
@@ -120,6 +121,23 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
defer clonedRepo1.Close() defer clonedRepo1.Close()
testGetCommitsInfo(t, clonedRepo1) testGetCommitsInfo(t, clonedRepo1)
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
commit, err := bareRepo1.GetCommit("HEAD")
require.NoError(t, err)
treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
require.NoError(t, err)
cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
require.NoError(t, err)
assert.Equal(t, &CommitSubmoduleFile{
repoLink: "/any/repo-link",
fullPath: "file1.txt",
refURL: "",
refID: "e2129701f1a4d54dc44f03c93bca0a2aec7c5449",
}, cisf)
// since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
})
} }
func BenchmarkEntries_GetCommitsInfo(b *testing.B) { func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
@@ -159,7 +177,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.Run(benchmark.name, func(b *testing.B) { b.Run(benchmark.name, func(b *testing.B) {
for b.Loop() { for b.Loop() {
_, _, err := entries.GetCommitsInfo(b.Context(), commit, "") _, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }

View File

@@ -35,7 +35,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
return c.submoduleCache, nil return c.submoduleCache, nil
} }
// GetSubModule get the submodule according entry name // GetSubModule gets the submodule by the entry name.
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
modules, err := c.GetSubModules() modules, err := c.GetSubModules()
if err != nil { if err != nil {

View File

@@ -6,49 +6,64 @@ package git
import ( import (
"context" "context"
"path"
"strings"
giturl "code.gitea.io/gitea/modules/git/url" giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/util"
) )
// CommitSubmoduleFile represents a file with submodule type. // CommitSubmoduleFile represents a file with submodule type.
type CommitSubmoduleFile struct { type CommitSubmoduleFile struct {
refURL string
parsedURL *giturl.RepositoryURL
parsed bool
refID string
repoLink string repoLink string
fullPath string
refURL string
refID string
parsed bool
parsedTargetLink string
} }
// NewCommitSubmoduleFile create a new submodule file // NewCommitSubmoduleFile create a new submodule file
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile { func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile {
return &CommitSubmoduleFile{refURL: refURL, refID: refID} return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
} }
// RefID returns the commit ID of the submodule, it returns empty string for nil receiver
func (sf *CommitSubmoduleFile) RefID() string { func (sf *CommitSubmoduleFile) RefID() string {
return sf.refID // this function is only used in templates if sf == nil {
return ""
}
return sf.refID
} }
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink { if sf == nil || sf.refURL == "" {
if sf == nil {
return nil return nil
} }
if strings.HasPrefix(sf.refURL, "../") {
targetLink := path.Join(sf.repoLink, sf.refURL)
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
}
if !sf.parsed { if !sf.parsed {
sf.parsed = true sf.parsed = true
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL) parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
if err != nil { if err != nil {
return nil return nil
} }
sf.parsedURL = parsedURL sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
} }
var commitLink string return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
if len(optCommitID) == 2 {
commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
} else if len(optCommitID) == 1 {
commitLink = sf.repoLink + "/tree/" + optCommitID[0]
} else {
commitLink = sf.repoLink + "/tree/" + sf.refID
} }
return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
// It returns nil if the submodule does not have a valid URL or is nil
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID()))
}
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
// It returns nil if the submodule does not have a valid URL or is nil
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
} }

View File

@@ -10,20 +10,31 @@ import (
) )
func TestCommitSubmoduleLink(t *testing.T) { func TestCommitSubmoduleLink(t *testing.T) {
sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa") assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context()))
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", ""))
wl := sf.SubmoduleWebLink(t.Context()) t.Run("GitHubRepo", func(t *testing.T) {
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
wl := sf.SubmoduleWebLinkTree(t.Context())
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink) assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(t.Context(), "1111") wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222")
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink) assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink) assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
})
wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context()) t.Run("RelativePath", func(t *testing.T) {
assert.Nil(t, wl) sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa")
wl := sf.SubmoduleWebLinkTree(t.Context())
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../user/repo", "aaaa")
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
})
} }

View File

@@ -100,8 +100,8 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
} }
// ParseDiffHunkString parse the diff hunk content and return // ParseDiffHunkString parse the diff hunk content and return
func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) { func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) {
ss := strings.Split(diffhunk, "@@") ss := strings.Split(diffHunk, "@@")
ranges := strings.Split(ss[1][1:], " ") ranges := strings.Split(ss[1][1:], " ")
leftRange := strings.Split(ranges[0], ",") leftRange := strings.Split(ranges[0], ",")
leftLine, _ = strconv.Atoi(leftRange[0][1:]) leftLine, _ = strconv.Atoi(leftRange[0][1:])
@@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
rightRange := strings.Split(ranges[1], ",") rightRange := strings.Split(ranges[1], ",")
rightLine, _ = strconv.Atoi(rightRange[0]) rightLine, _ = strconv.Atoi(rightRange[0])
if len(rightRange) > 1 { if len(rightRange) > 1 {
righHunk, _ = strconv.Atoi(rightRange[1]) rightHunk, _ = strconv.Atoi(rightRange[1])
} }
} else { } else {
log.Debug("Parse line number failed: %v", diffhunk) log.Debug("Parse line number failed: %v", diffHunk)
rightLine = leftLine rightLine = leftLine
righHunk = leftHunk rightHunk = leftHunk
} }
return leftLine, leftHunk, rightLine, righHunk if rightLine == 0 {
// FIXME: GIT-DIFF-CUT-BUG search this tag to see details
// this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases.
rightLine++
}
return leftLine, leftHunk, rightLine, rightHunk
} }
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9] // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
@@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
oldNumOfLines++ oldNumOfLines++
} }
} }
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
// FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@"
// It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check.
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
// construct the new hunk header // construct the new hunk header
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@", newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
oldBegin, oldNumOfLines, newBegin, newNumOfLines) oldBegin, oldNumOfLines, newBegin, newNumOfLines)

View File

@@ -30,6 +30,31 @@ func (e EntryMode) String() string {
return strconv.FormatInt(int64(e), 8) return strconv.FormatInt(int64(e), 8)
} }
// IsSubModule if the entry is a sub module
func (e EntryMode) IsSubModule() bool {
return e == EntryModeCommit
}
// IsDir if the entry is a sub dir
func (e EntryMode) IsDir() bool {
return e == EntryModeTree
}
// IsLink if the entry is a symlink
func (e EntryMode) IsLink() bool {
return e == EntryModeSymlink
}
// IsRegular if the entry is a regular file
func (e EntryMode) IsRegular() bool {
return e == EntryModeBlob
}
// IsExecutable if the entry is an executable file (not necessarily binary)
func (e EntryMode) IsExecutable() bool {
return e == EntryModeExec
}
func ParseEntryMode(mode string) (EntryMode, error) { func ParseEntryMode(mode string) (EntryMode, error) {
switch mode { switch mode {
case "000000": case "000000":

View File

@@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
// IsSubModule if the entry is a sub module // IsSubModule if the entry is a sub module
func (te *TreeEntry) IsSubModule() bool { func (te *TreeEntry) IsSubModule() bool {
return te.entryMode == EntryModeCommit return te.entryMode.IsSubModule()
} }
// IsDir if the entry is a sub dir // IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool { func (te *TreeEntry) IsDir() bool {
return te.entryMode == EntryModeTree return te.entryMode.IsDir()
} }
// IsLink if the entry is a symlink // IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool { func (te *TreeEntry) IsLink() bool {
return te.entryMode == EntryModeSymlink return te.entryMode.IsLink()
} }
// IsRegular if the entry is a regular file // IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool { func (te *TreeEntry) IsRegular() bool {
return te.entryMode == EntryModeBlob return te.entryMode.IsRegular()
} }
// IsExecutable if the entry is an executable file (not necessarily binary) // IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool { func (te *TreeEntry) IsExecutable() bool {
return te.entryMode == EntryModeExec return te.entryMode.IsExecutable()
} }
// Blob returns the blob object the entry // Blob returns the blob object the entry

View File

@@ -11,6 +11,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"code.gitea.io/gitea/modules/util"
) )
// ObjectCache provides thread-safe cache operations. // ObjectCache provides thread-safe cache operations.
@@ -106,3 +108,16 @@ func HashFilePathForWebUI(s string) string {
_, _ = h.Write([]byte(s)) _, _ = h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
} }
func SplitCommitTitleBody(commitMessage string, titleRuneLimit int) (title, body string) {
title, body, _ = strings.Cut(commitMessage, "\n")
title, title2 := util.EllipsisTruncateRunes(title, titleRuneLimit)
if title2 != "" {
if body == "" {
body = title2
} else {
body = title2 + "\n" + body
}
}
return title, body
}

View File

@@ -15,3 +15,17 @@ func TestHashFilePathForWebUI(t *testing.T) {
HashFilePathForWebUI("foobar"), HashFilePathForWebUI("foobar"),
) )
} }
func TestSplitCommitTitleBody(t *testing.T) {
title, body := SplitCommitTitleBody("啊bcdefg", 4)
assert.Equal(t, "啊…", title)
assert.Equal(t, "…bcdefg", body)
title, body = SplitCommitTitleBody("abcdefg\n1234567", 4)
assert.Equal(t, "a…", title)
assert.Equal(t, "…bcdefg\n1234567", body)
title, body = SplitCommitTitleBody("abcdefg\n1234567", 100)
assert.Equal(t, "abcdefg", title)
assert.Equal(t, "1234567", body)
}

View File

@@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head
return nil return nil
} }
req := private.NewInternalRequest(ctx, internalURL, method) req := private.NewInternalRequest(ctx, internalURL, method)
req.SetReadWriteTimeout(0)
for k, v := range headers { for k, v := range headers {
req.Header(k, v) req.Header(k, v)
} }

View File

@@ -22,6 +22,13 @@ func FromPtr[T any](v *T) Option[T] {
return Some(*v) return Some(*v)
} }
func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] {
if v, ok := m[k]; ok {
return Some(v)
}
return None[V]()
}
func FromNonDefault[T comparable](v T) Option[T] { func FromNonDefault[T comparable](v T) Option[T] {
var zero T var zero T
if v == zero { if v == zero {

View File

@@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
opt3 := optional.FromNonDefault(1) opt3 := optional.FromNonDefault(1)
assert.True(t, opt3.Has()) assert.True(t, opt3.Has())
assert.Equal(t, int(1), opt3.Value()) assert.Equal(t, int(1), opt3.Value())
opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a")
assert.True(t, opt4.Has())
assert.Equal(t, 1, opt4.Value())
opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b")
assert.False(t, opt4.Has())
} }
func Test_ParseBool(t *testing.T) { func Test_ParseBool(t *testing.T) {

View File

@@ -41,11 +41,14 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
if err != nil { if err != nil {
return 0, fmt.Errorf("GetObjectFormat: %w", err) return 0, fmt.Errorf("GetObjectFormat: %w", err)
} }
_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
if repo.ObjectFormatName != objFmt.Name() {
repo.ObjectFormatName = objFmt.Name()
_, err = db.GetEngine(ctx).ID(repo.ID).NoAutoTime().Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
if err != nil { if err != nil {
return 0, fmt.Errorf("UpdateRepository: %w", err) return 0, fmt.Errorf("UpdateRepository: %w", err)
} }
repo.ObjectFormatName = objFmt.Name() // keep consistent with db }
allBranches := container.Set[string]{} allBranches := container.Set[string]{}
{ {

View File

@@ -24,7 +24,7 @@ var (
ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"` ZombieTaskTimeout time.Duration `ini:"ZOMBIE_TASK_TIMEOUT"`
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"` EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
}{ }{
Enabled: true, Enabled: true,
DefaultActionsURL: defaultActionsURLGitHub, DefaultActionsURL: defaultActionsURLGitHub,

View File

@@ -41,3 +41,56 @@ EXTEND = true
assert.Equal(t, "white rabbit", extended.Second) assert.Equal(t, "white rabbit", extended.Second)
assert.True(t, extended.Extend) assert.True(t, extended.Extend)
} }
// Test_getCronSettings2 tests that getCronSettings can not handle two levels of embedding
func Test_getCronSettings2(t *testing.T) {
type BaseStruct struct {
Enabled bool
RunAtStart bool
Schedule string
}
type Extended struct {
BaseStruct
Extend bool
}
type Extended2 struct {
Extended
Third string
}
iniStr := `
[cron.test]
ENABLED = TRUE
RUN_AT_START = TRUE
SCHEDULE = @every 1h
EXTEND = true
THIRD = white rabbit
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
extended := &Extended2{
Extended: Extended{
BaseStruct: BaseStruct{
Enabled: false,
RunAtStart: false,
Schedule: "@every 72h",
},
Extend: false,
},
Third: "black rabbit",
}
_, err = getCronSettings(cfg, "test", extended)
assert.NoError(t, err)
// This confirms the first level of embedding works
assert.Equal(t, "white rabbit", extended.Third)
assert.True(t, extended.Extend)
// This confirms 2 levels of embedding doesn't work
assert.False(t, extended.Enabled)
assert.False(t, extended.RunAtStart)
assert.Equal(t, "@every 72h", extended.Schedule)
}

View File

@@ -275,7 +275,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr) HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
} }
default: default:
log.Fatal("Invalid PROTOCOL %q", Protocol) log.Fatal("Invalid PROTOCOL %q", protocolCfg)
} }
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false) UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false) ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)

View File

@@ -57,7 +57,7 @@ type Repository struct {
Private bool `json:"private"` Private bool `json:"private"`
Fork bool `json:"fork"` Fork bool `json:"fork"`
Template bool `json:"template"` Template bool `json:"template"`
Parent *Repository `json:"parent"` Parent *Repository `json:"parent,omitempty"`
Mirror bool `json:"mirror"` Mirror bool `json:"mirror"`
Size int `json:"size"` Size int `json:"size"`
Language string `json:"language"` Language string `json:"language"`
@@ -112,7 +112,7 @@ type Repository struct {
ObjectFormatName string `json:"object_format_name"` ObjectFormatName string `json:"object_format_name"`
// swagger:strfmt date-time // swagger:strfmt date-time
MirrorUpdated time.Time `json:"mirror_updated,omitempty"` MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
RepoTransfer *RepoTransfer `json:"repo_transfer"` RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
Topics []string `json:"topics"` Topics []string `json:"topics"`
Licenses []string `json:"licenses"` Licenses []string `json:"licenses"`
} }

View File

@@ -19,7 +19,7 @@ func IsLikelyEllipsisLeftPart(s string) bool {
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis) return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
} }
func ellipsisGuessDisplayWidth(r rune) int { func ellipsisDisplayGuessWidth(r rune) int {
// To make the truncated string as long as possible, // To make the truncated string as long as possible,
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width. // CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
// Here we only make the best guess (better than counting them in bytes), // Here we only make the best guess (better than counting them in bytes),
@@ -48,13 +48,17 @@ func ellipsisGuessDisplayWidth(r rune) int {
// It appends "…" or "..." at the end of truncated string. // It appends "…" or "..." at the end of truncated string.
// It guarantees the length of the returned runes doesn't exceed the limit. // It guarantees the length of the returned runes doesn't exceed the limit.
func EllipsisDisplayString(str string, limit int) string { func EllipsisDisplayString(str string, limit int) string {
s, _, _, _ := ellipsisDisplayString(str, limit) s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
return s return s
} }
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part // EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
func EllipsisDisplayStringX(str string, limit int) (left, right string) { func EllipsisDisplayStringX(str string, limit int) (left, right string) {
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit) return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
}
func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
if truncated { if truncated {
right = str[offset:] right = str[offset:]
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right)) r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
@@ -68,7 +72,7 @@ func EllipsisDisplayStringX(str string, limit int) (left, right string) {
return left, right return left, right
} }
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) { func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
if len(str) <= limit { if len(str) <= limit {
return str, len(str), false, false return str, len(str), false, false
} }
@@ -81,7 +85,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
for i, r := range str { for i, r := range str {
encounterInvalid = encounterInvalid || r == utf8.RuneError encounterInvalid = encounterInvalid || r == utf8.RuneError
pos = i pos = i
runeWidth := ellipsisGuessDisplayWidth(r) runeWidth := widthGuess(r)
if used+runeWidth+3 > limit { if used+runeWidth+3 > limit {
break break
} }
@@ -96,7 +100,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
if nextCnt >= 4 { if nextCnt >= 4 {
break break
} }
nextWidth += ellipsisGuessDisplayWidth(r) nextWidth += widthGuess(r)
nextCnt++ nextCnt++
} }
if nextCnt <= 3 && used+nextWidth <= limit { if nextCnt <= 3 && used+nextWidth <= limit {
@@ -114,6 +118,10 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
return str[:offset] + ellipsis, offset, true, encounterInvalid return str[:offset] + ellipsis, offset, true, encounterInvalid
} }
func EllipsisTruncateRunes(str string, limit int) (left, right string) {
return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
}
// TruncateRunes returns a truncated string with given rune limit, // TruncateRunes returns a truncated string with given rune limit,
// it returns input string if its rune length doesn't exceed the limit. // it returns input string if its rune length doesn't exceed the limit.
func TruncateRunes(str string, limit int) string { func TruncateRunes(str string, limit int) string {

View File

@@ -29,7 +29,7 @@ func TestEllipsisGuessDisplayWidth(t *testing.T) {
t.Run(c.r, func(t *testing.T) { t.Run(c.r, func(t *testing.T) {
w := 0 w := 0
for _, r := range c.r { for _, r := range c.r {
w += ellipsisGuessDisplayWidth(r) w += ellipsisDisplayGuessWidth(r)
} }
assert.Equal(t, c.want, w, "hex=% x", []byte(c.r)) assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
}) })

View File

@@ -1957,7 +1957,7 @@ pulls.cmd_instruction_checkout_title = Checkout
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
pulls.cmd_instruction_merge_title = Merge pulls.cmd_instruction_merge_title = Merge
pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea. pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea.
pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable pulls.cmd_instruction_merge_warning = Warning: This operation cannot merge pull request because "autodetect manual merge" is not enabled.
pulls.clear_merge_message = Clear merge message pulls.clear_merge_message = Clear merge message
pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …". pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …".
@@ -2153,6 +2153,7 @@ settings.collaboration.write = Write
settings.collaboration.read = Read settings.collaboration.read = Read
settings.collaboration.owner = Owner settings.collaboration.owner = Owner
settings.collaboration.undefined = Undefined settings.collaboration.undefined = Undefined
settings.collaboration.per_unit = Unit Permissions
settings.hooks = Webhooks settings.hooks = Webhooks
settings.githooks = Git Hooks settings.githooks = Git Hooks
settings.basic_settings = Basic Settings settings.basic_settings = Basic Settings

View File

@@ -320,6 +320,7 @@ func InitiateUploadBlob(ctx *context.Context) {
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
func GetUploadBlob(ctx *context.Context) { func GetUploadBlob(ctx *context.Context) {
image := ctx.PathParam("image")
uuid := ctx.PathParam("uuid") uuid := ctx.PathParam("uuid")
upload, err := packages_model.GetBlobUploadByID(ctx, uuid) upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
@@ -334,6 +335,7 @@ func GetUploadBlob(ctx *context.Context) {
// FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578 // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
respHeaders := &containerHeaders{ respHeaders := &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
UploadUUID: upload.ID, UploadUUID: upload.ID,
Status: http.StatusNoContent, Status: http.StatusNoContent,
} }
@@ -386,7 +388,7 @@ func UploadBlob(ctx *context.Context) {
UploadUUID: uploader.ID, UploadUUID: uploader.ID,
Status: http.StatusAccepted, Status: http.StatusAccepted,
} }
if contentRange != "" { if uploader.Size() > 0 {
respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1) respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
} }
setResponseHeaders(ctx.Resp, respHeaders) setResponseHeaders(ctx.Resp, respHeaders)

View File

@@ -240,7 +240,7 @@ func EditUser(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description), Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active), IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal), AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),

View File

@@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) {
} }
} }
if !ctx.Repo.Permission.HasAnyUnitAccess() { if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
ctx.APIErrorNotFound() ctx.APIErrorNotFound()
return return
} }
@@ -1241,7 +1241,7 @@ func Routes() *web.Router {
}, reqToken()) }, reqToken())
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks). m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream)
@@ -1445,7 +1445,7 @@ func Routes() *web.Router {
m.Delete("", repo.DeleteAvatar) m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken()) }, reqAdmin(), reqToken())
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
}, repoAssignment(), checkTokenPublicOnly()) }, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))

View File

@@ -393,7 +393,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.Some(form.Description), Description: optional.Some(form.Description),
Website: optional.Some(form.Website), Website: optional.Some(form.Website),
Location: optional.Some(form.Location), Location: optional.Some(form.Location),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
} }
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {

View File

@@ -44,5 +44,5 @@ type swaggerResponseActionWorkflow struct {
// swagger:response ActionWorkflowList // swagger:response ActionWorkflowList
type swaggerResponseActionWorkflowList struct { type swaggerResponseActionWorkflowList struct {
// in:body // in:body
Body []api.ActionWorkflow `json:"body"` Body api.ActionWorkflowResponse `json:"body"`
} }

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
@@ -15,11 +16,15 @@ import (
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed // ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) { func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "") var commits []*git.Commit
var err error
if ctx.Repo.Commit != nil {
commits, err = ctx.Repo.Commit.CommitsByRange(0, 10, "")
if err != nil { if err != nil {
ctx.ServerError("ShowBranchFeed", err) ctx.ServerError("ShowBranchFeed", err)
return return
} }
}
title := "Latest commits for branch " + ctx.Repo.BranchName title := "Latest commits for branch " + ctx.Repo.BranchName
link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL()} link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL()}

View File

@@ -283,7 +283,18 @@ func NewTeam(ctx *context.Context) {
} }
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future, // FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
// admin team won't inherit the correct admin permission for the new unit. // The existing teams won't inherit the correct admin permission for the new unit.
// The full history is like this:
// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
// - Sometimes, "team unit" is used not really used and "team unit" is used.
// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
//
// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
//
// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode { func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
unitPerms := make(map[unit_model.Type]perm.AccessMode) unitPerms := make(map[unit_model.Type]perm.AccessMode)
for _, ut := range unit_model.AllRepoUnitTypes { for _, ut := range unit_model.AllRepoUnitTypes {

View File

@@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -168,10 +169,13 @@ func Graph(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name
divOnly := ctx.FormBool("div-only")
queryParams := ctx.Req.URL.Query()
queryParams.Del("div-only")
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
paginator.AddParamFromRequest(ctx.Req) paginator.AddParamFromQuery(queryParams)
ctx.Data["Page"] = paginator ctx.Data["Page"] = paginator
if ctx.FormBool("div-only") { if divOnly {
ctx.HTML(http.StatusOK, tplGraphDiv) ctx.HTML(http.StatusOK, tplGraphDiv)
return return
} }
@@ -313,7 +317,7 @@ func Diff(ctx *context.Context) {
maxLines, maxFiles = -1, -1 maxLines, maxFiles = -1, -1
} }
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{ diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID, AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"), SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines, MaxLines: maxLines,
@@ -369,7 +373,11 @@ func Diff(ctx *context.Context) {
return return
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)

View File

@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv" csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@@ -613,7 +614,7 @@ func PrepareCompareDiff(
fileOnly := ctx.FormBool("file-only") fileOnly := ctx.FormBool("file-only")
diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo, diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadRepo.Link(), ci.HeadGitRepo,
&gitdiff.DiffOptions{ &gitdiff.DiffOptions{
BeforeCommitID: beforeCommitID, BeforeCommitID: beforeCommitID,
AfterCommitID: headCommitID, AfterCommitID: headCommitID,
@@ -644,7 +645,11 @@ func PrepareCompareDiff(
return false return false
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil) renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID) headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)

View File

@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
@@ -642,8 +643,17 @@ func ViewPullCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplPullCommits) ctx.HTML(http.StatusOK, tplPullCommits)
} }
func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
for i := range commits {
if commits[i].ID.String() == commitID {
return commits[i]
}
}
return nil
}
// ViewPullFiles render pull request changed files list page // ViewPullFiles render pull request changed files list page
func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) { func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
ctx.Data["PageIsPullList"] = true ctx.Data["PageIsPullList"] = true
ctx.Data["PageIsPullFiles"] = true ctx.Data["PageIsPullFiles"] = true
@@ -653,11 +663,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} }
pull := issue.PullRequest pull := issue.PullRequest
var ( gitRepo := ctx.Repo.GitRepo
startCommitID string
endCommitID string
gitRepo = ctx.Repo.GitRepo
)
prInfo := preparePullViewPullInfo(ctx, issue) prInfo := preparePullViewPullInfo(ctx, issue)
if ctx.Written() { if ctx.Written() {
@@ -667,77 +673,68 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return return
} }
// Validate the given commit sha to show (if any passed)
if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
foundStartCommit := len(specifiedStartCommit) == 0
foundEndCommit := len(specifiedEndCommit) == 0
if !(foundStartCommit && foundEndCommit) {
for _, commit := range prInfo.Commits {
if commit.ID.String() == specifiedStartCommit {
foundStartCommit = true
}
if commit.ID.String() == specifiedEndCommit {
foundEndCommit = true
}
if foundStartCommit && foundEndCommit {
break
}
}
}
if !(foundStartCommit && foundEndCommit) {
ctx.NotFound(nil)
return
}
}
if ctx.Written() {
return
}
headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName()) headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil { if err != nil {
ctx.ServerError("GetRefCommitID", err) ctx.ServerError("GetRefCommitID", err)
return return
} }
ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit isSingleCommit := beforeCommitID == "" && afterCommitID != ""
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
if willShowSpecifiedCommit || willShowSpecifiedCommitRange { if afterCommitID == "" || afterCommitID == headCommitID {
if len(specifiedEndCommit) > 0 { afterCommitID = headCommitID
endCommitID = specifiedEndCommit
} else {
endCommitID = headCommitID
} }
if len(specifiedStartCommit) > 0 { afterCommit := indexCommit(prInfo.Commits, afterCommitID)
startCommitID = specifiedStartCommit if afterCommit == nil {
} else { ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
startCommitID = prInfo.MergeBase return
}
var beforeCommit *git.Commit
if !isSingleCommit {
if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase {
beforeCommitID = prInfo.MergeBase
// mergebase commit is not in the list of the pull request commits
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
} }
ctx.Data["IsShowingAllCommits"] = false
} else { } else {
endCommitID = headCommitID beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
startCommitID = prInfo.MergeBase if beforeCommit == nil {
ctx.Data["IsShowingAllCommits"] = true ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
return
}
}
} else {
beforeCommit, err = afterCommit.Parent(0)
if err != nil {
ctx.ServerError("Parent", err)
return
}
beforeCommitID = beforeCommit.ID.String()
} }
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["AfterCommitID"] = endCommitID ctx.Data["MergeBase"] = prInfo.MergeBase
ctx.Data["BeforeCommitID"] = startCommitID ctx.Data["AfterCommitID"] = afterCommitID
ctx.Data["BeforeCommitID"] = beforeCommitID
fileOnly := ctx.FormBool("file-only")
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
files := ctx.FormStrings("files") files := ctx.FormStrings("files")
fileOnly := ctx.FormBool("file-only")
if fileOnly && (len(files) == 2 || len(files) == 1) { if fileOnly && (len(files) == 2 || len(files) == 1) {
maxLines, maxFiles = -1, -1 maxLines, maxFiles = -1, -1
} }
diffOptions := &gitdiff.DiffOptions{ diffOptions := &gitdiff.DiffOptions{
AfterCommitID: endCommitID, BeforeCommitID: beforeCommitID,
AfterCommitID: afterCommitID,
SkipTo: ctx.FormString("skip-to"), SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines, MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters, MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
@@ -745,11 +742,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)), WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
} }
if !willShowSpecifiedCommit { diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...)
diffOptions.BeforeCommitID = startCommitID
}
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
if err != nil { if err != nil {
ctx.ServerError("GetDiff", err) ctx.ServerError("GetDiff", err)
return return
@@ -760,7 +753,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
// as the viewed information is designed to be loaded only on latest PR // as the viewed information is designed to be loaded only on latest PR
// diff and if you're signed in. // diff and if you're signed in.
var reviewState *pull_model.ReviewState var reviewState *pull_model.ReviewState
if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange { if ctx.IsSigned && isShowAllCommits {
reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions) reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
if err != nil { if err != nil {
ctx.ServerError("SyncUserSpecificDiff", err) ctx.ServerError("SyncUserSpecificDiff", err)
@@ -768,7 +761,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} }
} }
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, startCommitID, endCommitID) diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
if err != nil { if err != nil {
ctx.ServerError("GetDiffShortStat", err) ctx.ServerError("GetDiffShortStat", err)
return return
@@ -815,7 +808,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if !fileOnly { if !fileOnly {
// note: use mergeBase is set to false because we already have the merge base from the pull request info // note: use mergeBase is set to false because we already have the merge base from the pull request info
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID) diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, beforeCommitID, afterCommitID)
if err != nil { if err != nil {
ctx.ServerError("GetDiffTree", err) ctx.ServerError("GetDiffTree", err)
return return
@@ -824,23 +817,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if reviewState != nil { if reviewState != nil {
filesViewedState = reviewState.UpdatedFiles filesViewedState = reviewState.UpdatedFiles
} }
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
renderedIconPool := fileicon.NewRenderedIconPool()
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
ctx.Data["Diff"] = diff ctx.Data["Diff"] = diff
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0 ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
commit, err := gitRepo.GetCommit(endCommitID)
if err != nil {
ctx.ServerError("GetCommit", err)
return
}
if ctx.IsSigned && ctx.Doer != nil { if ctx.IsSigned && ctx.Doer != nil {
if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
ctx.ServerError("CanMarkConversation", err) ctx.ServerError("CanMarkConversation", err)
@@ -848,7 +835,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} }
} }
setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name) setCompareContext(ctx, beforeCommit, afterCommit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil { if err != nil {
@@ -895,7 +882,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
} }
if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub { if isShowAllCommits && pull.Flow == issues_model.PullRequestFlowGithub {
if err := pull.LoadHeadRepo(ctx); err != nil { if err := pull.LoadHeadRepo(ctx); err != nil {
ctx.ServerError("LoadHeadRepo", err) ctx.ServerError("LoadHeadRepo", err)
return return
@@ -924,19 +911,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} }
func ViewPullFilesForSingleCommit(ctx *context.Context) { func ViewPullFilesForSingleCommit(ctx *context.Context) {
viewPullFiles(ctx, "", ctx.PathParam("sha"), true, true) // it doesn't support showing files from mergebase to the special commit
// otherwise it will be ambiguous
viewPullFiles(ctx, "", ctx.PathParam("sha"))
} }
func ViewPullFilesForRange(ctx *context.Context) { func ViewPullFilesForRange(ctx *context.Context) {
viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"), true, false) viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"))
}
func ViewPullFilesStartingFromCommit(ctx *context.Context) {
viewPullFiles(ctx, "", ctx.PathParam("sha"), true, false)
} }
func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) { func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) {
viewPullFiles(ctx, "", "", false, false) viewPullFiles(ctx, "", "")
} }
// UpdatePullRequest merge PR's baseBranch into headBranch // UpdatePullRequest merge PR's baseBranch into headBranch

View File

@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
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/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
@@ -89,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["recent_status_checks"] = contexts c.Data["recent_status_checks"] = contexts
if c.Repo.Owner.IsOrganization() { if c.Repo.Owner.IsOrganization() {
teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead) teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil { if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return return

View File

@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "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/models/unit"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
ctx.Data["Users"] = users ctx.Data["Users"] = users
if ctx.Repo.Owner.IsOrganization() { if ctx.Repo.Owner.IsOrganization() {
teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead) teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil { if err != nil {
ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return err return err

View File

@@ -4,6 +4,7 @@
package repo package repo
import ( import (
"html/template"
"net/http" "net/http"
"strings" "strings"
@@ -67,7 +68,7 @@ type WebDiffFileItem struct {
EntryMode string EntryMode string
IsViewed bool IsViewed bool
Children []*WebDiffFileItem Children []*WebDiffFileItem
// TODO: add icon support in the future FileIcon template.HTML
} }
// WebDiffFileTree is used by frontend, check the field names in frontend before changing // WebDiffFileTree is used by frontend, check the field names in frontend before changing
@@ -77,7 +78,7 @@ type WebDiffFileTree struct {
// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering // transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
// it also takes a map of file names to their viewed state, which is used to mark files as viewed // it also takes a map of file names to their viewed state, which is used to mark files as viewed
func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) { func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot} dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
addItem := func(item *WebDiffFileItem) { addItem := func(item *WebDiffFileItem) {
var parentPath string var parentPath string
@@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
item.NameHash = git.HashFilePathForWebUI(item.FullName) item.NameHash = git.HashFilePathForWebUI(item.FullName)
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
switch file.HeadMode { switch file.HeadMode {
case git.EntryModeTree: case git.EntryModeTree:
@@ -141,7 +143,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
func TreeViewNodes(ctx *context.Context) { func TreeViewNodes(ctx *context.Context) {
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path")) results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
if err != nil { if err != nil {
ctx.ServerError("GetTreeViewNodes", err) ctx.ServerError("GetTreeViewNodes", err)
return return

View File

@@ -4,9 +4,11 @@
package repo package repo
import ( import (
"html/template"
"testing" "testing"
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
@@ -14,7 +16,8 @@ import (
) )
func TestTransformDiffTreeForWeb(t *testing.T) { func TestTransformDiffTreeForWeb(t *testing.T) {
ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{ renderedIconPool := fileicon.NewRenderedIconPool()
ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
{ {
Status: "changed", Status: "changed",
HeadPath: "dir-a/dir-a-x/file-deep", HeadPath: "dir-a/dir-a-x/file-deep",
@@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
"dir-a/dir-a-x/file-deep": pull_model.Viewed, "dir-a/dir-a-x/file-deep": pull_model.Viewed,
}) })
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>`)
}
assert.Equal(t, WebDiffFileTree{ assert.Equal(t, WebDiffFileTree{
TreeRoot: WebDiffFileItem{ TreeRoot: WebDiffFileItem{
Children: []*WebDiffFileItem{ Children: []*WebDiffFileItem{
@@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b", NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
DiffStatus: "changed", DiffStatus: "changed",
IsViewed: true, IsViewed: true,
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },
@@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
FullName: "file1", FullName: "file1",
NameHash: "60b27f004e454aca81b0480209cce5081ec52390", NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
DiffStatus: "added", DiffStatus: "added",
FileIcon: mockIconForFile(`svg-mfi-file`),
}, },
}, },
}, },

View File

@@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
fileIcons := map[string]template.HTML{} fileIcons := map[string]template.HTML{}
for _, f := range files { for _, f := range files {
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry) fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry))
} }
fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
ctx.Data["FileIcons"] = fileIcons ctx.Data["FileIcons"] = fileIcons
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
} }
@@ -298,7 +299,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
defer cancel() defer cancel()
} }
files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil { if err != nil {
ctx.ServerError("GetCommitsInfo", err) ctx.ServerError("GetCommitsInfo", err)
return nil return nil

View File

@@ -20,8 +20,8 @@ import (
unit_model "code.gitea.io/gitea/models/unit" unit_model "code.gitea.io/gitea/models/unit"
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"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
@@ -309,34 +309,41 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
ctx.Redirect(link) ctx.Redirect(link)
} }
func handleRepoViewSubmodule(ctx *context.Context, submodule *git.SubModule) { func isViewHomeOnlyContent(ctx *context.Context) bool {
submoduleRepoURL, err := giturl.ParseRepositoryURL(ctx, submodule.URL) return ctx.FormBool("only_content")
if err != nil { }
HandleGitError(ctx, "prepareToRenderDirOrFile: ParseRepositoryURL", err)
func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
if submoduleWebLink == nil {
ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
ctx.NotFound(nil)
return return
} }
submoduleURL := giturl.MakeRepositoryWebLink(submoduleRepoURL)
if httplib.IsCurrentGiteaSiteURL(ctx, submoduleURL) { redirectLink := submoduleWebLink.CommitWebLink
ctx.RedirectToCurrentSite(submoduleURL) if isViewHomeOnlyContent(ctx) {
} else { ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
} else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
// don't auto-redirect to external URL, to avoid open redirect or phishing // don't auto-redirect to external URL, to avoid open redirect or phishing
ctx.Data["NotFoundPrompt"] = submoduleURL ctx.Data["NotFoundPrompt"] = redirectLink
ctx.NotFound(nil) ctx.NotFound(nil)
} else {
ctx.Redirect(submoduleWebLink.CommitWebLink)
} }
} }
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) { return func(ctx *context.Context) {
if entry.IsSubModule() { if entry.IsSubModule() {
submodule, err := ctx.Repo.Commit.GetSubModule(entry.Name()) commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
if err != nil { if err != nil {
HandleGitError(ctx, "prepareToRenderDirOrFile: GetSubModule", err) HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
return return
} }
handleRepoViewSubmodule(ctx, submodule) handleRepoViewSubmodule(ctx, commitSubmoduleFile)
return } else if entry.IsDir() {
}
if entry.IsDir() {
prepareToRenderDirectory(ctx) prepareToRenderDirectory(ctx)
} else { } else {
prepareToRenderFile(ctx, entry) prepareToRenderFile(ctx, entry)
@@ -472,7 +479,7 @@ func Home(ctx *context.Context) {
} }
} }
if ctx.FormBool("only_content") { if isViewHomeOnlyContent(ctx) {
ctx.HTML(http.StatusOK, tplRepoViewContent) ctx.HTML(http.StatusOK, tplRepoViewContent)
} else if len(treeNames) != 0 { } else if len(treeNames) != 0 {
ctx.HTML(http.StatusOK, tplRepoView) ctx.HTML(http.StatusOK, tplRepoView)

View File

@@ -9,7 +9,6 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
git_module "code.gitea.io/gitea/modules/git" git_module "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -19,14 +18,20 @@ func TestViewHomeSubmoduleRedirect(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule") ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
submodule := &git_module.SubModule{Path: "test-submodule", URL: setting.AppURL + "user2/repo-other.git"} submodule := git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule) handleRepoViewSubmodule(ctx, submodule)
assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Equal(t, "/user2/repo-other", ctx.Resp.Header().Get("Location")) assert.Equal(t, "/user2/repo-other/tree/any-ref-id", ctx.Resp.Header().Get("Location"))
ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule") ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
submodule = &git_module.SubModule{Path: "test-submodule", URL: "https://other/user2/repo-other.git"} submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "https://other/user2/repo-other.git", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule) handleRepoViewSubmodule(ctx, submodule)
// do not auto-redirect for external URLs, to avoid open redirect or phishing // do not auto-redirect for external URLs, to avoid open redirect or phishing
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus()) assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
ctx, respWriter := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule?only_content=true")
submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
handleRepoViewSubmodule(ctx, submodule)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Equal(t, `<a href="/user2/repo-other/tree/any-ref-id">/user2/repo-other/tree/any-ref-id</a>`, respWriter.Body.String())
} }

View File

@@ -6,7 +6,6 @@ package repo
import ( import (
"bytes" "bytes"
gocontext "context"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@@ -666,7 +665,7 @@ func WikiPages(ctx *context.Context) {
} }
allEntries.CustomSort(base.NaturalSortLess) allEntries.CustomSort(base.NaturalSortLess)
entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath) entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
if err != nil { if err != nil {
ctx.ServerError("GetCommitsInfo", err) ctx.ServerError("GetCommitsInfo", err)
return return

View File

@@ -1509,7 +1509,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/commits", func() { m.Group("/commits", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits) m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
m.Get("/list", repo.GetPullCommits) m.Get("/list", repo.GetPullCommits)
m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit) m.Get("/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
}) })
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest) m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
@@ -1518,8 +1518,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest) m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest)
m.Group("/files", func() { m.Group("/files", func() {
m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr) m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit) m.Get("/{shaFrom:[a-f0-9]{7,64}}..{shaTo:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() { m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm) m.Get("/new_comment", repo.RenderNewCodeCommentForm)
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment) m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)

View File

@@ -260,11 +260,6 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) { func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
ctx = withMethod(ctx, "UpdateComment") ctx = withMethod(ctx, "UpdateComment")
if err := c.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
}
if c.Issue.IsPull { if c.Issue.IsPull {
notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited) notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
return return
@@ -275,11 +270,6 @@ func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) { func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
ctx = withMethod(ctx, "DeleteComment") ctx = withMethod(ctx, "DeleteComment")
if err := comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
}
if comment.Issue.IsPull { if comment.Issue.IsPull {
notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted) notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
return return
@@ -288,6 +278,7 @@ func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
} }
func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) { func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
comment.Issue = nil // force issue to be loaded
if err := comment.LoadIssue(ctx); err != nil { if err := comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err) log.Error("LoadIssue: %v", err)
return return

View File

@@ -5,10 +5,12 @@ package agit
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"os" "os"
"strings" "strings"
git_model "code.gitea.io/gitea/models/git"
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"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -17,17 +19,30 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
) )
func parseAgitPushOptionValue(s string) string {
if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
decoded, err := base64.StdEncoding.DecodeString(base64Value)
return util.Iif(err == nil, string(decoded), s)
}
return s
}
// ProcReceive handle proc receive work // ProcReceive handle proc receive work
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush) forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
topicBranch := opts.GitPushOptions["topic"] topicBranch := opts.GitPushOptions["topic"]
title := strings.TrimSpace(opts.GitPushOptions["title"])
description := strings.TrimSpace(opts.GitPushOptions["description"]) // some options are base64-encoded with "{base64}" prefix if they contain new lines
// other agit push options like "issue", "reviewer" and "cc" are not supported
title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
userName := strings.ToLower(opts.UserName) userName := strings.ToLower(opts.UserName)
@@ -199,11 +214,37 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
} }
} }
// Store old commit ID for review staleness checking
oldHeadCommitID := pr.HeadCommitID
pr.HeadCommitID = opts.NewCommitIDs[i] pr.HeadCommitID = opts.NewCommitIDs[i]
if err = pull_service.UpdateRef(ctx, pr); err != nil { if err = pull_service.UpdateRef(ctx, pr); err != nil {
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err) return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
} }
// Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
if oldHeadCommitID != opts.NewCommitIDs[i] {
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
log.Error("MarkReviewsAsStale: %v", err)
}
// Dismiss all approval reviews if protected branch rule item enabled
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
}
if pb != nil && pb.DismissStaleApprovals {
if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
log.Error("DismissApprovalReviews: %v", err)
}
}
// Mark reviews for the new commit as not stale
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
}
pull_service.StartPullRequestCheckImmediately(ctx, pr) pull_service.StartPullRequestCheckImmediately(ctx, pr)
err = pr.LoadIssue(ctx) err = pr.LoadIssue(ctx)
if err != nil { if err != nil {

View File

@@ -0,0 +1,16 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package agit
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseAgitPushOptionValue(t *testing.T) {
assert.Equal(t, "a", parseAgitPushOptionValue("a"))
assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ=="))
assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value"))
}

View File

@@ -22,23 +22,21 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
) )
// prAutoMergeQueue represents a queue to handle update pull request tests
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
// Init runs the task queue to that handles auto merges // Init runs the task queue to that handles auto merges
func Init() error { func Init() error {
notify_service.RegisterNotifier(NewNotifier()) notify_service.RegisterNotifier(NewNotifier())
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler) automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if prAutoMergeQueue == nil { if automergequeue.AutoMergeQueue == nil {
return errors.New("unable to create pr_auto_merge queue") return errors.New("unable to create pr_auto_merge queue")
} }
go graceful.GetManager().RunWithCancel(prAutoMergeQueue) go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
return nil return nil
} }
@@ -56,24 +54,23 @@ func handler(items ...string) []string {
return nil return nil
} }
func addToQueue(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly // ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) { func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
err = db.WithTx(ctx, func(ctx context.Context) error { err = db.WithTx(ctx, func(ctx context.Context) error {
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil { if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
return err return err
} }
scheduled = true
_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer) _, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
return err return err
}) })
// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
// If the transaction rolls back, then the pull request is not scheduled to auto merge.
// So we should only set "scheduled" to true if there is no error.
scheduled = err == nil
if scheduled {
log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
}
return scheduled, err return scheduled, err
} }
@@ -99,38 +96,12 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
} }
for _, pr := range pulls { for _, pr := range pulls {
addToQueue(pr, sha) automergequeue.AddToQueue(pr, sha)
} }
return nil return nil
} }
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
addToQueue(pull, commitID)
}
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) { func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo) gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil { if err != nil {

View File

@@ -12,6 +12,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
) )
@@ -45,7 +46,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
return return
} }
// as reviews could have blocked a pending automerge let's recheck // as reviews could have blocked a pending automerge let's recheck
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest) automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
} }
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {

View File

@@ -0,0 +1,49 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package automergequeue
import (
"context"
"fmt"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue"
)
var AutoMergeQueue *queue.WorkerPoolQueue[string]
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
if err := pull.LoadBaseRepo(ctx); err != nil {
log.Error("LoadBaseRepo: %v", err)
return
}
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
if err != nil {
log.Error("OpenRepository: %v", err)
return
}
defer gitRepo.Close()
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
log.Error("GetRefCommitID: %v", err)
return
}
AddToQueue(pull, commitID)
}

View File

@@ -33,8 +33,8 @@ func (p *Pagination) WithCurRows(n int) *Pagination {
return p return p
} }
func (p *Pagination) AddParamFromRequest(req *http.Request) { func (p *Pagination) AddParamFromQuery(q url.Values) {
for key, values := range req.URL.Query() { for key, values := range q {
if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") {
continue continue
} }
@@ -45,6 +45,10 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
} }
} }
func (p *Pagination) AddParamFromRequest(req *http.Request) {
p.AddParamFromQuery(req.URL.Query())
}
// 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, "&"))

View File

@@ -28,7 +28,6 @@ func init() {
}) })
} }
// Deadline is part of the interface for context.Context and we pass this to the request context
func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
if ctx.Override != nil { if ctx.Override != nil {
return ctx.Override.Deadline() return ctx.Override.Deadline()
@@ -36,7 +35,6 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
return ctx.Base.Deadline() return ctx.Base.Deadline()
} }
// Done is part of the interface for context.Context and we pass this to the request context
func (ctx *PrivateContext) Done() <-chan struct{} { func (ctx *PrivateContext) Done() <-chan struct{} {
if ctx.Override != nil { if ctx.Override != nil {
return ctx.Override.Done() return ctx.Override.Done()
@@ -44,7 +42,6 @@ func (ctx *PrivateContext) Done() <-chan struct{} {
return ctx.Base.Done() return ctx.Base.Done()
} }
// Err is part of the interface for context.Context and we pass this to the request context
func (ctx *PrivateContext) Err() error { func (ctx *PrivateContext) Err() error {
if ctx.Override != nil { if ctx.Override != nil {
return ctx.Override.Err() return ctx.Override.Err()
@@ -52,14 +49,14 @@ func (ctx *PrivateContext) Err() error {
return ctx.Base.Err() return ctx.Base.Err()
} }
var privateContextKey any = "default_private_context" type privateContextKeyType struct{}
var privateContextKey privateContextKeyType
// GetPrivateContext returns a context for Private routes
func GetPrivateContext(req *http.Request) *PrivateContext { func GetPrivateContext(req *http.Request) *PrivateContext {
return req.Context().Value(privateContextKey).(*PrivateContext) return req.Context().Value(privateContextKey).(*PrivateContext)
} }
// PrivateContexter returns apicontext as middleware
func PrivateContexter() func(http.Handler) http.Handler { func PrivateContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

View File

@@ -143,7 +143,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs) mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs) approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil { if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
} }
@@ -485,7 +485,7 @@ func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs) whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil { if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
} }

View File

@@ -245,7 +245,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
RepoTransfer: transfer, RepoTransfer: transfer,
Topics: util.SliceNilAsEmpty(repo.Topics), Topics: util.SliceNilAsEmpty(repo.Topics),
ObjectFormatName: repo.ObjectFormatName, ObjectFormatName: repo.ObjectFormatName,
Licenses: repoLicenses.StringList(), Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()),
} }
} }

View File

@@ -171,19 +171,20 @@ func registerDeleteOldSystemNotices() {
}) })
} }
func registerGCLFS() {
if !setting.LFS.StartServer {
return
}
type GCLFSConfig struct { type GCLFSConfig struct {
OlderThanConfig BaseConfig
OlderThan time.Duration
LastUpdatedMoreThanAgo time.Duration LastUpdatedMoreThanAgo time.Duration
NumberToCheckPerRepo int64 NumberToCheckPerRepo int64
ProportionToCheckPerRepo float64 ProportionToCheckPerRepo float64
} }
func registerGCLFS() {
if !setting.LFS.StartServer {
return
}
RegisterTaskFatal("gc_lfs", &GCLFSConfig{ RegisterTaskFatal("gc_lfs", &GCLFSConfig{
OlderThanConfig: OlderThanConfig{
BaseConfig: BaseConfig{ BaseConfig: BaseConfig{
Enabled: false, Enabled: false,
RunAtStart: false, RunAtStart: false,
@@ -198,7 +199,7 @@ func registerGCLFS() {
// It is likely that a week is potentially excessive but it should definitely be enough that any // It is likely that a week is potentially excessive but it should definitely be enough that any
// unassociated LFS object is genuinely unassociated. // unassociated LFS object is genuinely unassociated.
OlderThan: 24 * time.Hour * 7, OlderThan: 24 * time.Hour * 7,
},
// Only GC things that haven't been looked at in the past 3 days // Only GC things that haven't been looked at in the past 3 days
LastUpdatedMoreThanAgo: 24 * time.Hour * 3, LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100, NumberToCheckPerRepo: 100,

View File

@@ -0,0 +1,51 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cron
import (
"testing"
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func Test_GCLFSConfig(t *testing.T) {
cfg, err := setting.NewConfigProviderFromData(`
[cron.gc_lfs]
ENABLED = true
RUN_AT_START = true
SCHEDULE = "@every 2h"
OLDER_THAN = "1h"
LAST_UPDATED_MORE_THAN_AGO = "7h"
NUMBER_TO_CHECK_PER_REPO = 10
PROPORTION_TO_CHECK_PER_REPO = 0.1
`)
assert.NoError(t, err)
defer test.MockVariableValue(&setting.CfgProvider, cfg)()
config := &GCLFSConfig{
BaseConfig: BaseConfig{
Enabled: false,
RunAtStart: false,
Schedule: "@every 24h",
},
OlderThan: 24 * time.Hour * 7,
LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100,
ProportionToCheckPerRepo: 0.6,
}
_, err = setting.GetCronSettings("gc_lfs", config)
assert.NoError(t, err)
assert.True(t, config.Enabled)
assert.True(t, config.RunAtStart)
assert.Equal(t, "@every 2h", config.Schedule)
assert.Equal(t, 1*time.Hour, config.OlderThan)
assert.Equal(t, 7*time.Hour, config.LastUpdatedMoreThanAgo)
assert.Equal(t, int64(10), config.NumberToCheckPerRepo)
assert.InDelta(t, 0.1, config.ProportionToCheckPerRepo, 0.001)
}

View File

@@ -179,7 +179,7 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
} }
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line) leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line)
return &DiffLineSectionInfo{ return &DiffLineSectionInfo{
Path: treePath, Path: treePath,
@@ -188,7 +188,7 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int
LeftIdx: leftLine, LeftIdx: leftLine,
RightIdx: rightLine, RightIdx: rightLine,
LeftHunkSize: leftHunk, LeftHunkSize: leftHunk,
RightHunkSize: righHunk, RightHunkSize: rightHunk,
} }
} }
@@ -335,7 +335,7 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
// try to find equivalent diff line. ignore, otherwise // try to find equivalent diff line. ignore, otherwise
switch diffLine.Type { switch diffLine.Type {
case DiffLineSection: case DiffLineSection:
return getLineContent(diffLine.Content[1:], locale) return getLineContent(diffLine.Content, locale)
case DiffLineAdd: case DiffLineAdd:
compareDiffLine := diffSection.GetLine(diffLine.Match) compareDiffLine := diffSection.GetLine(diffLine.Match)
return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale) return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale)
@@ -904,6 +904,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
lastLeftIdx = -1 lastLeftIdx = -1
curFile.Sections = append(curFile.Sections, curSection) curFile.Sections = append(curFile.Sections, curSection)
// FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug.
lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1) lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
diffLine := &DiffLine{ diffLine := &DiffLine{
Type: DiffLineSection, Type: DiffLineSection,
@@ -1232,13 +1233,13 @@ func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio
return diff, err return diff, err
} }
func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...) diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage}) checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1247,6 +1248,7 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
for _, diffFile := range diff.Files { for _, diffFile := range diff.Files {
isVendored := optional.None[bool]() isVendored := optional.None[bool]()
isGenerated := optional.None[bool]() isGenerated := optional.None[bool]()
attrDiff := optional.None[string]()
attrs, err := checker.CheckPath(diffFile.Name) attrs, err := checker.CheckPath(diffFile.Name)
if err == nil { if err == nil {
isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated() isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated()
@@ -1254,11 +1256,12 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
if language.Has() { if language.Has() {
diffFile.Language = language.Value() diffFile.Language = language.Value()
} }
attrDiff = attrs.Get(attribute.Diff).ToString()
} }
// Populate Submodule URLs // Populate Submodule URLs
if diffFile.SubmoduleDiffInfo != nil { if diffFile.SubmoduleDiffInfo != nil {
diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit) diffFile.SubmoduleDiffInfo.PopulateURL(repoLink, diffFile, beforeCommit, afterCommit)
} }
if !isVendored.Has() { if !isVendored.Has() {
@@ -1275,7 +1278,8 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
diffFile.Sections = append(diffFile.Sections, tailSection) diffFile.Sections = append(diffFile.Sections, tailSection)
} }
if !setting.Git.DisableDiffHighlight { shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
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.String())
} }
@@ -1359,6 +1363,7 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.
// But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible
if err != nil { if err != nil {
log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err)
err = nil //nolint
} }
filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState)
@@ -1400,7 +1405,7 @@ outer:
} }
} }
return review, err return review, nil
} }
// CommentAsDiff returns c.Patch as *Diff // CommentAsDiff returns c.Patch as *Diff

View File

@@ -20,7 +20,7 @@ type SubmoduleDiffInfo struct {
PreviousRefID string PreviousRefID string
} }
func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) { func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
si.SubmoduleName = diffFile.Name si.SubmoduleName = diffFile.Name
submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit
if diffFile.IsDeleted { if diffFile.IsDeleted {
@@ -30,18 +30,19 @@ func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCo
return return
} }
submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName()) submoduleFullPath := diffFile.GetDiffFileName()
submodule, err := submoduleCommit.GetSubModule(submoduleFullPath)
if err != nil { if err != nil {
log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err) log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", submoduleFullPath, err)
return // ignore the error, do not cause 500 errors for end users return // ignore the error, do not cause 500 errors for end users
} }
if submodule != nil { if submodule != nil {
si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String()) si.SubmoduleFile = git.NewCommitSubmoduleFile(repoLink, submoduleFullPath, submodule.URL, submoduleCommit.ID.String())
} }
} }
func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML { func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID) webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx, commitID)
if webLink == nil { if webLink == nil {
return htmlutil.HTMLFormat("%s", base.ShortSha(commitID)) return htmlutil.HTMLFormat("%s", base.ShortSha(commitID))
} }
@@ -49,7 +50,7 @@ func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID s
} }
func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML { func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID) webLink := si.SubmoduleFile.SubmoduleWebLinkCompare(ctx, si.PreviousRefID, si.NewRefID)
if webLink == nil { if webLink == nil {
return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID)) return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID))
} }
@@ -57,7 +58,7 @@ func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.
} }
func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML { func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML {
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx) webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx)
if webLink == nil { if webLink == nil {
return htmlutil.HTMLFormat("%s", si.SubmoduleName) return htmlutil.HTMLFormat("%s", si.SubmoduleName)
} }

View File

@@ -228,7 +228,7 @@ func TestSubmoduleInfo(t *testing.T) {
assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx)) assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx)) assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx))
sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234") sdi.SubmoduleFile = git.NewCommitSubmoduleFile("/any/repo-link", "fullpath", "https://github.com/owner/repo", "1234")
assert.EqualValues(t, `<a href="https://github.com/owner/repo/tree/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111")) assert.EqualValues(t, `<a href="https://github.com/owner/repo/tree/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx)) assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx))
assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx)) assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx))

View File

@@ -304,7 +304,7 @@ func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, rep
// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
if repo.Owner.IsOrganization() { if repo.Owner.IsOrganization() {
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
if err != nil { if err != nil {
log.Error("GetTeamsWithAccessToRepo: %v", err) log.Error("GetTeamsWithAccessToRepo: %v", err)
return false return false

View File

@@ -80,6 +80,12 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
return nil, err return nil, err
} }
// reload issue to ensure it has the latest data, especially the number of comments
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, err
}
notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions) notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
return comment, nil return comment, nil

View File

@@ -322,7 +322,10 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith
httpClient := NewMigrationHTTPClient() httpClient := NewMigrationHTTPClient()
for _, asset := range rel.Assets { for _, asset := range rel.Assets {
assetID := *asset.ID // Don't optimize this, for closure we need a local variable assetID := asset.GetID() // Don't optimize this, for closure we need a local variable TODO: no need to do so in new Golang
if assetID == 0 {
continue
}
r.Assets = append(r.Assets, &base.ReleaseAsset{ r.Assets = append(r.Assets, &base.ReleaseAsset{
ID: asset.GetID(), ID: asset.GetID(),
Name: asset.GetName(), Name: asset.GetName(),

View File

@@ -46,10 +46,25 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
} }
} }
func shouldSendCommentChangeNotification(ctx context.Context, comment *issues_model.Comment) bool {
if err := comment.LoadReview(ctx); err != nil {
log.Error("LoadReview: %v", err)
return false
} else if comment.Review != nil && comment.Review.Type == issues_model.ReviewTypePending {
// Pending review comments updating should not triggered
return false
}
return true
}
// CreateIssueComment notifies issue comment related message to notifiers // CreateIssueComment notifies issue comment related message to notifiers
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User, issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
) { ) {
if !shouldSendCommentChangeNotification(ctx, comment) {
return
}
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions) notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
} }
@@ -156,6 +171,10 @@ func PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issue
// UpdateComment notifies update comment to notifiers // UpdateComment notifies update comment to notifiers
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) { func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
if !shouldSendCommentChangeNotification(ctx, c) {
return
}
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.UpdateComment(ctx, doer, c, oldContent) notifier.UpdateComment(ctx, doer, c, oldContent)
} }
@@ -163,6 +182,10 @@ func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.C
// DeleteComment notifies delete comment to notifiers // DeleteComment notifies delete comment to notifiers
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) { func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
if !shouldSendCommentChangeNotification(ctx, c) {
return
}
for _, notifier := range notifiers { for _, notifier := range notifiers {
notifier.DeleteComment(ctx, doer, c) notifier.DeleteComment(ctx, doer, c)
} }

View File

@@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors. // Copyright 2019 The Gitea Authors. All rights reserved.
// All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package pull package pull
@@ -16,6 +15,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
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" access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/pull"
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"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automergequeue"
notify_service "code.gitea.io/gitea/services/notify" notify_service "code.gitea.io/gitea/services/notify"
) )
@@ -238,7 +239,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status, // markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
// and set to be either conflict or mergeable. // and set to be either conflict or mergeable.
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) { func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable // If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
if pr.Status == issues_model.PullRequestStatusChecking { if pr.Status == issues_model.PullRequestStatusChecking {
pr.Status = issues_model.PullRequestStatusMergeable pr.Status = issues_model.PullRequestStatusMergeable
} }
@@ -257,6 +258,16 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil { if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
log.Error("Update[%-v]: %v", pr, err) log.Error("Update[%-v]: %v", pr, err)
} }
// if there is a scheduled merge for this pull request, start the auto merge check (again)
exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
if err != nil {
log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
return
} else if !exist {
return
}
automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
} }
// getMergeCommit checks if a pull request has been merged // getMergeCommit checks if a pull request has been merged

View File

@@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors. // Copyright 2019 The Gitea Authors. All rights reserved.
// All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package pull package pull
@@ -11,11 +10,18 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/pull"
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/graceful"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/automergequeue"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestPullRequest_AddToTaskQueue(t *testing.T) { func TestPullRequest_AddToTaskQueue(t *testing.T) {
@@ -63,6 +69,46 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status) assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
prPatchCheckerQueue.ShutdownWait(5 * time.Second) prPatchCheckerQueue.ShutdownWait(time.Second)
prPatchCheckerQueue = nil prPatchCheckerQueue = nil
} }
func TestMarkPullRequestAsMergeable(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string { return nil })
go prPatchCheckerQueue.Run()
defer func() {
prPatchCheckerQueue.ShutdownWait(time.Second)
prPatchCheckerQueue = nil
}()
addToQueueShaChan := make(chan string, 1)
defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
addToQueueShaChan <- sha
})()
ctx := t.Context()
_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.False(t, pr.HasMerged)
require.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
err := pull.ScheduleAutoMerge(ctx, &user_model.User{ID: 99999}, pr.ID, repo_model.MergeStyleMerge, "test msg", true)
require.NoError(t, err)
exist, scheduleMerge, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
require.NoError(t, err)
assert.True(t, exist)
assert.True(t, scheduleMerge.Doer.IsGhost())
markPullRequestAsMergeable(ctx, pr)
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
select {
case sha := <-addToQueueShaChan:
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
case <-time.After(1 * time.Second):
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
}
}

View File

@@ -35,15 +35,16 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
} }
for _, gp := range requiredContextsGlob { for _, gp := range requiredContextsGlob {
var targetStatus structs.CommitStatusState var targetStatuses []*git_model.CommitStatus
for _, commitStatus := range commitStatuses { for _, commitStatus := range commitStatuses {
if gp.Match(commitStatus.Context) { if gp.Match(commitStatus.Context) {
targetStatus = commitStatus.State targetStatuses = append(targetStatuses, commitStatus)
matchedCount++ matchedCount++
break
} }
} }
targetStatus := git_model.CalcCommitStatus(targetStatuses).State
// If required rule not match any action, then it is pending // If required rule not match any action, then it is pending
if targetStatus == "" { if targetStatus == "" {
if structs.CommitStatusPending.NoBetterThan(returnedStatus) { if structs.CommitStatusPending.NoBetterThan(returnedStatus) {

View File

@@ -30,6 +30,11 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
{Context: "Build 2", State: structs.CommitStatusSuccess}, {Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusFailure}, {Context: "Build 2t", State: structs.CommitStatusFailure},
}, },
{
{Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess},
{Context: "Build 2t", State: structs.CommitStatusFailure},
},
{ {
{Context: "Build 1", State: structs.CommitStatusSuccess}, {Context: "Build 1", State: structs.CommitStatusSuccess},
{Context: "Build 2", State: structs.CommitStatusSuccess}, {Context: "Build 2", State: structs.CommitStatusSuccess},
@@ -45,6 +50,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
{"Build*"}, {"Build*"},
{"Build*", "Build 2t*"}, {"Build*", "Build 2t*"},
{"Build*", "Build 2t*"}, {"Build*", "Build 2t*"},
{"Build*"},
{"Build*", "Build 2t*", "Build 3*"}, {"Build*", "Build 2t*", "Build 3*"},
{"Build*", "Build *", "Build 2t*", "Build 1*"}, {"Build*", "Build *", "Build 2t*", "Build 1*"},
} }
@@ -53,6 +59,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
structs.CommitStatusSuccess, structs.CommitStatusSuccess,
structs.CommitStatusPending, structs.CommitStatusPending,
structs.CommitStatusFailure, structs.CommitStatusFailure,
structs.CommitStatusFailure,
structs.CommitStatusPending, structs.CommitStatusPending,
structs.CommitStatusSuccess, structs.CommitStatusSuccess,
} }

View File

@@ -85,5 +85,5 @@ func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*orga
return nil, nil return nil, nil
} }
return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) return organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
} }

View File

@@ -90,15 +90,8 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if rangeStart >= len(entries) { if rangeStart >= len(entries) {
return tree, nil return tree, nil
} }
var rangeEnd int rangeEnd := min(rangeStart+perPage, len(entries))
if len(entries) > perPage { tree.Truncated = rangeEnd < len(entries)
tree.Truncated = true
}
if rangeStart+perPage < len(entries) {
rangeEnd = rangeStart + perPage
} else {
rangeEnd = len(entries)
}
tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart) tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
for e := rangeStart; e < rangeEnd; e++ { for e := rangeStart; e < rangeEnd; e++ {
i := e - rangeStart i := e - rangeStart
@@ -158,37 +151,31 @@ func (node *TreeViewNode) sortLevel() int {
return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1) return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
} }
func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode { func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
node := &TreeViewNode{ node := &TreeViewNode{
EntryName: entry.Name(), EntryName: entry.Name(),
EntryMode: entryModeString(entry.Mode()), EntryMode: entryModeString(entry.Mode()),
FullPath: path.Join(parentDir, entry.Name()), FullPath: path.Join(parentDir, entry.Name()),
} }
if entry.IsLink() { entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
// TODO: symlink to a folder or a file, the icon differs node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
target, err := entry.FollowLink() if entryInfo.EntryMode.IsDir() {
if err == nil { entryInfo.IsOpen = true
_ = target.IsDir() node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
// if target.IsDir() { } else { }
}
}
if node.EntryIcon == "" {
node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry)
// TODO: no open icon support yet
// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry)
} }
if node.EntryMode == "commit" { if node.EntryMode == "commit" {
if subModule, err := commit.GetSubModule(node.FullPath); err != nil { if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
log.Error("GetSubModule: %v", err) log.Error("GetSubModule: %v", err)
} else if subModule != nil { } else if subModule != nil {
submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String()) submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String())
webLink := submoduleFile.SubmoduleWebLink(ctx) webLink := submoduleFile.SubmoduleWebLinkTree(ctx)
if webLink != nil {
node.SubmoduleURL = webLink.CommitWebLink node.SubmoduleURL = webLink.CommitWebLink
} }
} }
}
return node return node
} }
@@ -204,7 +191,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
}) })
} }
func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) { func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
entries, err := tree.ListEntries() entries, err := tree.ListEntries()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -213,14 +200,14 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/") subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
nodes := make([]*TreeViewNode, 0, len(entries)) nodes := make([]*TreeViewNode, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry) node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry)
nodes = append(nodes, node) nodes = append(nodes, node)
if entry.IsDir() && subPathDirName == entry.Name() { if entry.IsDir() && subPathDirName == entry.Name() {
subTreePath := treePath + "/" + node.EntryName subTreePath := treePath + "/" + node.EntryName
if subTreePath[0] == '/' { if subTreePath[0] == '/' {
subTreePath = subTreePath[1:] subTreePath = subTreePath[1:]
} }
subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining) subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
if err != nil { if err != nil {
log.Error("listTreeNodes: %v", err) log.Error("listTreeNodes: %v", err)
} else { } else {
@@ -232,10 +219,10 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
return nodes, nil return nodes, nil
} }
func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) { func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
entry, err := commit.GetTreeEntryByPath(treePath) entry, err := commit.GetTreeEntryByPath(treePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath) return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath)
} }

View File

@@ -64,6 +64,7 @@ func TestGetTreeViewNodes(t *testing.T) {
contexttest.LoadGitRepo(t, ctx) contexttest.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close() defer ctx.Repo.GitRepo.Close()
curRepoLink := "/any/repo-link"
renderedIconPool := fileicon.NewRenderedIconPool() renderedIconPool := fileicon.NewRenderedIconPool()
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 xlink:href="#` + id + `"></use></svg>`)
@@ -71,7 +72,10 @@ func TestGetTreeViewNodes(t *testing.T) {
mockIconForFolder := func(id string) template.HTML { mockIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`) return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
} }
treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "") mockOpenIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
}
treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
@@ -79,10 +83,11 @@ func TestGetTreeViewNodes(t *testing.T) {
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
}, },
}, treeNodes) }, treeNodes)
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md") treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {
@@ -90,6 +95,7 @@ func TestGetTreeViewNodes(t *testing.T) {
EntryMode: "tree", EntryMode: "tree",
FullPath: "docs", FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
Children: []*TreeViewNode{ Children: []*TreeViewNode{
{ {
EntryName: "README.md", EntryName: "README.md",
@@ -101,7 +107,7 @@ func TestGetTreeViewNodes(t *testing.T) {
}, },
}, treeNodes) }, treeNodes)
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md") treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*TreeViewNode{ assert.Equal(t, []*TreeViewNode{
{ {

View File

@@ -402,16 +402,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
} }
rel, has := relMap[lowerTag] rel, has := relMap[lowerTag]
title, note := git.SplitCommitTitleBody(tag.Message, 255)
parts := strings.SplitN(tag.Message, "\n", 2)
note := ""
if len(parts) > 1 {
note = parts[1]
}
if !has { if !has {
rel = &repo_model.Release{ rel = &repo_model.Release{
RepoID: repo.ID, RepoID: repo.ID,
Title: parts[0], Title: title,
TagName: tags[i], TagName: tags[i],
LowerTagName: lowerTag, LowerTagName: lowerTag,
Target: "", Target: "",
@@ -430,7 +425,7 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
rel.Sha1 = commit.ID.String() rel.Sha1 = commit.ID.String()
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
if rel.IsTag { if rel.IsTag {
rel.Title = parts[0] rel.Title = title
rel.Note = note rel.Note = note
} else { } else {
rel.IsDraft = false rel.IsDraft = false

View File

@@ -445,6 +445,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
log.Error("LoadPoster: %v", err) log.Error("LoadPoster: %v", err)
return return
} }
comment.Issue = nil // reload issue to ensure it has the latest data, especially the number of comments
if err = comment.LoadIssue(ctx); err != nil { if err = comment.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err) log.Error("LoadIssue: %v", err)
return return

View File

@@ -16,7 +16,7 @@
<td class="author"> <td class="author">
<div class="tw-flex"> <div class="tw-flex">
{{$userName := .Author.Name}} {{$userName := .Author.Name}}
{{if .User}} {{if and .User (gt .User.ID 0)}}
{{if and .User.FullName DefaultShowFullName}} {{if and .User.FullName DefaultShowFullName}}
{{$userName = .User.FullName}} {{$userName = .User.FullName}}
{{end}} {{end}}

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