From e98dd6ee5b0efb8e7a9f992fa35903cf5b2c1634 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 Dec 2024 11:58:27 +0800
Subject: [PATCH 001/206] Backport 1.23 (#32868)

Co-authored-by: delvh <dev.lh@web.de>
---
 models/actions/run_job_status_test.go         |   2 +-
 modules/markup/markdown/markdown.go           |   3 +-
 modules/repository/fork.go                    |   3 +
 options/locale/locale_fr-FR.ini               |   1 -
 options/locale/locale_ga-IE.ini               |   1 -
 options/locale/locale_pt-PT.ini               |   2 +-
 options/locale/locale_zh-CN.ini               | 171 +++++++++++++++++-
 routers/web/repo/actions/view.go              |   3 +
 .../auth/source/ldap/source_authenticate.go   |   6 +
 templates/package/content/container.tmpl      |   3 +-
 templates/repo/commit_page.tmpl               |   4 +-
 templates/repo/issue/filter_item_label.tmpl   |   3 +
 templates/user/auth/signin_inner.tmpl         |   1 +
 templates/user/auth/signup_inner.tmpl         |   2 +
 templates/user/dashboard/issues.tmpl          |   2 +
 .../js/components/RepoBranchTagSelector.vue   |  10 +
 16 files changed, 209 insertions(+), 8 deletions(-)

diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go
index 04fd9ceba7b72..523d38327e4cc 100644
--- a/models/actions/run_job_status_test.go
+++ b/models/actions/run_job_status_test.go
@@ -69,7 +69,7 @@ func TestAggregateJobStatus(t *testing.T) {
 		{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
 
 		// skipped with other status
-		// TODO: need to clarify whether a PR with "skipped" job status is considered as "mergeable" or not.
+		// "all skipped" is also considered as "mergeable" by "services/actions.toCommitStatus", the same as GitHub
 		{[]Status{StatusSkipped}, StatusSkipped},
 		{[]Status{StatusSkipped, StatusSuccess}, StatusSuccess},
 		{[]Status{StatusSkipped, StatusFailure}, StatusFailure},
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index a14c0cad597dc..b5fffccdb989a 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -129,7 +129,8 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
 				Enabled:           setting.Markdown.EnableMath,
 				ParseDollarInline: true,
 				ParseDollarBlock:  true,
-				ParseSquareBlock:  true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options)
+				ParseSquareBlock:  true, // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options)
+				// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future
 			}),
 			meta.Meta,
 		),
diff --git a/modules/repository/fork.go b/modules/repository/fork.go
index d53063407160f..84ed4b23d6719 100644
--- a/modules/repository/fork.go
+++ b/modules/repository/fork.go
@@ -12,6 +12,9 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 )
 
+// CanUserForkBetweenOwners returns true if user can fork between owners.
+// By default, a user can fork a repository from another owner, but not from themselves.
+// Many users really like to fork their own repositories, so add an experimental setting to allow this.
 func CanUserForkBetweenOwners(id1, id2 int64) bool {
 	if id1 != id2 {
 		return true
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 4d4940cff5af1..0870983c98621 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1676,7 +1676,6 @@ issues.timetracker_timer_stop=Arrêter le minuteur
 issues.timetracker_timer_discard=Annuler le minuteur
 issues.timetracker_timer_manually_add=Pointer du temps
 
-issues.time_estimate_placeholder=1h 2m
 issues.time_estimate_set=Définir le temps estimé
 issues.time_estimate_display=Estimation : %s
 issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s
diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini
index f40e0037d2c55..0b74776c17419 100644
--- a/options/locale/locale_ga-IE.ini
+++ b/options/locale/locale_ga-IE.ini
@@ -1676,7 +1676,6 @@ issues.timetracker_timer_stop=Stop an t-amadóir
 issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh
 issues.timetracker_timer_manually_add=Cuir Am leis
 
-issues.time_estimate_placeholder=1u 2n
 issues.time_estimate_set=Socraigh am measta
 issues.time_estimate_display=Meastachán: %s
 issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 776d2bdc2bf2c..b959e7fdba259 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -1679,7 +1679,6 @@ issues.timetracker_timer_stop=Parar cronómetro
 issues.timetracker_timer_discard=Descartar cronómetro
 issues.timetracker_timer_manually_add=Adicionar tempo
 
-issues.time_estimate_placeholder=1h 2m
 issues.time_estimate_set=Definir tempo estimado
 issues.time_estimate_display=Estimativa: %s
 issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s
@@ -2632,6 +2631,7 @@ release.new_release=Novo lançamento
 release.draft=Rascunho
 release.prerelease=Pré-lançamento
 release.stable=Estável
+release.latest=Mais recente
 release.compare=Comparar
 release.edit=editar
 release.ahead.commits=<strong>%d</strong> cometimentos
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 3add7f8be38cf..8c6392a4dc995 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -93,6 +93,7 @@ remove_all=移除所有
 remove_label_str=`删除标签 "%s"`
 edit=编辑
 view=查看
+test=测试
 
 enabled=启用
 disabled=禁用
@@ -103,6 +104,7 @@ copy_url=复制网址
 copy_hash=复制哈希值
 copy_content=复制内容
 copy_branch=复制分支名
+copy_path=复制路径
 copy_success=复制成功！
 copy_error=复制失败
 copy_type_unsupported=无法复制此类型的文件内容
@@ -143,6 +145,7 @@ confirm_delete_selected=确认删除所有选中项目？
 
 name=名称
 value=值
+readme=自述文档
 
 filter=过滤
 filter.clear=清除筛选器
@@ -178,6 +181,7 @@ package_kind=搜索软件包...
 project_kind=搜索项目...
 branch_kind=搜索分支...
 tag_kind=搜索标签...
+tag_tooltip=搜索匹配的标签。使用“%”来匹配任何序列的数字
 commit_kind=搜索提交记录...
 runner_kind=搜索runners...
 no_results=未找到匹配结果
@@ -223,16 +227,20 @@ string.desc=Z - A
 
 [error]
 occurred=发生了一个错误
+report_message=如果您确定这是一个 Gitea bug，请在 <a href="%s" target="_blank">这里</a> 搜索问题，或在必要时创建一个新工单。
 not_found=找不到目标。
 network_error=网络错误
 
 [startpage]
 app_desc=一款极易搭建的自助 Git 服务
 install=易安装
+install_desc=通过 <a target="_blank" rel="noopener noreferrer" href="%[1]s">二进制</a> 来运行；或者通过 <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a> 来运行；或者通过 <a target="_blank" rel="noopener noreferrer" href="%[3]s">安装包</a> 来运行。
 platform=跨平台
+platform_desc=任何 <a target="_blank" rel="noopener noreferrer" href="%s">Go 语言</a> 支持的平台都可以运行 Gitea，包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行！
 lightweight=轻量级
 lightweight_desc=一个廉价的树莓派的配置足以满足 Gitea 的最低系统硬件要求。最大程度上节省您的服务器资源！
 license=开源化
+license_desc=所有的代码都开源在 <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[2]s</a> 上，赶快加入我们来<a target="_blank" rel="noopener noreferrer" href="%[3]s">共同发展</a>这个伟大的项目！还等什么？成为贡献者吧！
 
 [install]
 install=安装页面
@@ -346,6 +354,7 @@ enable_update_checker=启用更新检查
 enable_update_checker_helper=通过连接到 gitea.io 定期检查新版本发布。
 env_config_keys=环境配置
 env_config_keys_prompt=以下环境变量也将应用于您的配置文件：
+config_write_file_prompt=这些配置选项将写入以下位置: %s
 
 [home]
 nav_menu=导航菜单
@@ -386,6 +395,7 @@ relevant_repositories=只显示相关的仓库， <a href="%s">显示未过滤
 
 [auth]
 create_new_account=注册帐号
+already_have_account=已有账号？
 sign_in_now=立即登录
 disable_register_prompt=对不起，注册功能已被关闭。请联系网站管理员。
 disable_register_mail=已禁用注册的电子邮件确认。
@@ -394,6 +404,7 @@ remember_me=记住此设备
 remember_me.compromised=登录令牌不再有效，因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
 forgot_password_title=忘记密码
 forgot_password=忘记密码？
+need_account=需要一个帐户?
 sign_up_now=还没账号？马上注册。
 sign_up_successful=帐户创建成功。欢迎！
 confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 <b>%s</b>请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确，您可以重新登录并更改它。
@@ -449,12 +460,15 @@ authorize_application=应用授权
 authorize_redirect_notice=如果您授权此应用，您将会被重定向到 %s。
 authorize_application_created_by=此应用由%s创建。
 authorize_application_description=如果您允许，它将能够读取和修改您的所有帐户信息，包括私人仓库和组织。
+authorize_application_with_scopes=范围: %s
 authorize_title=授权 %s 访问您的帐户？
 authorization_failed=授权失败
 authorization_failed_desc=因为检测到无效请求，授权失败。请尝试联系您授权应用的管理员。
 sspi_auth_failed=SSPI 认证失败
+password_pwned=此密码出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密码</a> 列表上并且曾经被公开。 请使用另一个密码再试一次。
 password_pwned_err=无法完成对 HaveIBeenPwned 的请求
 last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。
+signin_passkey=使用密钥登录
 back_to_sign_in=返回登录页面
 
 [mail]
@@ -574,6 +588,8 @@ lang_select_error=从列表中选出语言
 
 username_been_taken=用户名已被使用。
 username_change_not_local_user=非本地用户不允许更改用户名。
+change_username_disabled=更改用户名已被禁用。
+change_full_name_disabled=更改用户全名已禁用
 username_has_not_been_changed=用户名未更改
 repo_name_been_taken=仓库名称已被使用。
 repository_force_private=“强制私有”已启用：私有仓库不能被公开。
@@ -623,6 +639,7 @@ org_still_own_repo=该组织仍然是某些仓库的拥有者，请先删除或
 org_still_own_packages=该组织仍然是一个或多个软件包的拥有者，请先删除它们。
 
 target_branch_not_exist=目标分支不存在。
+target_ref_not_exist=目标引用 %s 不存在
 
 admin_cannot_delete_self=当您是管理员时，您不能删除自己。请先移除您的管理员权限
 
@@ -688,14 +705,18 @@ applications=应用
 orgs=管理组织
 repos=仓库列表
 delete=删除帐户
+twofa=两步验证（TOTP）
 account_link=已绑定帐户
 organization=组织
 uid=UID
+webauthn=两步验证（安全密钥）
 
 public_profile=公开信息
 biography_placeholder=告诉我们一点您自己！ (您可以使用Markdown)
 location_placeholder=与他人分享你的大概位置
 profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作
+password_username_disabled=不允许非本地用户更改他们的用户名。更多详情请联系您的系统管理员。
+password_full_name_disabled=您无权更改他们的全名。请联系您的站点管理员了解更多详情。
 full_name=自定义名称
 website=个人网站
 location=所在地区
@@ -745,6 +766,7 @@ uploaded_avatar_not_a_image=上传的文件不是一张图片。
 uploaded_avatar_is_too_big=上传的文件大小(%d KiB) 超过最大限制(%d KiB)。
 update_avatar_success=您的头像已更新。
 update_user_avatar_success=用户头像已更新。
+cropper_prompt=您可以在保存前编辑图像。编辑的图像将被保存为 PNG 格式。
 
 change_password=更新密码
 old_password=当前密码
@@ -787,6 +809,7 @@ add_email_success=新的电子邮件地址已添加。
 email_preference_set_success=电子邮件首选项已成功设置。
 add_openid_success=新的 OpenID 地址已添加。
 keep_email_private=隐藏电子邮件地址
+keep_email_private_popup=这将会隐藏您的电子邮件地址，不仅在您的个人资料中，还在您使用Web界面创建合并请求或编辑文件时。已推送的提交将不会被修改。在提交中使用 %s 以和您的账号关联。
 openid_desc=OpenID 让你可以将认证转发到外部服务。
 
 manage_ssh_keys=管理 SSH 密钥
@@ -905,6 +928,7 @@ create_oauth2_application_success=您已成功创建了一个新的 OAuth2 应
 update_oauth2_application_success=您已成功更新了此 OAuth2 应用。
 oauth2_application_name=应用名称
 oauth2_confidential_client=机密客户端。是否是能够维持凭据机密性的应用，比如网页应用程序。如果是本地应用程序请不要勾选，包括桌面和移动端应用。
+oauth2_skip_secondary_authorization=首次授权后允许公共客户端跳过授权步骤。 <strong>可能会带来安全风险。</strong>
 oauth2_redirect_uris=重定向 URI。每行一个 URI。
 save_application=保存
 oauth2_client_id=客户端ID
@@ -915,6 +939,7 @@ oauth2_client_secret_hint=您离开或刷新此页面后将不会再显示此密
 oauth2_application_edit=编辑
 oauth2_application_create_description=OAuth2 应用允许您的第三方应用程序访问此实例的用户帐户。
 oauth2_application_remove_description=移除一个OAuth2应用将会阻止它访问此实例上的已授权用户账户。是否继续？
+oauth2_application_locked=如果配置启用，Gitea预注册一些OAuth2应用程序。 为了防止意外的行为, 这些应用既不能编辑也不能删除。请参阅OAuth2文档以获取更多信息。
 
 authorized_oauth2_applications=已授权的 OAuth2 应用
 authorized_oauth2_applications_description=您已授予这些第三方应用程序访问您的个人 Gitea 账户的权限。请撤销那些您不再需要的应用程序的访问权限。
@@ -923,20 +948,26 @@ revoke_oauth2_grant=撤回权限
 revoke_oauth2_grant_description=确定撤销此三方应用程序的授权，并阻止此应用程序访问您的数据？
 revoke_oauth2_grant_success=成功撤销了访问权限。
 
+twofa_desc=为保护你的账号密码安全，你可以使用智能手机或其它设备来接收时间强相关的一次性密码（TOTP）。
 twofa_recovery_tip=如果您丢失了您的设备，您将能够使用一次性恢复密钥来重新获得对您账户的访问。
 twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。
 twofa_not_enrolled=你的账号未开启两步验证。
 twofa_disable=禁用两步认证
+twofa_scratch_token_regenerate=重新生成初始令牌
+twofa_scratch_token_regenerated=您的初始令牌现在是 %s。将其存放在安全的地方，它将不会再次显示。
 twofa_enroll=启用两步验证
 twofa_disable_note=如果需要, 可以禁用双因素身份验证。
 twofa_disable_desc=关掉两步验证会使得您的账号不安全，继续执行？
+regenerate_scratch_token_desc=如果您丢失了您的恢复密钥或已经使用它登录, 您可以在这里重置它。
 twofa_disabled=两步验证已被禁用。
 scan_this_image=使用您的授权应用扫描这张图片：
 or_enter_secret=或者输入密钥：%s
 then_enter_passcode=并输入应用程序中显示的密码:
 passcode_invalid=密码不正确。再试一次。
+twofa_enrolled=你的账号已经启用了两步验证。请保存初始令牌（%s）到一个安全的地方，此令牌仅显示一次。
 twofa_failed_get_secret=获取 secret 失败。
 
+webauthn_desc=安全密钥是包含加密密钥的硬件设备。它们可以用于双因素身份验证。安全密钥必须支持 <a rel="noreferrer" target="_blank" href="%s">WebAuthn 身份验证器</a> 标准。
 webauthn_register_key=添加安全密钥
 webauthn_nickname=昵称
 webauthn_delete_key=移除安全密钥
@@ -1002,6 +1033,8 @@ fork_to_different_account=派生到其他账号
 fork_visibility_helper=无法更改派生仓库的可见性。
 fork_branch=要克隆到 Fork 的分支
 all_branches=所有分支
+view_all_branches=查看所有分支
+view_all_tags=查看所有标签
 fork_no_valid_owners=这个代码仓库无法被派生，因为没有有效的所有者。
 fork.blocked_user=无法克隆仓库，因为您被仓库所有者屏蔽。
 use_template=使用此模板
@@ -1013,6 +1046,8 @@ generate_repo=生成仓库
 generate_from=生成自
 repo_desc=仓库描述
 repo_desc_helper=输入简要描述 (可选)
+repo_no_desc=无详细信息
+repo_lang=编程语言
 repo_gitignore_helper=选择 .gitignore 模板。
 repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下，由开发或构建工具生成的特殊文件都包含在 .gitignore 中。
 issue_labels=工单标签
@@ -1074,6 +1109,7 @@ delete_preexisting_success=删除 %s 中未收录的文件
 blame_prior=查看此更改前的 blame
 blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点击 <a href="%s">绕过</a> 并查看正常的 Blame 视图。
 blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。
+user_search_tooltip=最多显示30名用户
 
 tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在
 tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。
@@ -1222,6 +1258,7 @@ releases=版本发布
 tag=Git标签
 released_this=发布
 tagged_this=已标记
+file.title=%s 位于 %s
 file_raw=原始文件
 file_history=文件历史
 file_view_source=源码模式
@@ -1238,6 +1275,7 @@ ambiguous_runes_header=`此文件含有模棱两可的 Unicode 字符`
 ambiguous_runes_description=`此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的，可以安全地忽略该警告。 使用 Escape 按钮显示他们。`
 invisible_runes_line=`此行含有不可见的 unicode 字符`
 ambiguous_runes_line=`此行有模棱两可的 unicode 字符`
+ambiguous_character=`%[1]c [U+%04[1]X] 容易和 %[2]c [U+%04[2]X] 混淆`
 
 escape_control_characters=Escape
 unescape_control_characters=Unescape
@@ -1429,6 +1467,7 @@ issues.new.clear_milestone=取消选中里程碑
 issues.new.assignees=指派成员
 issues.new.clear_assignees=取消指派成员
 issues.new.no_assignees=未指派成员
+issues.new.no_reviewers=无审核者
 issues.new.blocked_user=无法创建工单，因为您已被仓库所有者屏蔽。
 issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改
 issues.edit.blocked_user=无法编辑内容，因为您已被仓库所有者或工单创建者屏蔽。
@@ -1457,6 +1496,7 @@ issues.remove_labels=于 %[2]s 删除了标签 %[1]s
 issues.add_remove_labels=于 %[3]s 添加了标签 %[1]s ，删除了标签 %[2]s
 issues.add_milestone_at=`于 %[2]s 添加了里程碑 <b>%[1]s</b>`
 issues.add_project_at=`于 %[2]s 将此添加到 <b>%[1]s</b> 项目`
+issues.move_to_column_of_project=`将此对象移至 %s 的 %s 中在 %s 上`
 issues.change_milestone_at=`%[3]s 修改了里程碑从 <b>%[1]s</b> 到 <b>%[2]s</b>`
 issues.change_project_at=于 %[3]s 将此从项目 <b>%[1]s</b> 移到 <b>%[2]s</b>
 issues.remove_milestone_at=`%[2]s 删除了里程碑 <b>%[1]s</b>`
@@ -1488,6 +1528,8 @@ issues.filter_assignee=指派人筛选
 issues.filter_assginee_no_select=所有指派成员
 issues.filter_assginee_no_assignee=未指派
 issues.filter_poster=作者
+issues.filter_user_placeholder=搜索用户
+issues.filter_user_no_select=所有用户
 issues.filter_type=类型筛选
 issues.filter_type.all_issues=所有工单
 issues.filter_type.assigned_to_you=指派给您的
@@ -1541,7 +1583,9 @@ issues.no_content=没有提供说明。
 issues.close=关闭工单
 issues.comment_pull_merged_at=已合并提交 %[1]s 到 %[2]s %[3]s
 issues.comment_manually_pull_merged_at=手动合并提交 %[1]s 到 %[2]s %[3]s
+issues.close_comment_issue=评论并关闭
 issues.reopen_issue=重新开启
+issues.reopen_comment_issue=评论并重新开启
 issues.create_comment=评论
 issues.comment.blocked_user=无法创建或编辑评论，因为您已被仓库所有者或工单创建者屏蔽。
 issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此工单`
@@ -1630,12 +1674,25 @@ issues.delete.title=是否删除工单?
 issues.delete.text=您真的要删除这个工单吗？(该操作将会永久删除所有内容。如果您需要保留，请关闭它)
 
 issues.tracker=时间跟踪
-
+issues.timetracker_timer_start=启动计时器
+issues.timetracker_timer_stop=停止计时器
+issues.timetracker_timer_discard=删除计时器
+issues.timetracker_timer_manually_add=添加时间
+
+issues.time_estimate_set=设置预计时间
+issues.time_estimate_display=预计： %s
+issues.change_time_estimate_at=将预计时间修改为 <b>%s</b> %s
+issues.remove_time_estimate_at=删除预计时间 %s
+issues.time_estimate_invalid=预计时间格式无效
+issues.start_tracking_history=`开始工作 %s`
 issues.tracker_auto_close=当此工单关闭时，自动停止计时器
 issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪！`
+issues.stop_tracking_history=`停止工作 %s`
 issues.cancel_tracking_history=`取消时间跟踪 %s`
 issues.del_time=删除此时间跟踪日志
+issues.add_time_history=`添加计时 %s`
 issues.del_time_history=`已删除时间 %s`
+issues.add_time_manually=手动添加时间
 issues.add_time_hours=小时
 issues.add_time_minutes=分钟
 issues.add_time_sum_to_small=没有输入时间。
@@ -1695,6 +1752,7 @@ issues.dependency.add_error_dep_not_same_repo=这两个工单必须在同一仓
 issues.review.self.approval=您不能批准您自己的合并请求。
 issues.review.self.rejection=您不能请求对您自己的合并请求进行更改。
 issues.review.approve=于 %s 批准此合并请求
+issues.review.comment=评审于 %s
 issues.review.dismissed=于 %[2]s 取消了 %[1]s 的评审
 issues.review.dismissed_label=已取消
 issues.review.left_comment=留下了一条评论
@@ -1720,6 +1778,10 @@ issues.review.resolve_conversation=已解决问题
 issues.review.un_resolve_conversation=未解决问题
 issues.review.resolved_by=标记问题为已解决
 issues.review.commented=评论
+issues.review.official=已批准
+issues.review.requested=等待审核
+issues.review.rejected=请求变更
+issues.review.stale=批准后已更新
 issues.review.unofficial=非官方审批数
 issues.assignee.error=因为未知原因，并非所有的指派都成功。
 issues.reference_issue.body=内容
@@ -1837,7 +1899,9 @@ pulls.unrelated_histories=合并失败：两个分支没有共同历史。提示
 pulls.merge_out_of_date=合并失败：在生成合并时，主分支已更新。提示：再试一次。
 pulls.head_out_of_date=合并失败：在生成合并时，head 已更新。提示：再试一次。
 pulls.has_merged=失败：合并请求已经被合并，您不能再次合并或更改目标分支。
+pulls.push_rejected=推送失败：推送被拒绝。审查此仓库的 Git 钩子。
 pulls.push_rejected_summary=详细拒绝信息
+pulls.push_rejected_no_message=推送失败：此推送被拒绝但未提供其他信息。请检查此仓库的 Git 钩子。
 pulls.open_unmerged_pull_exists=`您不能执行重新打开操作, 因为已经存在相同的合并请求 (#%d)。`
 pulls.status_checking=一些检测仍在等待运行
 pulls.status_checks_success=所有检测均成功
@@ -1881,6 +1945,10 @@ pulls.delete.title=删除此合并请求？
 pulls.delete.text=你真的要删除这个合并请求吗？ (这将永久删除所有内容。如果你打算将内容存档，请考虑关闭它)
 
 pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong>
+pulls.upstream_diverging_prompt_behind_1=该分支落后于 %s %d 个提交
+pulls.upstream_diverging_prompt_behind_n=该分支落后于 %s %d 个提交
+pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改
+pulls.upstream_diverging_merge=同步派生
 
 pull.deleted_branch=(已删除): %s
 pull.agit_documentation=查看有关 AGit 的文档
@@ -1894,6 +1962,7 @@ milestones.no_due_date=暂无截止日期
 milestones.open=开启
 milestones.close=关闭
 milestones.new_subheader=里程碑可以帮助您组织工单并跟踪其进度。
+milestones.completeness=<strong>%d%%</strong> 已完成
 milestones.create=创建里程碑
 milestones.title=标题
 milestones.desc=描述
@@ -2116,6 +2185,7 @@ settings.pulls.default_delete_branch_after_merge=默认合并后删除合并请
 settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑
 settings.releases_desc=启用发布
 settings.packages_desc=启用仓库软件包注册中心
+settings.projects_desc=启用项目
 settings.projects_mode_desc=项目模式 (要显示的项目类型)
 settings.projects_mode_repo=仅仓库项目
 settings.projects_mode_owner=仅限用户或组织项目
@@ -2155,6 +2225,7 @@ settings.transfer_in_progress=当前正在进行转让。 如果你想将此代
 settings.transfer_notices_1=- 如果将此仓库转移给其他用户, 您将失去对此仓库的访问权限。
 settings.transfer_notices_2=-如果将其转移到您 (共同) 拥有的组织，您可以继续访问该仓库。
 settings.transfer_notices_3=- 如果仓库是私有的并且被转移给某个用户，那么此操作可以确保该用户至少具有读权限(以及必要时的更改权限)。
+settings.transfer_notices_4=- 如果存储库属于某个组织，而您将其转移给另一个组织或个人，那么您将失去存储库工单与其组织项目系统之间的链接。
 settings.transfer_owner=新拥有者
 settings.transfer_perform=执行转让
 settings.transfer_started=该代码库已被标记为转让并等待来自 %s 的确认
@@ -2291,6 +2362,7 @@ settings.event_pull_request_merge=合并请求合并
 settings.event_package=软件包
 settings.event_package_desc=软件包已在仓库中被创建或删除。
 settings.branch_filter=分支过滤
+settings.branch_filter_desc=推送、创建，删除分支事件的分支白名单，使用 glob 模式匹配指定。若为空或 <code>*</code>，则将报告所有分支的事件。语法文档见 <a href="%[1]s">%[2]s</a>。示例：<code>master</code>,<code>{master,release*}</code>。
 settings.authorization_header=授权标头
 settings.authorization_header_desc=当存在时将被作为授权标头包含在内。例如: %s。
 settings.active=激活
@@ -2336,25 +2408,53 @@ settings.deploy_key_deletion=删除部署密钥
 settings.deploy_key_deletion_desc=删除部署密钥将取消此密钥对此仓库的访问权限。继续？
 settings.deploy_key_deletion_success=部署密钥已删除。
 settings.branches=分支
+settings.protected_branch=分支保护
 settings.protected_branch.save_rule=保存规则
 settings.protected_branch.delete_rule=删除规则
+settings.protected_branch_can_push=是否允许推送？
 settings.protected_branch_can_push_yes=你可以推
+settings.protected_branch_can_push_no=你不能推送
+settings.branch_protection=分支 '<b>%s</b>' 的保护规则
 settings.protect_this_branch=启用分支保护
 settings.protect_this_branch_desc=阻止删除并限制Git推送和合并到分支。
 settings.protect_disable_push=禁用推送
 settings.protect_disable_push_desc=此分支不允许推送。
+settings.protect_disable_force_push=禁用强制推送
+settings.protect_disable_force_push_desc=此分支禁止强制推送。
 settings.protect_enable_push=启用推送
 settings.protect_enable_push_desc=任何拥有写访问权限的人将被允许推送到此分支(但不能强行推送)。
+settings.protect_enable_force_push_all=启用强制推送
+settings.protect_enable_force_push_all_desc=任何拥有推送权限的人都可以对这个分支进行强制推送。
+settings.protect_enable_force_push_allowlist=强制推送白名单
+settings.protect_enable_force_push_allowlist_desc=只有白名单中的用户或团队才能强制推送到这个分支。
 settings.protect_enable_merge=启用合并
 settings.protect_enable_merge_desc=任何具有写入权限的人都可以将合并请求合并到此分支中。
+settings.protect_whitelist_committers=受白名单限制的推送
+settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。
+settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。
+settings.protect_whitelist_users=推送白名单用户：
+settings.protect_whitelist_teams=推送白名单团队：
+settings.protect_force_push_allowlist_users=允许强制推送的白名单用户：
+settings.protect_force_push_allowlist_teams=允许强制推送的白名单团队：
+settings.protect_force_push_allowlist_deploy_keys=允许白名单中的部署密钥进行强制推送。
+settings.protect_merge_whitelist_committers=启用合并白名单
+settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。
+settings.protect_merge_whitelist_users=合并白名单用户：
+settings.protect_merge_whitelist_teams=合并白名单团队：
 settings.protect_check_status_contexts=启用状态检查
 settings.protect_status_check_patterns=状态检查模式：
 settings.protect_status_check_patterns_desc=输入模式，指定哪些状态检查必须通过，才能将分支合并到符合此规则的分支中去。每一行指定一个模式，模式不能为空。
+settings.protect_check_status_contexts_desc=要求状态检查通过才能合并。如果启用，提交必须先推送到另一个分支，然后再合并或推送到匹配这些保护规则的分支。如果没有选择具体的状态检查上下文，则所有的状态检查都通过才能合并。
 settings.protect_check_status_contexts_list=此仓库上周进行过的状态检查
 settings.protect_status_check_matched=匹配
 settings.protect_invalid_status_check_pattern=无效的状态检查规则：“%s”。
 settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。
 settings.protect_required_approvals=所需的批准：
+settings.protect_required_approvals_desc=只允许合并有足够审核的合并请求。要求的审核必须来自白名单或者有权限的用户或团队。
+settings.protect_approvals_whitelist_enabled=仅列入白名单的用户或团队才可批准
+settings.protect_approvals_whitelist_enabled_desc=只有白名单用户或团队的审核才能计数。 如果规则没有批准白名单，来自任何有写访问权限的人的审核都将计数。
+settings.protect_approvals_whitelist_users=审查者白名单：
+settings.protect_approvals_whitelist_teams=审查团队白名单：
 settings.dismiss_stale_approvals=取消过时的批准
 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时，旧的批准将被撤销。
 settings.ignore_stale_approvals=忽略过期批准
@@ -2362,12 +2462,18 @@ settings.ignore_stale_approvals_desc=对旧提交（过期审核）的批准将
 settings.require_signed_commits=需要签名提交
 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
 settings.protect_branch_name_pattern=受保护的分支名称模式
+settings.protect_branch_name_pattern_desc=分支保护的名称匹配规则。语法请参阅 <a href="%s">文档</a> 。如：main, release/**
 settings.protect_patterns=规则
 settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔)：
+settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件，也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见<a href='%[1]s'>%[2]s</a>文档了解模式语法。例如： <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
 settings.protect_unprotected_file_patterns=不受保护的文件模式(使用分号 ';' 分隔)：
+settings.protect_unprotected_file_patterns_desc=如果用户有写权限，则允许直接更改的不受保护的文件，以绕过推送限制。可以使用分号分隔多个模式 (';')。 见 <a href='%[1]s'>%[2]s</a> 文档了解模式语法。例如： <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
+settings.add_protected_branch=启用保护
+settings.delete_protected_branch=禁用保护
 settings.update_protect_branch_success=分支保护规则 %s 更新成功。
 settings.remove_protected_branch_success=移除分支保护规则"%s"成功。
 settings.remove_protected_branch_failed=移除分支保护规则"%s"失败。
+settings.protected_branch_deletion=删除分支保护
 settings.protected_branch_deletion_desc=禁用分支保护允许具有写入权限的用户推送提交到此分支。继续？
 settings.block_rejected_reviews=拒绝审核阻止了此合并
 settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动，即使有足够的批准，合并也不允许。
@@ -2375,8 +2481,11 @@ settings.block_on_official_review_requests=有官方审核阻止了代码合并
 settings.block_on_official_review_requests_desc=处于评审状态时，即使有足够的批准，也不能合并。
 settings.block_outdated_branch=如果合并请求已经过时，阻止合并
 settings.block_outdated_branch_desc=当头部分支落后基础分支时，不能合并。
+settings.block_admin_merge_override=管理员须遵守分支保护规则
+settings.block_admin_merge_override_desc=管理员须遵守分支保护规则，不能规避该规则。
 settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交：
 settings.merge_style_desc=合并方式
+settings.default_merge_style_desc=默认合并风格
 settings.choose_branch=选择一个分支...
 settings.no_protected_branch=没有受保护的分支
 settings.edit_protected_branch=编辑
@@ -2392,12 +2501,25 @@ settings.tags.protection.allowed.teams=允许的团队
 settings.tags.protection.allowed.noone=无
 settings.tags.protection.create=保护Git标签
 settings.tags.protection.none=没有受保护的Git标签
+settings.tags.protection.pattern.description=你可以使用单个名称或 glob 模式匹配或正则表达式来匹配多个标签。了解详情请访问 <a target="_blank" rel="noopener" href="%s">保护Git标签指南</a>。
 settings.bot_token=Bot 令牌
 settings.chat_id=聊天 ID
 settings.thread_id=线程 ID
 settings.matrix.homeserver_url=主服务器网址
 settings.matrix.room_id=房间ID
 settings.matrix.message_type=消息类型
+settings.visibility.private.button=设为私有
+settings.visibility.private.text=将可见性更改为私有不仅会使仓库仅对允许的成员可见，而且可能会消除它与派生仓库、关注者和点赞之间的关系。
+settings.visibility.private.bullet_title=<strong>将可见性改为私有将会：</strong>
+settings.visibility.private.bullet_one=使仓库只对允许的成员可见。
+settings.visibility.private.bullet_two=可能会删除它与 <strong>派生仓库</strong>、 <strong>关注者</strong>和 <strong>点赞</strong> 之间的关系。
+settings.visibility.public.button=设为公开
+settings.visibility.public.text=将可见性更改为公开会使任何人都可见。
+settings.visibility.public.bullet_title=<strong>将可见性改为公开将会：</strong>
+settings.visibility.public.bullet_one=使仓库让任何人都可见。
+settings.visibility.success=仓库可见性已更改。
+settings.visibility.error=试图更改仓库可见性时出错。
+settings.visibility.fork_error=无法更改派生仓库的可见性。
 settings.archive.button=归档仓库
 settings.archive.header=归档此仓库
 settings.archive.text=归档仓库将使其完全只读。它将在首页隐藏。没有人（甚至你！）能够进行新的提交，或打开工单及合并请求。
@@ -2509,6 +2631,7 @@ release.new_release=发布新版
 release.draft=草稿
 release.prerelease=预发行
 release.stable=稳定
+release.latest=最新版本
 release.compare=比较
 release.edit=编辑
 release.ahead.commits=<strong>%d</strong> 次提交
@@ -2593,6 +2716,7 @@ tag.create_success=标签"%s"已存在
 
 topic.manage_topics=管理主题
 topic.done=保存
+topic.count_prompt=您最多选择25个主题
 topic.format_prompt=主题必须以字母或数字开头，可以包含连字符 ('-') 和句点 ('.')，长度不得超过35个字符。字符必须为小写。
 
 find_file.go_to_file=转到文件
@@ -2665,6 +2789,7 @@ settings.delete_prompt=删除操作会永久清除该组织的信息，并且 <s
 settings.confirm_delete_account=确认删除组织
 settings.delete_org_title=删除组织
 settings.delete_org_desc=此组织将会被永久删除，确认继续吗？
+settings.hooks_desc=在此处添加的 Web 钩子将会应用到该组织下的 <strong>所有仓库</strong>。
 
 settings.labels_desc=添加能够被该组织下的 <strong>所有仓库</strong> 的工单使用的标签。
 
@@ -2689,6 +2814,7 @@ teams.leave.detail=离开 %s？
 teams.can_create_org_repo=创建仓库
 teams.can_create_org_repo_helper=成员可以在组织中创建仓库。创建者将自动获得创建的仓库的管理员权限。
 teams.none_access=无访问权限
+teams.none_access_helper=成员无法查看此单元或对其执行任何其他操作。对公开仓库无此影响。
 teams.general_access=常规访问
 teams.general_access_helper=成员权限将由以下权限表决定。
 teams.read_access=可读
@@ -2757,6 +2883,7 @@ last_page=末页
 total=总计：%d
 settings=管理设置
 
+dashboard.new_version_hint=Gitea %s 现已可用，您正在运行 %s。查看 <a target="_blank" rel="noreferrer" href="%s">博客</a> 了解详情。
 dashboard.statistic=摘要
 dashboard.maintenance_operations=运维
 dashboard.system_status=系统状态
@@ -2799,6 +2926,7 @@ dashboard.reinit_missing_repos=重新初始化所有丢失的 Git 仓库存在
 dashboard.sync_external_users=同步外部用户数据
 dashboard.cleanup_hook_task_table=清理 hook_task 表
 dashboard.cleanup_packages=清理过期的软件包
+dashboard.cleanup_actions=清理过期的 Actions 资源
 dashboard.server_uptime=服务运行时间
 dashboard.current_goroutine=当前 Goroutines 数量
 dashboard.current_memory_usage=当前内存使用量
@@ -2828,12 +2956,19 @@ dashboard.total_gc_time=GC 暂停时间总量
 dashboard.total_gc_pause=GC 暂停时间总量
 dashboard.last_gc_pause=上次 GC 暂停时间
 dashboard.gc_times=GC 执行次数
+dashboard.delete_old_actions=从数据库中删除所有旧操作记录
+dashboard.delete_old_actions.started=已开始从数据库中删除所有旧操作记录。
 dashboard.update_checker=更新检查器
 dashboard.delete_old_system_notices=从数据库中删除所有旧系统通知
 dashboard.gc_lfs=垃圾回收 LFS 元数据
+dashboard.stop_zombie_tasks=停止僵尸任务
+dashboard.stop_endless_tasks=停止无法停止的任务
+dashboard.cancel_abandoned_jobs=取消丢弃的任务
+dashboard.start_schedule_tasks=开始Actions调度任务
 dashboard.sync_branch.started=分支同步已开始
 dashboard.sync_tag.started=标签同步已开始
 dashboard.rebuild_issue_indexer=重建工单索引
+dashboard.sync_repo_licenses=重新仓库许可证探测
 
 users.user_manage_panel=用户帐户管理
 users.new_account=创建新帐户
@@ -2905,6 +3040,10 @@ emails.not_updated=无法更新请求的电子邮件地址： %v
 emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。
 emails.change_email_header=更新电子邮件属性
 emails.change_email_text=您确定要更新该电子邮件地址吗？
+emails.delete=删除电子邮件
+emails.delete_desc=您确定要删除该电子邮件地址？
+emails.deletion_success=电子邮件地址已被删除。
+emails.delete_primary_email_error=您不能删除主电子邮件。
 
 orgs.org_manage_panel=组织管理
 orgs.name=名称
@@ -2937,10 +3076,12 @@ packages.size=大小
 packages.published=已发布
 
 defaulthooks=默认Web钩子
+defaulthooks.desc=当某些 Gitea 事件触发时，Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置，将被复制到所有新的仓库中。详情请访问 <a target="_blank" rel="noopener" href="%s">Web 钩子指南</a>。
 defaulthooks.add_webhook=添加默认Web 钩子
 defaulthooks.update_webhook=更新默认 Web 钩子
 
 systemhooks=系统 Web 钩子
+systemhooks.desc=当某些 Gitea 事件触发时，Web 钩子自动向服务器发出HTTP POST请求。这里定义的 Web 钩子将作用于系统上的所有仓库，所以请考虑这可能带来的任何性能影响。了解详情请访问 <a target="_blank" rel="noopener" href="%s">Web 钩子指南</a>。
 systemhooks.add_webhook=添加系统 Web 钩子
 systemhooks.update_webhook=更新系统 Web 钩子
 
@@ -3035,8 +3176,18 @@ auths.tips=帮助提示
 auths.tips.oauth2.general=OAuth2 认证
 auths.tips.oauth2.general.tip=当注册新的 OAuth2 身份验证时，回调/重定向 URL 应该是：
 auths.tip.oauth2_provider=OAuth2 提供程序
+auths.tip.bitbucket=在 %s 注册新的 OAuth 使用者同时添加权限“账号”-“读取”
 auths.tip.nextcloud=使用下面的菜单“设置（Settings） -> 安全（Security） -> OAuth 2.0 client”在您的实例上注册一个新的 OAuth 客户端。
+auths.tip.dropbox=在 %s 上创建一个新的应用程序
+auths.tip.facebook=`在 %s 注册一个新的应用，并添加产品"Facebook 登录"`
+auths.tip.github=在 %s 注册一个 OAuth 应用程序
+auths.tip.gitlab_new=在 %s 注册一个新的应用
+auths.tip.google_plus=从谷歌 API 控制台 %s 获得 OAuth2 客户端凭据
 auths.tip.openid_connect=使用 OpenID 连接发现 URL ({server}/.well-known/openid-configuration) 来指定终点
+auths.tip.twitter=访问 %s，创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。
+auths.tip.discord=在 %s 上注册新应用程序
+auths.tip.gitea=注册一个新的 OAuth2 应用程序。可以访问 %s 查看帮助
+auths.tip.yandex=在 %s 上创建一个新的应用程序。在“ Yandex.Passport API”这部分中选择以下权限：“访问电子邮件地址（Access to email address）”，“访问用户头像（Access to user avatar）”和“访问用户名，名字和姓氏，性别（Access to username, first name and surname, genderAccess to username, first name and surname, gender）”
 auths.tip.mastodon=输入您想要认证的 mastodon 实例的自定义 URL (或使用默认值)
 auths.edit=修改认证源
 auths.activated=该认证源已经启用
@@ -3153,6 +3304,9 @@ config.cache_interval=Cache 周期
 config.cache_conn=Cache 连接字符串
 config.cache_item_ttl=缓存项目 TTL
 config.cache_test=测试缓存
+config.cache_test_failed=缓存测试失败： %v。
+config.cache_test_slow=缓存测试成功，但响应缓慢： %s。
+config.cache_test_succeeded=缓存测试成功，在 %s 时间内得到响应。
 
 config.session_config=Session 配置
 config.session_provider=Session 提供者
@@ -3199,6 +3353,7 @@ monitor.next=下次执行时间
 monitor.previous=上次执行时间
 monitor.execute_times=执行次数
 monitor.process=运行中进程
+monitor.stacktrace=调用栈踪迹
 monitor.processes_count=%d 个进程
 monitor.download_diagnosis_report=下载诊断报告
 monitor.desc=进程描述
@@ -3206,6 +3361,8 @@ monitor.start=开始时间
 monitor.execute_time=执行时长
 monitor.last_execution_result=结果
 monitor.process.cancel=中止进程
+monitor.process.cancel_desc=中止一个进程可能导致数据丢失
+monitor.process.cancel_notices=中止：<strong>%s</strong> ？
 monitor.process.children=子进程
 
 monitor.queues=队列
@@ -3307,6 +3464,8 @@ raw_minutes=分钟
 
 [dropzone]
 default_message=拖动文件或者点击此处上传。
+invalid_input_type=您不能上传该类型的文件
+file_too_big=文件体积（{{filesize}} MB）超过了最大允许体积（{{maxFilesize}} MB）
 remove_file=移除文件
 
 [notification]
@@ -3378,6 +3537,8 @@ alpine.repository=仓库信息
 alpine.repository.branches=分支
 alpine.repository.repositories=仓库管理
 alpine.repository.architectures=架构
+arch.registry=添加具有相关仓库和架构的服务器到 <code>/etc/pacman.conf</code> 中：
+arch.install=使用 pacman 同步软件包：
 arch.repository=仓库信息
 arch.repository.repositories=仓库管理
 arch.repository.architectures=架构
@@ -3456,6 +3617,7 @@ settings.link=将此软件包链接到仓库
 settings.link.description=如果您将一个软件包与一个代码库链接起来，软件包将显示在代码库的软件包列表中。
 settings.link.select=选择仓库
 settings.link.button=更新仓库链接
+settings.link.success=仓库链接已成功更新。
 settings.link.error=更新仓库链接失败。
 settings.delete=删除软件包
 settings.delete.description=删除软件包是永久性的，无法撤消。
@@ -3579,12 +3741,18 @@ runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗？请查看
 runs.no_workflows.documentation=关于Gitea Actions的更多信息，请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
 runs.no_runs=工作流尚未运行过。
 runs.empty_commit_message=(空白的提交消息)
+runs.expire_log_message=旧的日志已被清除
 
 workflow.disable=禁用工作流
 workflow.disable_success=工作流 '%s' 已成功禁用。
 workflow.enable=启用工作流
 workflow.enable_success=工作流 '%s' 已成功启用。
 workflow.disabled=工作流已禁用。
+workflow.run=运行工作流
+workflow.not_found=工作流 %s 未找到。
+workflow.run_success=工作流 %s 已成功运行。
+workflow.from_ref=使用工作流从
+workflow.has_workflow_dispatch=此 Workflow 有一个 Workflow_dispatch 事件触发器。
 
 need_approval_desc=该工作流由派生仓库的合并请求所触发，需要批准方可运行。
 
@@ -3605,6 +3773,7 @@ variables.update.failed=编辑变量失败。
 variables.update.success=该变量已被编辑。
 
 [projects]
+deleted.display_name=已删除项目
 type-1.display_name=个人项目
 type-2.display_name=仓库项目
 type-3.display_name=组织项目
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index b711038da065a..ba17fa427d1a8 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -205,8 +205,11 @@ func ViewPost(ctx *context_module.Context) {
 		}
 	}
 
+	// TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
+	// need to be refactored together in the future
 	metas := ctx.Repo.Repository.ComposeMetas(ctx)
 
+	// the title for the "run" is from the commit message
 	resp.State.Run.Title = run.Title
 	resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas)
 	resp.State.Run.Link = run.Link()
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 020e5784dcfb7..6a6c60cd40e2c 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/models/auth"
 	user_model "code.gitea.io/gitea/models/user"
 	auth_module "code.gitea.io/gitea/modules/auth"
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	source_service "code.gitea.io/gitea/services/auth/source"
@@ -31,7 +32,12 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u
 		return nil, user_model.ErrUserNotExist{Name: loginName}
 	}
 	// Fallback.
+	// FIXME: this fallback would cause problems when the "Username" attribute is not set and a user inputs their email.
+	// In this case, the email would be used as the username, and will cause the "CreateUser" failure for the first login.
 	if sr.Username == "" {
+		if strings.Contains(userName, "@") {
+			log.Error("No username in search result (Username Attribute is not set properly?), using email as username might cause problems")
+		}
 		sr.Username = userName
 	}
 	if sr.Mail == "" {
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index 207774bfefd03..04732d276ae52 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -36,9 +36,10 @@
 				</thead>
 				<tbody>
 					{{range .PackageDescriptor.Metadata.Manifests}}
+						{{/* "unknown/unknown" is attestation-manifest, so we should skip it */}}
 						{{if ne .Platform "unknown/unknown"}}
 						<tr>
-							<td><a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td>
+							<td><a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td>
 							<td>{{.Platform}}</td>
 							<td>{{FileSize .Size}}</td>
 						</tr>
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 8792c4e1b2be5..71f77154fb923 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -68,11 +68,13 @@
 											<p id="cherry-pick-content" class="branch-dropdown"></p>
 
 											<form method="get">
+												{{/*FIXME: CurrentRefShortName seems not making sense here (old code),
+												because the "commit page" has no "$.BranchName" info, so only using DefaultBranch should be enough */}}
 												{{template "repo/branch_dropdown" dict
 													"Repository" .Repository
 													"ShowTabBranches" true
 													"CurrentRefType" "branch"
-													"CurrentRefShortName" (Iif $.BranchName $.Repository.DefaultBranch)
+													"CurrentRefShortName" (or $.BranchName $.Repository.DefaultBranch)
 													"RefFormActionTemplate" (print "{RepoLink}/_cherrypick/" .CommitID "/{RefShortName}")
 												}}
 												<input type="hidden" id="cherry-pick-type" name="cherry-pick-type"><br>
diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl
index 927328ba14ab6..88e2e43120552 100644
--- a/templates/repo/issue/filter_item_label.tmpl
+++ b/templates/repo/issue/filter_item_label.tmpl
@@ -23,6 +23,9 @@
 		<div class="divider"></div>
 		<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
 		<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
+		{{/* The logic here is not the same as the label selector in the issue sidebar.
+		The one in the issue sidebar renders "repo labels | divider | org labels".
+		Maybe the logic should be updated to be consistent.*/}}
 		{{$previousExclusiveScope := "_no_scope"}}
 		{{range .Labels}}
 			{{$exclusiveScope := .ExclusiveScope}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 9daa051e06104..dd608e5aa1e53 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -48,6 +48,7 @@
 			</div>
 		</form>
 		{{end}}{{/*if .EnablePasswordSignInForm*/}}
+		{{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}}
 		{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
 		{{if and $showOAuth2Methods .EnablePasswordSignInForm}}
 			<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index ea8d0bafe40da..b3b2a4205e1ce 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -47,6 +47,8 @@
 					</button>
 				</div>
 			{{end}}
+			{{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}}
+			{{/* TODO: it seems that "EnableSSPI" is only set in "sign-in" handlers, but it should use the same logic to control its display */}}
 			{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
 			{{if $showOAuth2Methods}}
 				<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 7f960a4709ac9..7924dd2305762 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -61,6 +61,7 @@
 							{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLinkWithFilter "SupportArchivedLabel" true}}
 						{{end}}
 
+						{{/* at the moment there is no easy way to get poster candidates on this page, so only show a username input, search for what the end user enters */}}
 						{{if ne $.ViewType "created_by"}}
 							{{template "repo/issue/filter_item_user_fetch" dict
 								"QueryParamKey" "poster"
@@ -70,6 +71,7 @@
 							}}
 						{{end}}
 
+						{{/* at the moment there is no easy way to get assignee candidates on this page, so only show a username input, search for what the end user enters */}}
 						{{if ne $.ViewType "assigned"}}
 							{{template "repo/issue/filter_item_user_fetch" dict
 								"QueryParamKey" "assignee"
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index 2f66336a66f3c..4b7ca1429dca4 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -113,6 +113,16 @@ const sfc = {
       if (this.menuVisible) this.menuVisible = false;
     });
   },
+
+  mounted() {
+    if (this.refFormActionTemplate) {
+      // if the selector is used in a form and needs to change the form action,
+      // make a mock item and select it to update the form action
+      const item: ListItem = {selected: true, refType: this.currentRefType, refShortName: this.currentRefShortName, rssFeedLink: ''};
+      this.selectItem(item);
+    }
+  },
+
   methods: {
     selectItem(item: ListItem) {
       this.menuVisible = false;

From c8ffe777cf9c5bb47a38e3e0b3a3b5de6cd8813d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 16 Dec 2024 20:00:55 -0800
Subject: [PATCH 002/206] Add 1.23.0-rc0 changelog (#32863)

Notice: some of pull requests haven't been added to the changelog

- Which have been backported to v1.22
-
https://github.com/go-gitea/gitea/pulls?q=sort%3Aupdated-desc+is%3Apr+milestone%3A1.23.0+is%3Amerged+label%3Abackport%2Fv1.22
- Which have skip-changelog
-
https://github.com/go-gitea/gitea/pulls?q=sort%3Aupdated-desc+is%3Apr+milestone%3A1.23.0+is%3Amerged+-label%3Abackport%2Fv1.22+label%3Askip-changelog
- Build and Testing
-
https://github.com/go-gitea/gitea/pulls?q=sort%3Aupdated-desc+is%3Apr+milestone%3A1.23.0+is%3Amerged+label%3Abackport%2Fv1.22+label%3Atype%2Ftesting
-
https://github.com/go-gitea/gitea/pulls?q=sort%3Aupdated-desc+is%3Apr+milestone%3A1.23.0+is%3Amerged+label%3Abackport%2Fv1.22+label%3Atopic%2Fbuild
- No `type` label marked may be missed, I need to check one by one, but
I believe those are not too many.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 CHANGELOG.md | 348 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 348 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab8c2ac2234c5..204d51dbaf675 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,354 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0-rc0) - 2024-12-16
+
+* BREAKING
+  * Rename config option `[camo].Allways` to `[camo].Always` (#32097)
+  * Remove SHA1 for support for ssh rsa signing (#31857)
+  * Use UTC as default timezone when schedule Actions cron tasks (#31742)
+  * Delete Actions logs older than 1 year by default (#31735)
+  * Make OIDC introspection authentication strictly require Client ID and secret (#31632)
+
+* SECURITY
+  * Include file extension checks in attachment API (#32151)
+  * Include all security fixes which have been backported to v1.22
+
+* FEATURES
+  * Allow to fork repository into the same owner (#32819)
+  * Support "merge upstream branch" (Sync fork) (#32741)
+  * Add Arch package registry (#32692)
+  * Allow to disable the password-based login (sign-in) form (#32687)
+  * Allow cropping an avatar before setting it (#32565)
+  * Support quote selected comments to reply (#32431)
+  * Add reviewers selection to new pull request (#32403)
+  * Suggestions for issues (#32327)
+  * Add priority to protected branch (#32286)
+  * Included tag search capabilities (#32045)
+  * Add option to filter board cards by labels and assignees (#31999)
+  * Add automatic light/dark option for the colorblind theme (#31997)
+  * Support migration from AWS CodeCommit (#31981)
+  * Introduce globallock as distributed locks (#31908 & #31813)
+  * Support compression for Actions logs & enable by default (#31761 & #32013)
+  * Add pure SSH LFS support (#31516)
+  * Add Passkey login support (#31504)
+  * Actions support workflow dispatch event (#28163)
+  * Support repo license (#24872)
+  * Issue time estimate, meaningful time tracking (#23113)
+  * GitHub like repo home page (#32213 & #32847)
+  * Rearrange Clone Panel (#31142)
+  * Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
+
+* PERFORMANCE
+  * Perf: add extra index to notification table (#32395)
+  * Introduce OrgList and add LoadTeams, optimaze Load teams for orgs (#32543)
+  * Improve performance of diffs (#32393)
+  * Make LFS http_client parallel within a batch. (#32369)
+  * Add new index for action to resolve the performance problem (#32333)
+  * Improve get feed with pagination (#31821)
+  * Performance improvements for pull request list API (#30490)
+  * Use batch database operations instead of one by one to optimze api pulls (#32680)
+
+* ENHANCEMENTS
+  * Code
+    * Remove unnecessary border in repo home page sidebar (#32767)
+    * Add 'Copy path' button to file view (#32584)
+    * Improve diff file tree (#32658)
+    * Add new [lfs_client].BATCH_SIZE and [server].LFS_MAX_BATCH_SIZE config settings. (#32307)
+    * Updated tokenizer to better matching when search for code snippets (#32261)
+    * Change the code search to sort results by relevance (#32134)
+    * Support migrating GitHub/GitLab PR draft status (#32242)
+    * Move lock icon position and add additional tooltips to branch list page (#31839)
+    * Add tag name in the commits list (#31082)
+    * Add `MAX_ROWS` option for CSV rendering (#30268)
+    * Allow code search by filename (#32210)
+    * Make git push options accept short name (#32245)
+    * Repo file list enhancements (#32835)
+
+  * Markdown & Editor
+    * Refactor markdown math render, add dollor-backquote syntax support (#32831)
+    * Make Monaco theme follow browser, fully type codeeditor.ts (#32756)
+    * Refactor markdown editor and use it for milestone description editor (#32688)
+    * Add some handy markdown editor features (#32400)
+    * Improve markdown textarea for indentation and lists (#31406)
+
+  * Issue
+    * Add label/author/assignee filters to the user/org home issue list (#32779)
+    * Refactor issue filter (labels, poster, assignee) (#32771)
+    * Style unification for the issue_management area (#32605)
+    * Add "View all branches/tags" entry to Branch Selector (#32653)
+    * Improve textarea paste (#31948)
+    * Add avif image file support (#32508)
+    * Prevent from submitting issue/comment on uploading (#32263)
+    * Issue Templates: add option to have dropdown printed list (#31577)
+    * Allow searching issues by ID (#31479)
+    * Add `is_archived` option for issue indexer (#32735)
+    * Improve attachment upload methods (#30513)
+    * Support issue template assignees (#31083)
+    * Prevent simultaneous editing of comments and issues (#31053)
+    * Add issue comment when moving issues from one column to another of the project (#29311)
+
+  * Pull Request
+    * Display head branch more comfortable on pull request view (#32000)
+    * Simplify review UI (#31062)
+    * Allow force push to protected branches (#28086)
+    * Add line-through for deleted branch on pull request view page (#32500)
+    * Support requested_reviewers data in comment webhook events (#26178)
+    * Allow maintainers to view and edit files of private repos when "Allow maintainers to edit" is enabled (#32215)
+    * Allow including `Reviewed-on`/`Reviewed-by` lines for custom merge messages (#31211)
+
+  * Actions
+    * Render job title as commit message (#32748)
+    * Refactor RepoActionView.vue, add `::group::` support (#32713)
+    * Make RepoActionView.vue support `##[group]` (#32770)
+    * Support `pull_request_target` event for commit status (#31703)
+    * Detect whether action view branch was deleted (#32764)
+    * Allow users with write permission to run actions (#32644)
+    * Show latest run when visit /run/latest (#31808)
+
+  * Packages
+    * Improve rubygems package registry (#31357)
+    * Add support for npm bundleDependencies (#30751)
+    * Add signature support for the RPM module (#27069)
+    * Extract and display readme and comments for Composer packages (#30927)
+
+  * Project
+    * Add title to project view page (#32747)
+    * Set the columns height to hug all its contents (#31726)
+    * Rename project `board` -> `column` to make the UI less confusing (#30170)
+
+  * User & Organazition
+    * Use better name for userinfo structure (#32544)
+    * Use user.FullName in Oauth2 id_token response (#32542)
+    * Limit org member view of restricted users (#32211)
+    * Allow disabling authentication related user features (#31535)
+    * Add option to change mail from user display name (#31528)
+    * Use FullName in Emails to address the recipient if possible (#31527)
+
+  * Administration
+    * Add support for a credentials chain for minio access (#31051)
+    * Move admin routers from /admin to /-/admin (#32189)
+    * Add cache test for admins (#31265)
+    * Add option for mailer to override mail headers (#27860)
+    * Azure blob storage support (#30995)
+    * Supports forced use of S3 virtual-hosted style (#30969)
+    * Move repository visibility to danger zone in the settings area (#31126)
+
+  * Others
+    * Remove urls from translations (#31950)
+    * Simplify 404/500 page (#31409)
+    * Optimize installation-page experience (#32558)
+    * Refactor login page (#31530)
+    * Add new event commit status creation and webhook implementation (#27151)
+    * Repo Activity: count new issues that were closed (#31776)
+    * Set manual `tabindex`es on login page (#31689)
+    * Add `YEAR`, `MONTH`, `MONTH_ENGLISH`, `DAY` variables for template repos (#31584)
+    * Add typescript guideline and typescript-specific eslint plugins and fix issues (#31521)
+    * Make toast support preventDuplicates (#31501)
+    * Fix tautological conditions (#30735)
+
+* API
+  * Implement update branch API (#32433)
+  * Fix missing outputs for jobs with matrix (#32823)
+  * Make API "compare" accept commit IDs (#32801)
+  * Add github compatible tarball download API endpoints (#32572)
+  * Harden runner updateTask and updateLog api (#32462)
+  * Add `DISABLE_ORGANIZATIONS_PAGE` and `DISABLE_CODE_PAGE` settings for explore pages and fix an issue related to user search (#32288)
+  * Make admins adhere to branch protection rules (#32248)
+  * Calculate `PublicOnly` for org membership only once (#32234)
+  * Allow filtering PRs by poster in the ListPullRequests API (#32209)
+  * Return 404 instead of error when commit not exist (#31977)
+  * Save initial signup information for users to aid in spam prevention (#31852)
+  * Fix upload maven pacakge parallelly (#31851)
+  * Fix null requested_reviewer from API (#31773)
+  * Add permission description for API to add repo collaborator (#31744)
+  * Add return type to GetRawFileOrLFS and GetRawFile (#31680)
+  * Add skip secondary authorization option for public oauth2 clients (#31454)
+  * Add tag protection via rest api #17862 (#31295)
+  * Document possible action types for the user activity feed API (#31196)
+  * Add topics for repository API (#31127)
+  * Add support for searching users by email (#30908)
+  * Add API endpoints for getting action jobs status (#26673)
+
+* REFACTOR
+  * Update JS and PY dependencies (#31940)
+  * Enable `no-jquery/no-parse-html-literal` and fix violation (#31684)
+  * Refactor image diff (#31444)
+  * Refactor CSRF token (#32216)
+  * Fix some typescript issues (#32586)
+  * Refactor names (#31405)
+  * Use per package global lock for container uploads instead of memory lock (#31860)
+  * Move team related functions to service layer (#32537)
+  * Move GetFeeds to service layer (#32526)
+  * Resolve lint for unused parameter and unnecessary type arguments (#30750)
+  * Reimplement GetUserOrgsList to make it simple and clear (#32486)
+  * Move some functions from issue.go to standalone files (#32468)
+  * Refactor sidebar assignee&milestone&project selectors (#32465)
+  * Refactor sidebar label selector (#32460)
+  * Fix a number of typescript issues (#32459)
+  * Refactor language menu and dom utils (#32450)
+  * Refactor issue page info (#32445)
+  * Split issue sidebar into small templates (#32444)
+  * Refactor template ctx and render utils (#32422)
+  * Refactor repo legacy (#32404)
+  * Refactor markup package (#32399)
+  * Refactor markup render system (#32533 & #32589 & #32612)
+  * Refactor the DB migration system slightly (#32344)
+  * Remove jQuery import from some files (#32512)
+  * Strict pagination check (#32548)
+  * Split mail sender sub package from mailer service package (#32618)
+  * Remove outdated code about fixture generation (#32708)
+  * Refactor RepoBranchTagSelector (#32681)
+  * Refactor issue list (#32755)
+  * Refactor LabelEdit (#32752)
+  * Split issue/pull view router function as multiple smaller functions (#32749)
+  * Refactor some LDAP code (#32849)
+  * Unify repo search order by logic (#30876)
+  * Remove duplicate empty repo check in delete branch API (#32569)
+  * Replace deprecated `math/rand` functions (#30733)
+  * Remove fomantic dimmer module (#30723)
+  * Add types to fetch,toast,bootstrap,svg (#31627)
+  * Refactor webhook (#31587)
+  * Move AddCollabrator and CreateRepositoryByExample to service layer (#32419)
+  * Refactor RepoRefByType (#32413)
+  * Refactor: remove redundant err declarations (#32381)
+  * Refactor markup code (#31399)
+  * Refactor render system (orgmode) (#32671)
+  * Refactor render system (#32492)
+  * Refactor markdown render (#32736 & #32728)
+  * Refactor repo unit "disabled" check (#31389)
+  * Refactor route path normalization (#31381)
+  * Refactor to use UnsafeStringToBytes (#31358)
+  * Migrate vue components to setup (#32329)
+  * Refactor globallock (#31933)
+  * Use correct function name (#31887)
+  * Use a common message template instead of a special one (#31878)
+  * Fix a number of Typescript issues (#31877)
+  * Refactor dropzone (#31482)
+  * Move custom `tw-` helpers to tailwind plugin (#31184)
+  * Replace `gt-word-break` with `tw-break-anywhere` (#31183)
+  * Drop `IDOrderDesc` for listing Actions task and always order by `id DESC` (#31150)
+  * Split common-global.js into separate files (#31438)
+  * Improve detecting empty files (#31332)
+  * Use `querySelector` over alternative DOM methods (#31280)
+  * Remove jQuery `.text()` (#30506)
+  * Use repo as of renderctx's member rather than a repoPath on metas (#29222)
+  * Refactor some frontend problems (#32646)
+  * Refactor DateUtils and merge TimeSince (#32409)
+  * Replace DateTime with proper functions (#32402)
+  * Replace DateTime with DateUtils (#32383)
+  * Convert frontend code to typescript (#31559)
+
+* BUGFIXES
+  * Fix issues with inconsistent spacing in areas (#32607)
+  * Fix incomplete Actions status aggregations (#32859)
+  * In some lfs server implementations, they require the ref attribute. (#32838)
+  * Update the list of watchers and stargazers when clicking watch/unwatch or star/unstar (#32570)
+  * Fix `recentupdate` sorting bugs (#32505)
+  * Fix incorrect "Target branch does not exist" in PR title (#32222)
+  * Handle "close" actionable references for manual merges (#31879)
+  * render plain text file if the LFS object doesn't exist (#31812)
+  * Fix Null Pointer error for CommitStatusesHideActionsURL (#31731)
+  * Fix loadRepository error when access user dashboard (#31719)
+  * Hide the "Details" link of commit status when the user cannot access actions (#30156)
+  * Fix duplicate dropdown dividers (#32760)
+  * Fix SSPI button visibility when SSPI is the only enabled method (#32841)
+  * Fix overflow on org header (#32837)
+  * Exclude protected branches from recently pushed (#31748)
+  * Fix large image overflow in comment page (#31740)
+  * Fix milestone deadline and date related problems (#32339)
+  * Fix markdown preview $$ support (#31514)
+  * Fix a compilation error in the Gitpod environment (#32559)
+  * Fix PR diff review form submit (#32596)
+  * Fix a number of typescript issues (#32308)
+  * Fix some function names in comment (#32300)
+  * Fix absolute-date (#32375)
+  * Clarify Actions resources ownership (#31724)
+
+* MISC
+  * Optimize branch protection rule loading (#32280)
+  * Bump to go 1.23 (#31855)
+  * Remove unused call to $.HeadRepo in view_title template (#32317)
+  * Do not display `attestation-manifest` and use short sha256 instead of full sha256 (#32851)
+  * Upgrade htmx to 2.0.4 (#32834)
+  * Improve JSX/TSX support in code editor (#32833)
+  * Add User-Agent for gitea's self-implemented lfs client. (#32832)
+  * Use errors.New to replace fmt.Errorf with no parameters (#32800)
+  * Add "n commits" link to contributors in contributors graph page (#32799)
+  * Update dependencies, tweak eslint (#32719)
+  * Remove all "floated" CSS styles (#32691)
+  * Show tag name on branch/tag selector if repo shown from tag ref (#32689)
+  * Use new mail package instead of an unmintained one (#32682)
+  * Optimize the styling of icon buttons within file-header-right (#32675)
+  * Validate OAuth Redirect URIs (#32643)
+  * Support optional/configurable IAMEndpoint for Minio Client (#32581) (#32581)
+  * Make search box in issue sidebar dropdown list always show when scrolling (#32576)
+  * Bump CI,Flake and Snap to Node 22 (#32487)
+  * Update `github.com/meilisearch/meilisearch-go` (#32484)
+  * Add `DEFAULT_MIRROR_REPO_UNITS` and `DEFAULT_TEMPLATE_REPO_UNITS` options (#32416)
+  * Update go dependencies (#32389)
+  * Update JS and PY dependencies (#32388)
+  * Upgrade rollup to 4.24.0 (#32312)
+  * Upgrade vue to 3.5.12 (#32311)
+  * Improve the maintainblity of the reserved username list (#32229)
+  * Upgrade htmx to 2.0.3 (#32192)
+  * Count typescript files as frontend for labeling (#32088)
+  * Only use Host header from reverse proxy (#32060)
+  * Failed authentications are logged to level Warning (#32016)
+  * Enhance USER_DISABLED_FEATURES to allow disabling change username or full name (#31959)
+  * Distinguish official vs non-official reviews, add tool tips, and upgr… (#31924)
+  * Update mermaid to v11 (#31913)
+  * Bump relative-time-element to v4.4.3 (#31910)
+  * Upgrade `htmx` to `2.0.2` (#31847)
+  * Add warning message in merge instructions when `AutodetectManualMerge` was not enabled (#31805)
+  * Add types to various low-level functions (#31781)
+  * Update JS dependencies (#31766)
+  * Remove unused code from models/repos/release.go (#31756)
+  * Support delete user email in admin panel (#31690)
+  * Add `username` to OIDC introspection response (#31688)
+  * Use GetDisplayName() instead of DisplayName() to generate rss feeds (#31687)
+  * Code editor theme enhancements (#31629)
+  * Update JS dependencies (#31616)
+  * Add types for js globals (#31586)
+  * Add back esbuild-loader for .js files (#31585)
+  * Don't show hidden labels when filling out an issue template (#31576)
+  * Allow synchronizing user status from OAuth2 login providers (#31572)
+  * Display app name in the registration email title (#31562)
+  * Use stable version of fabric (#31526)
+  * Support legacy _links LFS batch responses (#31513)
+  * Fix JS error with disabled attachment and easymde (#31511)
+  * Always use HTML attributes for avatar size (#31509)
+  * Use nolyfill to remove some polyfills (#31468)
+  * Disable issue/PR comment button given empty input (#31463)
+  * Add simple JS init performance trace (#31459)
+  * Bump htmx to 2.0.0 (#31413)
+  * Update JS dependencies, remove `eslint-plugin-jquery` (#31402)
+  * Split org Propfile README to a new tab `overview` (#31373)
+  * Update nix flake and add gofumpt (#31320)
+  * Code optimization (#31315)
+  * Enable poetry non-package mode (#31282)
+  * Optimize profile layout to enhance visual experience (#31278)
+  * Update `golang.org/x/net` (#31260)
+  * Bump `@github/relative-time-element` to v4.4.1 (#31232)
+  * Remove unnecessary inline style for tab-size (#31224)
+  * Update golangci-lint to v1.59.0 (#31221)
+  * Update chroma to v2.14.0 (#31177)
+  * Update JS dependencies (#31120)
+  * Improve the handling of `jobs.<job_id>.if` (#31070)
+  * Clean up revive linter config, tweak golangci output (#30980)
+  * Use CSS `inset` shorthand (#30939)
+  * Forbid deprecated `break-word` in CSS (#30934)
+  * Remove obsolete monaco workaround (#30893)
+  * Update JS dependencies, add new eslint rules (#30840)
+  * Fix body margin shifting with modals, fix error on project column edit (#30831)
+  * Remove disk-clean workflow (#30741)
+  * Bump `github.com/google/go-github` to v61 (#30738)
+  * Add built js files to eslint ignore (#30737)
+  * Use `ProtonMail/go-crypto` for `opengpg` in tests (#30736)
+  * Upgrade xorm to v1.3.9 and improve some migrations Sync (#29899)
+  * Added default sorting milestones by name (#27084)
+  * Enable `unparam` linter (#31277)
+
 ## [1.22.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14
 
 * SECURITY

From cb422320809977b11921d34df3b981708ff1a90a Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 18 Dec 2024 19:56:47 +0800
Subject: [PATCH 003/206] Fix Arch package metadata introduced incorrect field
 (#32881) (#32882)

Backport #32881 by ExplodingDragon

Incorrect content was introduced while generating the index, which has
now been removed, and the missing fields have been added.

Co-authored-by: Exploding Dragon <explodingfkl@gmail.com>
---
 modules/packages/arch/metadata.go      | 6 ++++++
 modules/packages/arch/metadata_test.go | 4 ++++
 services/packages/arch/repository.go   | 3 ++-
 3 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go
index e1e79c60e0dfe..06a2206a36218 100644
--- a/modules/packages/arch/metadata.go
+++ b/modules/packages/arch/metadata.go
@@ -69,10 +69,12 @@ type FileMetadata struct {
 	Packager      string   `json:"packager,omitempty"`
 	Groups        []string `json:"groups,omitempty"`
 	Provides      []string `json:"provides,omitempty"`
+	Replaces      []string `json:"replaces,omitempty"`
 	Depends       []string `json:"depends,omitempty"`
 	OptDepends    []string `json:"opt_depends,omitempty"`
 	MakeDepends   []string `json:"make_depends,omitempty"`
 	CheckDepends  []string `json:"check_depends,omitempty"`
+	Conflicts     []string `json:"conflicts,omitempty"`
 	XData         []string `json:"xdata,omitempty"`
 	Backup        []string `json:"backup,omitempty"`
 	Files         []string `json:"files,omitempty"`
@@ -201,12 +203,16 @@ func ParsePackageInfo(r io.Reader) (*Package, error) {
 			p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
 		case "depend":
 			p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
+		case "replaces":
+			p.FileMetadata.Replaces = append(p.FileMetadata.Replaces, value)
 		case "optdepend":
 			p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
 		case "makedepend":
 			p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
 		case "checkdepend":
 			p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
+		case "conflict":
+			p.FileMetadata.Conflicts = append(p.FileMetadata.Conflicts, value)
 		case "backup":
 			p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
 		case "group":
diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go
index f611ef5e84586..37c0a553b8423 100644
--- a/modules/packages/arch/metadata_test.go
+++ b/modules/packages/arch/metadata_test.go
@@ -42,8 +42,10 @@ depend = gitea
 provides = common
 provides = gitea
 optdepend = hex
+replaces = gogs
 checkdepend = common
 makedepend = cmake
+conflict = ninja
 backup = usr/bin/paket1`)
 }
 
@@ -149,8 +151,10 @@ func TestParsePackageInfo(t *testing.T) {
 		assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
 		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
 		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
+		assert.ElementsMatch(t, []string{"gogs"}, p.FileMetadata.Replaces)
 		assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
 		assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
+		assert.ElementsMatch(t, []string{"ninja"}, p.FileMetadata.Conflicts)
 		assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
 		assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
 	})
diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go
index ab1b85ae95870..6731d9a1ac728 100644
--- a/services/packages/arch/repository.go
+++ b/services/packages/arch/repository.go
@@ -371,11 +371,12 @@ func writeDescription(tw *tar.Writer, opts *entryOptions) error {
 		{"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)},
 		{"PACKAGER", opts.FileMetadata.Packager},
 		{"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")},
+		{"REPLACES", strings.Join(opts.FileMetadata.Replaces, "\n")},
+		{"CONFLICTS", strings.Join(opts.FileMetadata.Conflicts, "\n")},
 		{"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")},
 		{"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")},
 		{"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")},
 		{"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")},
-		{"XDATA", strings.Join(opts.FileMetadata.XData, "\n")},
 	})
 }
 

From fa5a0645597e8f27d613170796e8b7b54fe6f683 Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Wed, 18 Dec 2024 17:44:55 -0500
Subject: [PATCH 004/206] bump x/net (#32896) (#32899)

backport #32896 to 1.23
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 671151d4b633a..9b99face841ae 100644
--- a/go.mod
+++ b/go.mod
@@ -123,7 +123,7 @@ require (
 	github.com/yuin/goldmark-meta v1.1.0
 	golang.org/x/crypto v0.31.0
 	golang.org/x/image v0.21.0
-	golang.org/x/net v0.30.0
+	golang.org/x/net v0.33.0
 	golang.org/x/oauth2 v0.23.0
 	golang.org/x/sync v0.10.0
 	golang.org/x/sys v0.28.0
diff --git a/go.sum b/go.sum
index afa3abece8f6e..93b2f9fa99910 100644
--- a/go.sum
+++ b/go.sum
@@ -932,8 +932,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
 golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

From a4291fd55378ee73dbda70a19b93764e8c547377 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 19 Dec 2024 17:14:04 +0800
Subject: [PATCH 005/206] Add more load functions to make sure the reference
 object loaded (#32901) (#32912)

Backport #32901 by @lunny

Fix #32897

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 services/convert/pull.go     | 5 +++++
 services/webhook/notifier.go | 4 ++++
 2 files changed, 9 insertions(+)

diff --git a/services/convert/pull.go b/services/convert/pull.go
index ddaaa300a4420..a1ab7eeb8eca6 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -31,6 +31,11 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		err        error
 	)
 
+	if err = pr.LoadIssue(ctx); err != nil {
+		log.Error("pr.LoadIssue[%d]: %v", pr.ID, err)
+		return nil
+	}
+
 	if err = pr.Issue.LoadRepo(ctx); err != nil {
 		log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err)
 		return nil
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index cc263947e9b90..a3d5cb34b19d4 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -410,6 +410,10 @@ func (m *webhookNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 	var pullRequest *api.PullRequest
 	if issue.IsPull {
 		eventType = webhook_module.HookEventPullRequestComment
+		if err := issue.LoadPullRequest(ctx); err != nil {
+			log.Error("LoadPullRequest: %v", err)
+			return
+		}
 		pullRequest = convert.ToAPIPullRequest(ctx, issue.PullRequest, doer)
 	} else {
 		eventType = webhook_module.HookEventIssueComment

From c20642fa9936c00e1277896152a0f73d89e3de4b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 19 Dec 2024 19:02:58 +0800
Subject: [PATCH 006/206] Relax the version checking for Arch packages (#32908)
 (#32913)

Backport #32908 by ExplodingDragon

It is mentioned in https://man.archlinux.org/man/PKGBUILD.5: 'The
variable is not allowed to contain colons, forward slashes, hyphens, or
whitespace.'

`_` is also an allowed character, and some software in the Arch Linux
AUR uses this naming convention.

Co-authored-by: Exploding Dragon <explodingfkl@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/packages/arch/metadata.go      | 5 +++--
 modules/packages/arch/metadata_test.go | 8 ++++++++
 2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go
index 06a2206a36218..3d7bdf3997f74 100644
--- a/modules/packages/arch/metadata.go
+++ b/modules/packages/arch/metadata.go
@@ -43,8 +43,9 @@ var (
 	ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
 
 	// https://man.archlinux.org/man/PKGBUILD.5
-	namePattern    = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
-	versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
+	namePattern = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
+	// (epoch:pkgver-pkgrel)
+	versionPattern = regexp.MustCompile(`\A(?:\d:)?[\w.+~]+(?:-[-\w.+~]+)?\z`)
 )
 
 type Package struct {
diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go
index 37c0a553b8423..5bdf63a3ee33e 100644
--- a/modules/packages/arch/metadata_test.go
+++ b/modules/packages/arch/metadata_test.go
@@ -122,6 +122,14 @@ func TestParsePackageInfo(t *testing.T) {
 		assert.ErrorIs(t, err, ErrInvalidName)
 	})
 
+	t.Run("Regexp", func(t *testing.T) {
+		assert.Regexp(t, versionPattern, "1.2_3~4+5")
+		assert.Regexp(t, versionPattern, "1:2_3~4+5")
+		assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
+		assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
+		assert.NotRegexp(t, versionPattern, "1.0.0 -1")
+	})
+
 	t.Run("InvalidVersion", func(t *testing.T) {
 		data := createPKGINFOContent(packageName, "")
 

From 1e71ad89cee8ea433ae32be0fd17cda3f1f5b0e8 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 21 Dec 2024 00:20:51 +0800
Subject: [PATCH 007/206] Deprecated gopid in log (#32932)

---
 modules/log/event_format.go       | 26 +++++++++++-------------
 modules/log/event_format_test.go  | 30 +++++++++++++---------------
 modules/log/flags.go              |  2 +-
 modules/log/groutinelabel.go      | 19 ------------------
 modules/log/groutinelabel_test.go | 33 -------------------------------
 modules/log/logger_impl.go        |  5 -----
 6 files changed, 27 insertions(+), 88 deletions(-)
 delete mode 100644 modules/log/groutinelabel.go
 delete mode 100644 modules/log/groutinelabel_test.go

diff --git a/modules/log/event_format.go b/modules/log/event_format.go
index 0b8d1cec7919f..8fda0a498071e 100644
--- a/modules/log/event_format.go
+++ b/modules/log/event_format.go
@@ -13,10 +13,9 @@ import (
 type Event struct {
 	Time time.Time
 
-	GoroutinePid string
-	Caller       string
-	Filename     string
-	Line         int
+	Caller   string
+	Filename string
+	Line     int
 
 	Level Level
 
@@ -218,17 +217,16 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms
 	}
 
 	if flags&Lgopid == Lgopid {
-		if event.GoroutinePid != "" {
-			buf = append(buf, '[')
-			if mode.Colorize {
-				buf = append(buf, ColorBytes(FgHiYellow)...)
-			}
-			buf = append(buf, event.GoroutinePid...)
-			if mode.Colorize {
-				buf = append(buf, resetBytes...)
-			}
-			buf = append(buf, ']', ' ')
+		deprecatedGoroutinePid := "no-gopid" // use a dummy value to avoid breaking the log format
+		buf = append(buf, '[')
+		if mode.Colorize {
+			buf = append(buf, ColorBytes(FgHiYellow)...)
+		}
+		buf = append(buf, deprecatedGoroutinePid...)
+		if mode.Colorize {
+			buf = append(buf, resetBytes...)
 		}
+		buf = append(buf, ']', ' ')
 	}
 	buf = append(buf, msg...)
 
diff --git a/modules/log/event_format_test.go b/modules/log/event_format_test.go
index 7c299a607d862..6fd0f36d48581 100644
--- a/modules/log/event_format_test.go
+++ b/modules/log/event_format_test.go
@@ -24,34 +24,32 @@ func TestItoa(t *testing.T) {
 func TestEventFormatTextMessage(t *testing.T) {
 	res := EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: false, Flags: Flags{defined: true, flags: 0xffffffff}},
 		&Event{
-			Time:         time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
-			Caller:       "caller",
-			Filename:     "filename",
-			Line:         123,
-			GoroutinePid: "pid",
-			Level:        ERROR,
-			Stacktrace:   "stacktrace",
+			Time:       time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
+			Caller:     "caller",
+			Filename:   "filename",
+			Line:       123,
+			Level:      ERROR,
+			Stacktrace: "stacktrace",
 		},
 		"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
 	)
 
-	assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [pid] msg format: arg0 arg1
+	assert.Equal(t, `[PREFIX] 2020/01/02 03:04:05.000000 filename:123:caller [E] [no-gopid] msg format: arg0 arg1
 	stacktrace
 
 `, string(res))
 
 	res = EventFormatTextMessage(&WriterMode{Prefix: "[PREFIX] ", Colorize: true, Flags: Flags{defined: true, flags: 0xffffffff}},
 		&Event{
-			Time:         time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
-			Caller:       "caller",
-			Filename:     "filename",
-			Line:         123,
-			GoroutinePid: "pid",
-			Level:        ERROR,
-			Stacktrace:   "stacktrace",
+			Time:       time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
+			Caller:     "caller",
+			Filename:   "filename",
+			Line:       123,
+			Level:      ERROR,
+			Stacktrace: "stacktrace",
 		},
 		"msg format: %v %v", "arg0", NewColoredValue("arg1", FgBlue),
 	)
 
-	assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mpid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
+	assert.Equal(t, "[PREFIX] \x1b[36m2020/01/02 03:04:05.000000 \x1b[0m\x1b[32mfilename:123:\x1b[32mcaller\x1b[0m \x1b[1;31m[E]\x1b[0m [\x1b[93mno-gopid\x1b[0m] msg format: arg0 \x1b[34marg1\x1b[0m\n\tstacktrace\n\n", string(res))
 }
diff --git a/modules/log/flags.go b/modules/log/flags.go
index f025159d53946..8064c91745094 100644
--- a/modules/log/flags.go
+++ b/modules/log/flags.go
@@ -30,7 +30,7 @@ const (
 	LUTC                              // if Ldate or Ltime is set, use UTC rather than the local time zone
 	Llevelinitial                     // Initial character of the provided level in brackets, eg. [I] for info
 	Llevel                            // Provided level in brackets [INFO]
-	Lgopid                            // the Goroutine-PID of the context
+	Lgopid                            // the Goroutine-PID of the context, deprecated and it is always a const value
 
 	Lmedfile  = Lshortfile | Llongfile                                    // last 20 characters of the filename
 	LstdFlags = Ldate | Ltime | Lmedfile | Lshortfuncname | Llevelinitial // default
diff --git a/modules/log/groutinelabel.go b/modules/log/groutinelabel.go
deleted file mode 100644
index 56d7af42da3fb..0000000000000
--- a/modules/log/groutinelabel.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package log
-
-import "unsafe"
-
-//go:linkname runtime_getProfLabel runtime/pprof.runtime_getProfLabel
-func runtime_getProfLabel() unsafe.Pointer //nolint
-
-type labelMap map[string]string
-
-func getGoroutineLabels() map[string]string {
-	l := (*labelMap)(runtime_getProfLabel())
-	if l == nil {
-		return nil
-	}
-	return *l
-}
diff --git a/modules/log/groutinelabel_test.go b/modules/log/groutinelabel_test.go
deleted file mode 100644
index 34e99653d6aa0..0000000000000
--- a/modules/log/groutinelabel_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package log
-
-import (
-	"context"
-	"runtime/pprof"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func Test_getGoroutineLabels(t *testing.T) {
-	pprof.Do(context.Background(), pprof.Labels(), func(ctx context.Context) {
-		currentLabels := getGoroutineLabels()
-		pprof.ForLabels(ctx, func(key, value string) bool {
-			assert.EqualValues(t, value, currentLabels[key])
-			return true
-		})
-
-		pprof.Do(ctx, pprof.Labels("Test_getGoroutineLabels", "Test_getGoroutineLabels_child1"), func(ctx context.Context) {
-			currentLabels := getGoroutineLabels()
-			pprof.ForLabels(ctx, func(key, value string) bool {
-				assert.EqualValues(t, value, currentLabels[key])
-				return true
-			})
-			if assert.NotNil(t, currentLabels) {
-				assert.EqualValues(t, "Test_getGoroutineLabels_child1", currentLabels["Test_getGoroutineLabels"])
-			}
-		})
-	})
-}
diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go
index d38c6516ed5ce..76dd5f43fbc2a 100644
--- a/modules/log/logger_impl.go
+++ b/modules/log/logger_impl.go
@@ -200,11 +200,6 @@ func (l *LoggerImpl) Log(skip int, level Level, format string, logArgs ...any) {
 		event.Stacktrace = Stack(skip + 1)
 	}
 
-	labels := getGoroutineLabels()
-	if labels != nil {
-		event.GoroutinePid = labels["pid"]
-	}
-
 	// get a simple text message without color
 	msgArgs := make([]any, len(logArgs))
 	copy(msgArgs, logArgs)

From b1c21880c161476e40d5cbdef9d66fb6c6538908 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 21 Dec 2024 23:24:17 +0800
Subject: [PATCH 008/206] Update i18n.go - Language Picker (#32933) (#32935)

---
 modules/setting/i18n.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/setting/i18n.go b/modules/setting/i18n.go
index c3076c0ab70b8..6e0767320f17d 100644
--- a/modules/setting/i18n.go
+++ b/modules/setting/i18n.go
@@ -11,6 +11,7 @@ var defaultI18nLangNames = []string{
 	"zh-TW", "繁體中文（台灣）",
 	"de-DE", "Deutsch",
 	"fr-FR", "Français",
+	"ga-IE", "Gaeilge",
 	"nl-NL", "Nederlands",
 	"lv-LV", "Latviešu",
 	"ru-RU", "Русский",

From 57868c2315aa11f3790076bec38fbe009bbcbd6a Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 22 Dec 2024 08:10:42 +0800
Subject: [PATCH 009/206] Fix locale (#32937) (#32942)

---
 options/locale/locale_en-US.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 92ce4f2db9ffe..73041ceee5607 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1946,8 +1946,8 @@ pulls.delete.title = Delete this pull request?
 pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
 
 pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s
-pulls.upstream_diverging_prompt_behind_1 = This branch is %d commit behind %s
-pulls.upstream_diverging_prompt_behind_n = This branch is %d commits behind %s
+pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[2]s
+pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
 pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
 pulls.upstream_diverging_merge = Sync fork
 

From c442c682ef57f83115593d72541f314da464b2dc Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 22 Dec 2024 23:28:36 +0800
Subject: [PATCH 010/206] Use primary as button color (#32949) (#32950)

Backport #32949 by wxiaoguang

* Fix #32871
* Fix #32948

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/clone_panel.tmpl                  | 2 +-
 templates/repo/code/upstream_diverging_info.tmpl | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl
index 8cbeda132dd8b..d3496bdb73a04 100644
--- a/templates/repo/clone_panel.tmpl
+++ b/templates/repo/clone_panel.tmpl
@@ -1,4 +1,4 @@
-<button class="ui green button js-btn-clone-panel">
+<button class="ui primary button js-btn-clone-panel">
 	<span>{{svg "octicon-code" 16}} Code</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 </button>
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index 299ba63e9efcf..51402598f95cb 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -10,7 +10,7 @@
 		{{end}}
 	</div>
 	{{if .CanWriteCode}}
-	<button class="ui compact green button tw-m-0 link-action" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
+	<button class="ui compact primary button tw-m-0 link-action" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
 		{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
 	</button>
 	{{end}}

From a8f98fd3be059c484fcee4d8e320f62059eea026 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 23 Dec 2024 06:08:35 +0800
Subject: [PATCH 011/206] fix trailing comma not matched in the case of
 alphanumeric issue (#32945) (#32959)

Backport #32945 by @katsusan

Fix #32428.

Patch the regex to match `,`besides `.` `"` `'` `:` and space.

Co-authored-by: katsu <evergonuaa@gmail.com>
---
 modules/references/references.go      | 2 +-
 modules/references/references_test.go | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/references/references.go b/modules/references/references.go
index 6e549cb875851..dcb70a33d0f6d 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -32,7 +32,7 @@ var (
 	// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
 	issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\'|\")([#!][0-9]+)(?:\s|$|\)|\]|\'|\"|[:;,.?!]\s|[:;,.?!]$)`)
 	// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
-	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\')`)
+	issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[|\"|\')([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$)|\"|\'|,)`)
 	// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
 	// e.g. org/repo#12345
 	crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index e224c919e9216..27803083c0b74 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -463,6 +463,7 @@ func TestRegExp_issueAlphanumericPattern(t *testing.T) {
 		"ABC-123:",
 		"\"ABC-123\"",
 		"'ABC-123'",
+		"ABC-123, unknown PR",
 	}
 	falseTestCases := []string{
 		"RC-08",

From d670820722bfc475599b847be99ad373dc438a0b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 23 Dec 2024 06:16:34 +0800
Subject: [PATCH 012/206] Use Alpine 3.21 for the docker images (#32924)
 (#32951)

---
 Dockerfile          | 6 +++---
 Dockerfile.rootless | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 2a6b1dd6b7d0d..b95ba8328960d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.23-alpine3.20 AS build-env
+FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY=${GOPROXY:-direct}
@@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
               /go/src/code.gitea.io/gitea/environment-to-ini
 RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 
-FROM docker.io/library/alpine:3.20
+FROM docker.io/library/alpine:3.21
 LABEL maintainer="maintainers@gitea.io"
 
 EXPOSE 22 3000
@@ -78,7 +78,7 @@ ENV GITEA_CUSTOM=/data/gitea
 VOLUME ["/data"]
 
 ENTRYPOINT ["/usr/bin/entrypoint"]
-CMD ["/bin/s6-svscan", "/etc/s6"]
+CMD ["/usr/bin/s6-svscan", "/etc/s6"]
 
 COPY --from=build-env /tmp/local /
 COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index 26f02205a7a93..be6f125104815 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
 # Build stage
-FROM docker.io/library/golang:1.23-alpine3.20 AS build-env
+FROM docker.io/library/golang:1.23-alpine3.21 AS build-env
 
 ARG GOPROXY
 ENV GOPROXY=${GOPROXY:-direct}
@@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
               /go/src/code.gitea.io/gitea/environment-to-ini
 RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
 
-FROM docker.io/library/alpine:3.20
+FROM docker.io/library/alpine:3.21
 LABEL maintainer="maintainers@gitea.io"
 
 EXPOSE 2222 3000

From e1026feddc71bad493b184b21e790db8c71c87a3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 23 Dec 2024 11:17:38 +0800
Subject: [PATCH 013/206] Fix repo avatar conflict (#32958) (#32960)

Backport #32958 by wxiaoguang

Continue even if the avatar deleting fails

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/repository/license_test.go | 9 ++-------
 services/repository/avatar.go      | 4 ++--
 services/repository/avatar_test.go | 8 ++++++++
 services/repository/delete.go      | 3 ++-
 4 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go
index 3b0cfa1eed483..d00156a496afd 100644
--- a/modules/repository/license_test.go
+++ b/modules/repository/license_test.go
@@ -31,12 +31,7 @@ func Test_getLicense(t *testing.T) {
 
 Copyright (c) 2023 Gitea
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-`,
+Permission is hereby granted`,
 			wantErr: assert.NoError,
 		},
 		{
@@ -53,7 +48,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 			if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) {
 				return
 			}
-			assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values)
+			assert.Contains(t, string(got), tt.want, "GetLicense(%v, %v)", tt.args.name, tt.args.values)
 		})
 	}
 }
diff --git a/services/repository/avatar.go b/services/repository/avatar.go
index 38c2621bc4d1a..15e51d4a258bc 100644
--- a/services/repository/avatar.go
+++ b/services/repository/avatar.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"io"
 	"strconv"
-	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -107,7 +106,8 @@ func RemoveRandomAvatars(ctx context.Context) error {
 
 // generateAvatar generates the avatar from a template repository
 func generateAvatar(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error {
-	generateRepo.Avatar = strings.Replace(templateRepo.Avatar, strconv.FormatInt(templateRepo.ID, 10), strconv.FormatInt(generateRepo.ID, 10), 1)
+	// generate a new different hash, whatever the "hash data" is, it doesn't matter
+	generateRepo.Avatar = avatar.HashAvatar(generateRepo.ID, []byte("new-avatar"))
 	if _, err := storage.Copy(storage.RepoAvatars, generateRepo.CustomAvatarRelativePath(), storage.RepoAvatars, templateRepo.CustomAvatarRelativePath()); err != nil {
 		return err
 	}
diff --git a/services/repository/avatar_test.go b/services/repository/avatar_test.go
index 4a0ba61853895..bea820e85f0f3 100644
--- a/services/repository/avatar_test.go
+++ b/services/repository/avatar_test.go
@@ -61,3 +61,11 @@ func TestDeleteAvatar(t *testing.T) {
 
 	assert.Equal(t, "", repo.Avatar)
 }
+
+func TestGenerateAvatar(t *testing.T) {
+	templateRepo := &repo_model.Repository{ID: 10, Avatar: "a"}
+	generateRepo := &repo_model.Repository{ID: 11}
+	_ = generateAvatar(db.DefaultContext, templateRepo, generateRepo)
+	assert.NotEmpty(t, generateRepo.Avatar)
+	assert.NotEqual(t, templateRepo.Avatar, generateRepo.Avatar)
+}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index f33bae779011e..aa1bbf884c684 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -324,7 +324,8 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 
 	if len(repo.Avatar) > 0 {
 		if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil {
-			return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err)
+			log.Error("remove avatar file %q: %v", repo.CustomAvatarRelativePath(), err)
+			// go on
 		}
 	}
 

From e18e31d557c7ad6286e844205c841233d3bddc8a Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 23 Dec 2024 15:32:29 +0800
Subject: [PATCH 014/206] Fix commit range paging (#32944) (#32962)

Backport #32944 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/git/repo_commit.go      |  6 ++----
 modules/git/repo_commit_test.go | 22 ++++++++++++++++++++++
 2 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 9ffadb833d8f6..647894bb21346 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct {
 
 // CommitsByFileAndRange return the commits according revision file and the page
 func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
-	skip := (opts.Page - 1) * setting.Git.CommitsRangeSize
-
 	stdoutReader, stdoutWriter := io.Pipe()
 	defer func() {
 		_ = stdoutReader.Close()
@@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
 	go func() {
 		stderr := strings.Builder{}
 		gitCmd := NewCommand(repo.Ctx, "rev-list").
-			AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page).
-			AddOptionFormat("--skip=%d", skip)
+			AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
+			AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
 		gitCmd.AddDynamicArguments(opts.Revision)
 
 		if opts.Not != "" {
diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go
index 4c26fa2a486bf..e9f469accdf0c 100644
--- a/modules/git/repo_commit_test.go
+++ b/modules/git/repo_commit_test.go
@@ -8,7 +8,11 @@ import (
 	"path/filepath"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestRepository_GetCommitBranches(t *testing.T) {
@@ -126,3 +130,21 @@ func TestGetRefCommitID(t *testing.T) {
 		}
 	}
 }
+
+func TestCommitsByFileAndRange(t *testing.T) {
+	defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)()
+
+	bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
+	bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
+	require.NoError(t, err)
+	defer bareRepo1.Close()
+
+	// "foo" has 3 commits in "master" branch
+	commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1})
+	require.NoError(t, err)
+	assert.Len(t, commits, 2)
+
+	commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2})
+	require.NoError(t, err)
+	assert.Len(t, commits, 1)
+}

From 0c581106d28acc0f377f54189e11e5215db74ce6 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 23 Dec 2024 15:57:36 +0800
Subject: [PATCH 015/206] Fix outdated tmpl code (#32953) (#32961)

Backport #32953 by wxiaoguang

Some PRs were before tmpl ctx refactoring and used outdated code

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/package/metadata/arch.tmpl            | 2 +-
 templates/repo/issue/view_content/comments.tmpl | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl
index 2aea036ec2dd2..62308e4aa0bc1 100644
--- a/templates/package/metadata/arch.tmpl
+++ b/templates/package/metadata/arch.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "arch"}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
 	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 3f9af180275c8..9fdbf45939060 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -693,7 +693,7 @@
 		{{else if eq .Type 38}}
 			<div class="timeline-item event" id="{{.HashTag}}">
 				<span class="badge">{{svg "octicon-clock"}}</span>
-				{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
+				{{template "shared/user/avatarlink" dict "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
 					{{$timeStr := .Content|TimeEstimateString}}

From 90bd08ceef6264eb4cb57201708044c9161bdbf3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 24 Dec 2024 00:37:11 +0800
Subject: [PATCH 016/206] Use env GITEA_RUNNER_REGISTRATION_TOKEN as global
 runner token (#32946) (#32964)

Backport #32946 by wxiaoguang

Fix #23703

When Gitea starts, it reads GITEA_RUNNER_REGISTRATION_TOKEN
or GITEA_RUNNER_REGISTRATION_TOKEN_FILE to add registration token.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/actions/runner_token.go            | 21 +++---
 options/locale/locale_en-US.ini           |  1 +
 routers/init.go                           |  2 +-
 routers/web/shared/actions/runners.go     |  3 +-
 routers/web/web.go                        |  2 +-
 services/actions/init.go                  | 51 ++++++++++++++-
 services/actions/init_test.go             | 80 +++++++++++++++++++++++
 templates/shared/actions/runner_list.tmpl | 10 ++-
 8 files changed, 152 insertions(+), 18 deletions(-)
 create mode 100644 services/actions/init_test.go

diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go
index fd6ba7ecadb3f..1eab5efcce70e 100644
--- a/models/actions/runner_token.go
+++ b/models/actions/runner_token.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 )
@@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist)
+		return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
 	}
 	return &runnerToken, nil
 }
@@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
 	return err
 }
 
-// NewRunnerToken creates a new active runner token and invalidate all old tokens
+// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens
 // ownerID will be ignored and treated as 0 if repoID is non-zero.
-func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
+func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) {
 	if ownerID != 0 && repoID != 0 {
 		// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
 		// Remove OwnerID to avoid confusion; it's not worth returning an error here.
 		ownerID = 0
 	}
 
-	token, err := util.CryptoRandomString(40)
-	if err != nil {
-		return nil, err
-	}
 	runnerToken := &ActionRunnerToken{
 		OwnerID:  ownerID,
 		RepoID:   repoID,
@@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
 			return err
 		}
 
-		_, err = db.GetEngine(ctx).Insert(runnerToken)
+		_, err := db.GetEngine(ctx).Insert(runnerToken)
 		return err
 	})
 }
 
+func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
+	token, err := util.CryptoRandomString(40)
+	if err != nil {
+		return nil, err
+	}
+	return NewRunnerTokenWithValue(ctx, ownerID, repoID, token)
+}
+
 // GetLatestRunnerToken returns the latest runner token
 func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
 	if ownerID != 0 && repoID != 0 {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 73041ceee5607..16bd0fe8b6edd 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3722,6 +3722,7 @@ runners.status.active = Active
 runners.status.offline = Offline
 runners.version = Version
 runners.reset_registration_token = Reset registration token
+runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one?
 runners.reset_registration_token_success = Runner registration token reset successfully
 
 runs.all_workflows = All Workflows
diff --git a/routers/init.go b/routers/init.go
index 2091f5967acac..98ce1bc4c9858 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) {
 	auth.Init()
 	mustInit(svg.Init)
 
-	actions_service.Init()
+	mustInitCtx(ctx, actions_service.Init)
 
 	mustInit(repo_service.InitLicenseClassifier)
 
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index f38933226b883..6d77bdd2fab74 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
 		ctx.ServerError("ResetRunnerRegistrationToken", err)
 		return
 	}
-
 	ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
-	ctx.Redirect(redirectTo)
+	ctx.JSONRedirect(redirectTo)
 }
 
 // RunnerDeletePost response for deleting a runner
diff --git a/routers/web/web.go b/routers/web/web.go
index 72ee47bb4c962..a243a79f8a17d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -463,7 +463,7 @@ func registerRoutes(m *web.Router) {
 			m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
 				Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
 			m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
-			m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
+			m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
 		})
 	}
 
diff --git a/services/actions/init.go b/services/actions/init.go
index 0f49cb629784f..7136da05ed8f3 100644
--- a/services/actions/init.go
+++ b/services/actions/init.go
@@ -4,23 +4,68 @@
 package actions
 
 import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"strings"
+
+	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/queue"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	notify_service "code.gitea.io/gitea/services/notify"
 )
 
-func Init() {
+func initGlobalRunnerToken(ctx context.Context) error {
+	// use the same env name as the runner, for consistency
+	token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN")
+	tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE")
+	if token != "" && tokenFile != "" {
+		return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used")
+	}
+	if tokenFile != "" {
+		file, err := os.ReadFile(tokenFile)
+		if err != nil {
+			return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err)
+		}
+		token = strings.TrimSpace(string(file))
+	}
+	if token == "" {
+		return nil
+	}
+
+	if len(token) < 32 {
+		return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters")
+	}
+
+	existing, err := actions_model.GetRunnerToken(ctx, token)
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		return fmt.Errorf("unable to check existing token: %w", err)
+	}
+	if existing != nil {
+		if !existing.IsActive {
+			log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI")
+		}
+		return nil
+	}
+	_, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token)
+	return err
+}
+
+func Init(ctx context.Context) error {
 	if !setting.Actions.Enabled {
-		return
+		return nil
 	}
 
 	jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
 	if jobEmitterQueue == nil {
-		log.Fatal("Unable to create actions_ready_job queue")
+		return errors.New("unable to create actions_ready_job queue")
 	}
 	go graceful.GetManager().RunWithCancel(jobEmitterQueue)
 
 	notify_service.RegisterNotifier(NewNotifier())
+	return initGlobalRunnerToken(ctx)
 }
diff --git a/services/actions/init_test.go b/services/actions/init_test.go
new file mode 100644
index 0000000000000..59c321ccd771e
--- /dev/null
+++ b/services/actions/init_test.go
@@ -0,0 +1,80 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+	"os"
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/util"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m, &unittest.TestOptions{
+		FixtureFiles: []string{"action_runner_token.yml"},
+	})
+	os.Exit(m.Run())
+}
+
+func TestInitToken(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	t.Run("NoToken", func(t *testing.T) {
+		_, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token")
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
+		err := initGlobalRunnerToken(db.DefaultContext)
+		require.NoError(t, err)
+		notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{})
+		require.NoError(t, err)
+		assert.False(t, notEmpty)
+	})
+
+	t.Run("EnvToken", func(t *testing.T) {
+		tokenValue, _ := util.CryptoRandomString(32)
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue)
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "")
+		err := initGlobalRunnerToken(db.DefaultContext)
+		require.NoError(t, err)
+		token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
+		assert.True(t, token.IsActive)
+
+		// init with the same token again, should not create a new token
+		err = initGlobalRunnerToken(db.DefaultContext)
+		require.NoError(t, err)
+		token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
+		assert.Equal(t, token.ID, token2.ID)
+		assert.True(t, token.IsActive)
+	})
+
+	t.Run("EnvFileToken", func(t *testing.T) {
+		tokenValue, _ := util.CryptoRandomString(32)
+		f := t.TempDir() + "/token"
+		_ = os.WriteFile(f, []byte(tokenValue), 0o644)
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "")
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f)
+		err := initGlobalRunnerToken(db.DefaultContext)
+		require.NoError(t, err)
+		token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
+		assert.True(t, token.IsActive)
+
+		// if the env token is invalidated by another new token, then it shouldn't be active anymore
+		_, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0)
+		require.NoError(t, err)
+		token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue})
+		assert.False(t, token.IsActive)
+	})
+
+	t.Run("InvalidToken", func(t *testing.T) {
+		t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc")
+		err := initGlobalRunnerToken(db.DefaultContext)
+		assert.ErrorContains(t, err, "must be at least")
+	})
+}
diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl
index f652d56e09bc6..e5907da8e81ab 100644
--- a/templates/shared/actions/runner_list.tmpl
+++ b/templates/shared/actions/runner_list.tmpl
@@ -3,7 +3,7 @@
 	<h4 class="ui top attached header">
 		{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
 		<div class="ui right">
-			<div class="ui top right pointing dropdown">
+			<div class="ui top right pointing dropdown jump">
 				<button class="ui primary tiny button">
 					{{ctx.Locale.Tr "actions.runners.new"}}
 					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
@@ -17,14 +17,18 @@
 						Registration Token
 					</div>
 					<div class="ui input">
-						<input type="text" value="{{.RegistrationToken}}">
+						<input type="text" value="{{.RegistrationToken}}" readonly>
 						<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
 							{{svg "octicon-copy" 14}}
 						</button>
 					</div>
 					<div class="divider"></div>
 					<div class="item">
-						<a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a>
+						<a class="link-action" data-url="{{$.Link}}/reset_registration_token"
+							data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
+						>
+							{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
+						</a>
 					</div>
 				</div>
 			</div>

From 0e0ebf68d78127bba19ce647bb1d99fcdc119715 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 25 Dec 2024 13:10:14 +0800
Subject: [PATCH 017/206] fix textarea newline handle (#32966) (#32977)

Backport #32966 by metiftikci

Co-authored-by: metiftikci <metiftikci@hotmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 .../js/features/comp/EditorMarkdown.test.ts   | 24 +++++++++++++++----
 web_src/js/features/comp/EditorMarkdown.ts    |  1 +
 2 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/web_src/js/features/comp/EditorMarkdown.test.ts b/web_src/js/features/comp/EditorMarkdown.test.ts
index acd496bed6d1b..7b4b44e83cb56 100644
--- a/web_src/js/features/comp/EditorMarkdown.test.ts
+++ b/web_src/js/features/comp/EditorMarkdown.test.ts
@@ -4,13 +4,24 @@ test('EditorMarkdown', () => {
   const textarea = document.createElement('textarea');
   initTextareaMarkdown(textarea);
 
-  const testInput = (value, expected) => {
-    textarea.value = value;
-    textarea.setSelectionRange(value.length, value.length);
+  type ValueWithCursor = string | {
+    value: string;
+    pos: number;
+  }
+  const testInput = (input: ValueWithCursor, result: ValueWithCursor) => {
+    const intputValue = typeof input === 'string' ? input : input.value;
+    const inputPos = typeof input === 'string' ? intputValue.length : input.pos;
+    textarea.value = intputValue;
+    textarea.setSelectionRange(inputPos, inputPos);
+
     const e = new KeyboardEvent('keydown', {key: 'Enter', cancelable: true});
     textarea.dispatchEvent(e);
-    if (!e.defaultPrevented) textarea.value += '\n';
-    expect(textarea.value).toEqual(expected);
+    if (!e.defaultPrevented) textarea.value += '\n'; // simulate default behavior
+
+    const expectedValue = typeof result === 'string' ? result : result.value;
+    const expectedPos = typeof result === 'string' ? expectedValue.length : result.pos;
+    expect(textarea.value).toEqual(expectedValue);
+    expect(textarea.selectionStart).toEqual(expectedPos);
   };
 
   testInput('-', '-\n');
@@ -18,8 +29,11 @@ test('EditorMarkdown', () => {
 
   testInput('- ', '');
   testInput('1. ', '');
+  testInput({value: '1. \n2. ', pos: 3}, {value: '\n2. ', pos: 0});
 
   testInput('- x', '- x\n- ');
+  testInput('1. foo', '1. foo\n1. ');
+  testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n1. \n2. b\n3. c', pos: 8});
   testInput('- [ ]', '- [ ]\n- ');
   testInput('- [ ] foo', '- [ ] foo\n- [ ] ');
   testInput('* [x] foo', '* [x] foo\n* [ ] ');
diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts
index 2af003ccb0a43..5e2ef121f5297 100644
--- a/web_src/js/features/comp/EditorMarkdown.ts
+++ b/web_src/js/features/comp/EditorMarkdown.ts
@@ -92,6 +92,7 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
   if (!line) {
     // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
     textarea.value = value.slice(0, lineStart) + value.slice(lineEnd);
+    textarea.setSelectionRange(selStart - prefix.length, selStart - prefix.length);
   } else {
     // start a new line with the same indention and prefix
     let newPrefix = prefix;

From af5e5e8f00f49e2332af06fb23764c0dd356600e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 25 Dec 2024 13:47:51 +0800
Subject: [PATCH 018/206] Fix maven pom inheritance (#32943) (#32976)

Backport #32943 by wxiaoguang

Fix  #30568

At the moment, here only `GroupID` (no `Version`) is parsed & used

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/packages/maven/metadata.go      | 36 ++++++++++++++++++-------
 modules/packages/maven/metadata_test.go | 34 +++++++++++++++++++++++
 routers/api/packages/api.go             |  2 ++
 3 files changed, 63 insertions(+), 9 deletions(-)

diff --git a/modules/packages/maven/metadata.go b/modules/packages/maven/metadata.go
index 42aa250718d37..a61a62c086208 100644
--- a/modules/packages/maven/metadata.go
+++ b/modules/packages/maven/metadata.go
@@ -7,6 +7,7 @@ import (
 	"encoding/xml"
 	"io"
 
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/validation"
 
 	"golang.org/x/net/html/charset"
@@ -31,18 +32,27 @@ type Dependency struct {
 }
 
 type pomStruct struct {
-	XMLName     xml.Name `xml:"project"`
-	GroupID     string   `xml:"groupId"`
-	ArtifactID  string   `xml:"artifactId"`
-	Version     string   `xml:"version"`
-	Name        string   `xml:"name"`
-	Description string   `xml:"description"`
-	URL         string   `xml:"url"`
-	Licenses    []struct {
+	XMLName xml.Name `xml:"project"`
+
+	Parent struct {
+		GroupID    string `xml:"groupId"`
+		ArtifactID string `xml:"artifactId"`
+		Version    string `xml:"version"`
+	} `xml:"parent"`
+
+	GroupID     string `xml:"groupId"`
+	ArtifactID  string `xml:"artifactId"`
+	Version     string `xml:"version"`
+	Name        string `xml:"name"`
+	Description string `xml:"description"`
+	URL         string `xml:"url"`
+
+	Licenses []struct {
 		Name         string `xml:"name"`
 		URL          string `xml:"url"`
 		Distribution string `xml:"distribution"`
 	} `xml:"licenses>license"`
+
 	Dependencies []struct {
 		GroupID    string `xml:"groupId"`
 		ArtifactID string `xml:"artifactId"`
@@ -81,8 +91,16 @@ func ParsePackageMetaData(r io.Reader) (*Metadata, error) {
 		})
 	}
 
+	pomGroupID := pom.GroupID
+	if pomGroupID == "" {
+		// the current module could inherit parent: https://maven.apache.org/pom.html#Inheritance
+		pomGroupID = pom.Parent.GroupID
+	}
+	if pomGroupID == "" {
+		return nil, util.ErrInvalidArgument
+	}
 	return &Metadata{
-		GroupID:      pom.GroupID,
+		GroupID:      pomGroupID,
 		ArtifactID:   pom.ArtifactID,
 		Name:         pom.Name,
 		Description:  pom.Description,
diff --git a/modules/packages/maven/metadata_test.go b/modules/packages/maven/metadata_test.go
index e675467730cc1..2cff290808687 100644
--- a/modules/packages/maven/metadata_test.go
+++ b/modules/packages/maven/metadata_test.go
@@ -7,7 +7,10 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/modules/util"
+
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"golang.org/x/text/encoding/charmap"
 )
 
@@ -86,4 +89,35 @@ func TestParsePackageMetaData(t *testing.T) {
 		assert.NoError(t, err)
 		assert.NotNil(t, m)
 	})
+
+	t.Run("ParentInherit", func(t *testing.T) {
+		pom := `<?xml version="1.0"?>
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.mycompany.app</groupId>
+    <artifactId>my-app</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>submodule1</artifactId>
+</project>
+`
+		m, err := ParsePackageMetaData(strings.NewReader(pom))
+		require.NoError(t, err)
+		require.NotNil(t, m)
+
+		assert.Equal(t, "com.mycompany.app", m.GroupID)
+		assert.Equal(t, "submodule1", m.ArtifactID)
+	})
+
+	t.Run("ParentInherit", func(t *testing.T) {
+		pom := `<?xml version="1.0"?>
+<project>
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId></artifactId>
+</project>
+`
+		_, err := ParsePackageMetaData(strings.NewReader(pom))
+		require.ErrorIs(t, err, util.ErrInvalidArgument)
+	})
 }
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 47ea7137b8250..93c29678b000b 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -465,6 +465,8 @@ func CommonRoutes() *web.Router {
 			r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
 		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/maven", func() {
+			// FIXME: this path design is not right.
+			// It should be `/.../{groupId}/{artifactId}/{version}`, but not `/.../{groupId}-{artifactId}/{version}`
 			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
 			r.Get("/*", maven.DownloadPackageFile)
 			r.Head("/*", maven.ProvidePackageFileHeader)

From 6636b37a9c8054d6d78f3b44b5496e6323f0c7e0 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 25 Dec 2024 14:17:27 +0800
Subject: [PATCH 019/206] Fix Azure blob object `Seek`  (#32974) (#32975)

Backport #32974 by Zettat123

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/storage/azureblob.go      |  2 +-
 modules/storage/azureblob_test.go | 45 +++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/modules/storage/azureblob.go b/modules/storage/azureblob.go
index 96c2525b29b05..837afd0ba62b4 100644
--- a/modules/storage/azureblob.go
+++ b/modules/storage/azureblob.go
@@ -70,7 +70,7 @@ func (a *azureBlobObject) Seek(offset int64, whence int) (int64, error) {
 	case io.SeekCurrent:
 		offset += a.offset
 	case io.SeekEnd:
-		offset = a.Size - offset
+		offset = a.Size + offset
 	default:
 		return 0, errors.New("Seek: invalid whence")
 	}
diff --git a/modules/storage/azureblob_test.go b/modules/storage/azureblob_test.go
index 604870cb98ebf..6905db500815d 100644
--- a/modules/storage/azureblob_test.go
+++ b/modules/storage/azureblob_test.go
@@ -4,6 +4,8 @@
 package storage
 
 import (
+	"bytes"
+	"io"
 	"os"
 	"testing"
 
@@ -54,3 +56,46 @@ func TestAzureBlobStoragePath(t *testing.T) {
 	assert.Equal(t, "base/a", m.buildAzureBlobPath("/a"))
 	assert.Equal(t, "base/a/b", m.buildAzureBlobPath("/a/b/"))
 }
+
+func Test_azureBlobObject(t *testing.T) {
+	if os.Getenv("CI") == "" {
+		t.Skip("azureBlobStorage not present outside of CI")
+		return
+	}
+
+	s, err := NewStorage(setting.AzureBlobStorageType, &setting.Storage{
+		AzureBlobConfig: setting.AzureBlobStorageConfig{
+			// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url
+			Endpoint: "http://devstoreaccount1.azurite.local:10000",
+			// https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#well-known-storage-account-and-key
+			AccountName: "devstoreaccount1",
+			AccountKey:  "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
+			Container:   "test",
+		},
+	})
+	assert.NoError(t, err)
+
+	data := "Q2xTckt6Y1hDOWh0"
+	_, err = s.Save("test.txt", bytes.NewBufferString(data), int64(len(data)))
+	assert.NoError(t, err)
+	obj, err := s.Open("test.txt")
+	assert.NoError(t, err)
+	offset, err := obj.Seek(2, io.SeekStart)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 2, offset)
+	buf1 := make([]byte, 3)
+	read, err := obj.Read(buf1)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 3, read)
+	assert.Equal(t, data[2:5], string(buf1))
+	offset, err = obj.Seek(-5, io.SeekEnd)
+	assert.NoError(t, err)
+	assert.EqualValues(t, len(data)-5, offset)
+	buf2 := make([]byte, 4)
+	read, err = obj.Read(buf2)
+	assert.NoError(t, err)
+	assert.EqualValues(t, 4, read)
+	assert.Equal(t, data[11:15], string(buf2))
+	assert.NoError(t, obj.Close())
+	assert.NoError(t, s.Delete("test.txt"))
+}

From ad1b76540eaf51d26b34f98f7d181f2acaff4041 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 25 Dec 2024 16:03:18 +0800
Subject: [PATCH 020/206] demilestone should not include milestone (#32923)
 (#32979)

Backport #32923 by @lunny

Fix #32887

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 services/issue/milestone.go      | 4 ++++
 services/issue/milestone_test.go | 8 ++++++++
 2 files changed, 12 insertions(+)

diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index ff645744a7ddc..beb6f131a908a 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -59,6 +59,10 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
 		}
 	}
 
+	if issue.MilestoneID == 0 {
+		issue.Milestone = nil
+	}
+
 	return nil
 }
 
diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go
index 42b910166f5a6..bf5abc85b7027 100644
--- a/services/issue/milestone_test.go
+++ b/services/issue/milestone_test.go
@@ -23,6 +23,7 @@ func TestChangeMilestoneAssign(t *testing.T) {
 
 	oldMilestoneID := issue.MilestoneID
 	issue.MilestoneID = 2
+	assert.NoError(t, issue.LoadMilestone(db.DefaultContext))
 	assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID))
 	unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
 		IssueID:        issue.ID,
@@ -31,4 +32,11 @@ func TestChangeMilestoneAssign(t *testing.T) {
 		OldMilestoneID: oldMilestoneID,
 	})
 	unittest.CheckConsistencyFor(t, &issues_model.Milestone{}, &issues_model.Issue{})
+	assert.NotNil(t, issue.Milestone)
+
+	oldMilestoneID = issue.MilestoneID
+	issue.MilestoneID = 0
+	assert.NoError(t, ChangeMilestoneAssign(db.DefaultContext, issue, doer, oldMilestoneID))
+	assert.EqualValues(t, 0, issue.MilestoneID)
+	assert.Nil(t, issue.Milestone)
 }

From a0b65ed17fe683ed48c020a94044265fb36c4f26 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 26 Dec 2024 01:12:18 +0800
Subject: [PATCH 021/206] Do not render truncated links in markdown (#32980)
 (#32983)

Backport #32980 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/html_link.go |  5 +++++
 modules/markup/html_test.go | 10 ++++++++++
 modules/util/string.go      | 17 ++++++++++++++++-
 modules/util/string_test.go |  5 +++++
 modules/util/truncate.go    | 20 ++++----------------
 5 files changed, 40 insertions(+), 17 deletions(-)

diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go
index 5fd38b63cd370..fea82e50ab0a8 100644
--- a/modules/markup/html_link.go
+++ b/modules/markup/html_link.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/modules/markup/common"
+	"code.gitea.io/gitea/modules/util"
 
 	"golang.org/x/net/html"
 	"golang.org/x/net/html/atom"
@@ -171,6 +172,10 @@ func linkProcessor(ctx *RenderContext, node *html.Node) {
 		}
 
 		uri := node.Data[m[0]:m[1]]
+		remaining := node.Data[m[1]:]
+		if util.IsLikelySplitLeftPart(remaining) {
+			return
+		}
 		replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
 		node = node.NextSibling.NextSibling
 	}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 54bd91f3b3b11..f14fe4075c437 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -206,6 +206,16 @@ func TestRender_links(t *testing.T) {
 	test(
 		"ftps://gitea.com",
 		`<p>ftps://gitea.com</p>`)
+
+	t.Run("LinkSplit", func(t *testing.T) {
+		input, _ := util.SplitStringAtByteN("http://10.1.2.3", 12)
+		assert.Equal(t, "http://10…", input)
+		test(input, "<p>http://10…</p>")
+
+		input, _ = util.SplitStringAtByteN("http://10.1.2.3", 13)
+		assert.Equal(t, "http://10.…", input)
+		test(input, "<p>http://10.…</p>")
+	})
 }
 
 func TestRender_email(t *testing.T) {
diff --git a/modules/util/string.go b/modules/util/string.go
index cf50f591c64cb..19cf75b8b35eb 100644
--- a/modules/util/string.go
+++ b/modules/util/string.go
@@ -3,7 +3,10 @@
 
 package util
 
-import "unsafe"
+import (
+	"strings"
+	"unsafe"
+)
 
 func isSnakeCaseUpper(c byte) bool {
 	return 'A' <= c && c <= 'Z'
@@ -95,3 +98,15 @@ func UnsafeBytesToString(b []byte) string {
 func UnsafeStringToBytes(s string) []byte {
 	return unsafe.Slice(unsafe.StringData(s), len(s))
 }
+
+// SplitTrimSpace splits the string at given separator and trims leading and trailing space
+func SplitTrimSpace(input, sep string) []string {
+	input = strings.TrimSpace(input)
+	var stringList []string
+	for _, s := range strings.Split(input, sep) {
+		if s = strings.TrimSpace(s); s != "" {
+			stringList = append(stringList, s)
+		}
+	}
+	return stringList
+}
diff --git a/modules/util/string_test.go b/modules/util/string_test.go
index 0a4a8bbcfbf9d..ff67b5c7d4073 100644
--- a/modules/util/string_test.go
+++ b/modules/util/string_test.go
@@ -45,3 +45,8 @@ func TestToSnakeCase(t *testing.T) {
 		assert.Equal(t, expected, ToSnakeCase(input))
 	}
 }
+
+func TestSplitTrimSpace(t *testing.T) {
+	assert.Equal(t, []string{"a", "b", "c"}, SplitTrimSpace("a\nb\nc", "\n"))
+	assert.Equal(t, []string{"a", "b"}, SplitTrimSpace("\r\na\n\r\nb\n\n", "\n"))
+}
diff --git a/modules/util/truncate.go b/modules/util/truncate.go
index f2edbdc67367b..9f932facc9e71 100644
--- a/modules/util/truncate.go
+++ b/modules/util/truncate.go
@@ -14,6 +14,10 @@ const (
 	asciiEllipsis = "..."
 )
 
+func IsLikelySplitLeftPart(s string) bool {
+	return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
+}
+
 // SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.)
 func SplitStringAtByteN(input string, n int) (left, right string) {
 	if len(input) <= n {
@@ -38,19 +42,3 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
 
 	return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
 }
-
-// SplitTrimSpace splits the string at given separator and trims leading and trailing space
-func SplitTrimSpace(input, sep string) []string {
-	// Trim initial leading & trailing space
-	input = strings.TrimSpace(input)
-	// replace CRLF with LF
-	input = strings.ReplaceAll(input, "\r\n", "\n")
-
-	var stringList []string
-	for _, s := range strings.Split(input, sep) {
-		// trim leading and trailing space
-		stringList = append(stringList, strings.TrimSpace(s))
-	}
-
-	return stringList
-}

From eed0968c371efe90b3f989d3882313572c08600d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 27 Dec 2024 08:35:36 +0800
Subject: [PATCH 022/206] Support org labels when adding labels by label names
 (#32988) (#32996)

Backport #32988 by @Zettat123

Fix #32891

Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 models/fixtures/label.yml                 | 11 +++++++++++
 models/issues/label.go                    | 11 +++++++++++
 routers/api/v1/repo/issue_label.go        | 14 ++++++++++++-
 tests/integration/api_issue_label_test.go | 24 ++++++++++++++---------
 4 files changed, 50 insertions(+), 10 deletions(-)

diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml
index 2242b90dcdc23..acfac74968631 100644
--- a/models/fixtures/label.yml
+++ b/models/fixtures/label.yml
@@ -96,3 +96,14 @@
   num_issues: 0
   num_closed_issues: 0
   archived_unix: 0
+
+-
+  id: 10
+  repo_id: 3
+  org_id: 0
+  name: repo3label1
+  color: '#112233'
+  exclusive: false
+  num_issues: 0
+  num_closed_issues: 0
+  archived_unix: 0
diff --git a/models/issues/label.go b/models/issues/label.go
index d80578193e545..b9d24bbe99166 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -349,6 +349,17 @@ func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []st
 		Find(&labelIDs)
 }
 
+// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org.
+func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) {
+	labelIDs := make([]int64, 0, len(labelNames))
+	return labelIDs, db.GetEngine(ctx).Table("label").
+		Where("org_id = ?", orgID).
+		In("name", labelNames).
+		Asc("name").
+		Cols("id").
+		Find(&labelIDs)
+}
+
 // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
 func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
 	return builder.Select("issue_label.issue_id").
diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go
index cc517619e97d0..6507864a3c53f 100644
--- a/routers/api/v1/repo/issue_label.go
+++ b/routers/api/v1/repo/issue_label.go
@@ -335,6 +335,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
 			labelIDs = append(labelIDs, int64(rv.Float()))
 		case reflect.String:
 			labelNames = append(labelNames, rv.String())
+		default:
+			ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string")
+			return nil, nil, fmt.Errorf("invalid label")
 		}
 	}
 	if len(labelIDs) > 0 && len(labelNames) > 0 {
@@ -342,11 +345,20 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
 		return nil, nil, fmt.Errorf("invalid labels")
 	}
 	if len(labelNames) > 0 {
-		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
+		repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
 			return nil, nil, err
 		}
+		labelIDs = append(labelIDs, repoLabelIDs...)
+		if ctx.Repo.Owner.IsOrganization() {
+			orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
+			if err != nil {
+				ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err)
+				return nil, nil, err
+			}
+			labelIDs = append(labelIDs, orgLabelIDs...)
+		}
 	}
 
 	labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go
index 0e4cd8243bce3..c9cdd46b9a8b3 100644
--- a/tests/integration/api_issue_label_test.go
+++ b/tests/integration/api_issue_label_test.go
@@ -117,27 +117,33 @@ func TestAPIAddIssueLabels(t *testing.T) {
 func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 
-	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
-	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+	issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+	repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID})
+	orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID})
 
-	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
-	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels",
-		repo.OwnerName, repo.Name, issue.Index)
+	user1Session := loginUser(t, "user1")
+	token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue)
+
+	// add the org label and the repo label to the issue
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
-		Labels: []any{"label1", "label2"},
+		Labels: []any{repoLabel.Name, orgLabel.Name},
 	}).AddTokenAuth(token)
 	resp := MakeRequest(t, req, http.StatusOK)
 	var apiLabels []*api.Label
 	DecodeJSON(t, resp, &apiLabels)
 	assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
-
 	var apiLabelNames []string
 	for _, label := range apiLabels {
 		apiLabelNames = append(apiLabelNames, label.Name)
 	}
-	assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"})
+	assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name})
+
+	// delete labels
+	req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusNoContent)
 }
 
 func TestAPIReplaceIssueLabels(t *testing.T) {

From 0137bc4e5cce2163fd2c9709c10a5214345e7cc7 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 27 Dec 2024 19:41:41 +0800
Subject: [PATCH 023/206] Support for email addresses containing uppercase
 characters when activating user account (#32998) (#33001)

Backport #32998 by Zettat123

Fix #32807

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/user/email_address.go              |  8 ++--
 models/user/user.go                       | 46 +++++++++++++++--------
 routers/web/auth/auth.go                  |  4 +-
 routers/web/auth/password.go              |  2 +-
 services/mailer/mail.go                   |  9 +++--
 tests/integration/org_team_invite_test.go |  3 +-
 tests/integration/signup_test.go          | 24 +++++++-----
 7 files changed, 61 insertions(+), 35 deletions(-)

diff --git a/models/user/email_address.go b/models/user/email_address.go
index 5c04909ed7c59..74ba5f617a55d 100644
--- a/models/user/email_address.go
+++ b/models/user/email_address.go
@@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
 	if user := GetVerifyUser(ctx, code); user != nil {
 		// time limit code
 		prefix := code[:base.TimeLimitCodeLength]
-		data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
-
+		opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
+		data := makeTimeLimitCodeHashData(opts, user)
 		if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
 			emailAddress := &EmailAddress{UID: user.ID, Email: email}
 			if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
@@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
 
 	// Activate/deactivate a user's primary email address and account
 	if addr.IsPrimary {
-		user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
+		user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID})
 		if err != nil {
 			return err
-		} else if !exist {
+		} else if !exist || !strings.EqualFold(user.Email, email) {
 			return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
 		}
 
diff --git a/models/user/user.go b/models/user/user.go
index bd92693b6e0ff..97852e916f7de 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() {
 		u.MaxRepoCreation = -1
 	}
 
-	// Organization does not need email
+	// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
+	// This trick could be removed in new releases to display the user inputed email as-is.
 	u.Email = strings.ToLower(u.Email)
 	if !u.IsOrganization() {
 		if len(u.AvatarEmail) == 0 {
@@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string {
 	return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
 }
 
-// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
-func (u *User) GenerateEmailActivateCode(email string) string {
-	code := base.CreateTimeLimitCode(
-		fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
-		setting.Service.ActiveCodeLives, time.Now(), nil)
-
-	// Add tail hex username
-	code += hex.EncodeToString([]byte(u.LowerName))
-	return code
-}
-
 // GetUserFollowers returns range of user's followers.
 func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
 	sess := db.GetEngine(ctx).
@@ -848,12 +838,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
 	return nil
 }
 
-// VerifyUserActiveCode verifies active code when active account
-func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
+type TimeLimitCodePurpose string
+
+const (
+	TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
+	TimeLimitCodeActivateEmail   TimeLimitCodePurpose = "activate_email"
+	TimeLimitCodeResetPassword   TimeLimitCodePurpose = "reset_password"
+)
+
+type TimeLimitCodeOptions struct {
+	Purpose  TimeLimitCodePurpose
+	NewEmail string
+}
+
+func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string {
+	return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands)
+}
+
+// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
+// TODO: need to use cache or db to store it to make sure a code can only be consumed once
+func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string {
+	data := makeTimeLimitCodeHashData(opts, u)
+	code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil)
+	code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username
+	return code
+}
+
+// VerifyUserTimeLimitCode verifies the time-limit code
+func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) {
 	if user = GetVerifyUser(ctx, code); user != nil {
 		// time limit code
 		prefix := code[:base.TimeLimitCodeLength]
-		data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
+		data := makeTimeLimitCodeHashData(opts, user)
 		if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
 			return user
 		}
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3f16da3cddf8e..8ddb9730c3ac3 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -689,7 +689,7 @@ func Activate(ctx *context.Context) {
 	}
 
 	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
-	user := user_model.VerifyUserActiveCode(ctx, code)
+	user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
 	if user == nil { // if code is wrong
 		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
@@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) {
 	}
 
 	// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
-	user := user_model.VerifyUserActiveCode(ctx, code)
+	user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
 	if user == nil { // if code is wrong
 		renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
 		return
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 334d864c6a7ff..5ec9468ecd8c7 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
 	}
 
 	// Fail early, don't frustrate the user
-	u := user_model.VerifyUserActiveCode(ctx, code)
+	u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code)
 	if u == nil {
 		ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
 		return nil, nil
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index ee2c8c0963893..114a21619671b 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
 		// No mail service configured
 		return
 	}
-	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
+	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
+	sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
 }
 
 // SendResetPasswordMail sends a password reset mail to the user
@@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) {
 		return
 	}
 	locale := translation.NewLocale(u.Language)
-	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
+	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
+	sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account")
 }
 
 // SendActivateEmailMail sends confirmation email to confirm new email address
@@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) {
 		return
 	}
 	locale := translation.NewLocale(u.Language)
+	opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email}
 	data := map[string]any{
 		"locale":          locale,
 		"DisplayName":     u.DisplayName(),
 		"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
-		"Code":            u.GenerateEmailActivateCode(email),
+		"Code":            user_model.GenerateUserTimeLimitCode(opts, u),
 		"Email":           email,
 		"Language":        locale.Language(),
 	}
diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go
index 274fde4085054..4c1053702e652 100644
--- a/tests/integration/org_team_invite_test.go
+++ b/tests/integration/org_team_invite_test.go
@@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
 	user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
 	assert.NoError(t, err)
 
-	activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
+	activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
+	activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode)
 	req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
 		"password": "examplePassword!1",
 	})
diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go
index e9a05201eefbd..e86851352e090 100644
--- a/tests/integration/signup_test.go
+++ b/tests/integration/signup_test.go
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
@@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) {
 
 	// try to sign up and send the activation email
 	req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
-		"user_name": "test-user-1",
-		"email":     "email-1@example.com",
+		"user_name": "Test-User-1",
+		"email":     "EmAiL-1@example.com",
 		"password":  "password1",
 		"retype":    "password1",
 	})
 	resp := MakeRequest(t, req, http.StatusOK)
-	assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
+	assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>EmAiL-1@example.com</b>.`)
 
 	// access "user/activate" means trying to re-send the activation email
 	session := loginUserWithPassword(t, "test-user-1", "password1")
 	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
 	assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
 
-	// access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email
+	// access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email
 	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
 	assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
 
 	// post to "user/activate" with a new email
 	session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
-	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
 	assert.Equal(t, "email-changed@example.com", user.Email)
 	email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
 	assert.False(t, email.IsActivated)
 	assert.True(t, email.IsPrimary)
 
+	// generate an activation code from lower-cased email
+	activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
+	// and update the user email to case-sensitive, it shouldn't affect the verification later
+	_, _ = db.Exec(db.DefaultContext, "UPDATE `user` SET email=? WHERE id=?", "EmAiL-changed@example.com", user.ID)
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
+	assert.Equal(t, "EmAiL-changed@example.com", user.Email)
+
 	// access "user/activate" with a valid activation code, then get the "verify password" page
-	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
-	activationCode := user.GenerateEmailActivateCode(user.Email)
 	resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
 	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
 
@@ -138,7 +144,7 @@ func TestSignupEmailActive(t *testing.T) {
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	assert.Contains(t, resp.Body.String(), `Your password does not match`)
 	assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
-	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
 	assert.False(t, user.IsActive)
 
 	// then use a correct password, the user should be activated
@@ -148,6 +154,6 @@ func TestSignupEmailActive(t *testing.T) {
 	})
 	resp = session.MakeRequest(t, req, http.StatusSeeOther)
 	assert.Equal(t, "/", test.RedirectURL(resp))
-	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
+	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
 	assert.True(t, user.IsActive)
 }

From f35ab5cd52871dd0ae3b175a54b4b63e1525a4aa Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 28 Dec 2024 11:34:28 +0800
Subject: [PATCH 024/206] Fix Agit pull request permission check (#32999)
 (#33005)

Backport #32999 by @a1012112796

user with read permission should also can create agit flow pull request.
looks this logic was broken in
https://github.com/go-gitea/gitea/pull/31033 this pull request try fix
it and add test code.

Signed-off-by: a1012112796 <1012112796@qq.com>
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/pull/pull.go                 |  3 ++-
 tests/integration/pull_create_test.go | 19 +++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/services/pull/pull.go b/services/pull/pull.go
index 3362cb97ff70f..98bd28a4a5441 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -65,7 +65,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
 	}
 
 	// user should be a collaborator or a member of the organization for base repo
-	if !issue.Poster.IsAdmin {
+	canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit
+	if !canCreate {
 		canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
 		if err != nil {
 			return err
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go
index 9812d2073d1e9..4bc2a1da9ab5f 100644
--- a/tests/integration/pull_create_test.go
+++ b/tests/integration/pull_create_test.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
@@ -226,3 +227,21 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) {
 		assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url)
 	})
 }
+
+func TestCreateAgitPullWithReadPermission(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		dstPath := t.TempDir()
+
+		u.Path = "user2/repo1.git"
+		u.User = url.UserPassword("user4", userPassword)
+
+		t.Run("Clone", doGitClone(dstPath, u))
+
+		t.Run("add commit", doGitAddSomeCommits(dstPath, "master"))
+
+		t.Run("do agit pull create", func(t *testing.T) {
+			err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath})
+			assert.NoError(t, err)
+		})
+	})
+}

From f27128bf9444146f387bbfb8566a4f935b496b27 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 28 Dec 2024 12:34:53 +0800
Subject: [PATCH 025/206] fix scoped label ui when contains emoji (#33007)
 (#33014)

Backport #33007 by metiftikci

Co-authored-by: metiftikci <metiftikci@hotmail.com>
---
 web_src/css/repo.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 6fdc9ec2a88e2..0cac3e9537580 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1622,7 +1622,7 @@ td .commit-summary {
   align-items: center;
 }
 
-.labels-list .label {
+.labels-list .label, .scope-parent > .label {
   padding: 0 6px;
   min-height: 20px;
   line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */

From c102e344f3e90157b66ec676969a4c20a10509e6 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 28 Dec 2024 13:04:07 +0800
Subject: [PATCH 026/206] Fix bug on activities (#33008) (#33016)

Backport #33008 by @lunny

A repository with no issue will display a random number on activities
page. This is caused by wrong usage of `And` and `Or`.


![9cdbbf81d50aa5d9bd16604e0dab5eb0](https://github.com/user-attachments/assets/828cebdc-bd35-4716-a58c-c1b43ddf8bf0)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/activities/repo_activity.go | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go
index 3ffad035b7a04..3ccdbd47d3d7a 100644
--- a/models/activities/repo_activity.go
+++ b/models/activities/repo_activity.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 
+	"xorm.io/builder"
 	"xorm.io/xorm"
 )
 
@@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *
 func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
 	sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
 		And("issue.is_pull = ?", false).
-		And("issue.created_unix >= ?", fromTime.Unix()).
-		Or("issue.closed_unix >= ?", fromTime.Unix())
+		And(builder.Or(
+			builder.Gte{"issue.created_unix": fromTime.Unix()},
+			builder.Gte{"issue.closed_unix": fromTime.Unix()},
+		))
 
 	return sess
 }

From 7f0050cf3991b4bfb2ab80ab02de40dc98cf75a1 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 00:58:40 +0800
Subject: [PATCH 027/206] Fix review code comment avatar alignment (#33031)
 (#33032)

Backport #33031 by henrygoodman

Fixes #33017

Co-authored-by: Henry Goodman <79623665+henrygoodman@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/issue/view_content/comments.tmpl | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index 9fdbf45939060..2e1a67edcc4ff 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -365,8 +365,9 @@
 					{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
 					{{if not .OriginalAuthor}}
 					{{/* Some timeline avatars need a offset to correctly align with their speech bubble.
-						The condition depends on whether the comment has contents/attachments or reviews */}}
-					<a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
+						The condition depends on whether the comment has contents/attachments,
+						review's comment is also controlled/rendered by issue comment's Content field */}}
+					<a class="timeline-avatar{{if or .Content .Attachments}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
 						{{ctx.AvatarUtils.Avatar .Poster 40}}
 					</a>
 					{{end}}

From ce6464123f731cbd333b68d8805e8bf7442896b4 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 30 Dec 2024 01:54:23 +0800
Subject: [PATCH 028/206] fix toggle commit body button ui when latest commit
 message is long (#32997) (#33034)

backport #32997 and #33002
---
 package-lock.json                   | 174 ++--------------------------
 package.json                        |   2 +-
 templates/repo/commit_statuses.tmpl |   4 +-
 templates/repo/latest_commit.tmpl   |   4 +-
 web_src/css/repo.css                |   5 +
 5 files changed, 18 insertions(+), 171 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 8755cfe06f702..1f67b69a70f1a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -87,7 +87,7 @@
         "eslint": "8.57.0",
         "eslint-import-resolver-typescript": "3.7.0",
         "eslint-plugin-array-func": "4.0.0",
-        "eslint-plugin-github": "5.1.3",
+        "eslint-plugin-github": "5.0.2",
         "eslint-plugin-import-x": "4.5.0",
         "eslint-plugin-no-jquery": "3.1.0",
         "eslint-plugin-no-use-extend-native": "0.5.0",
@@ -8241,166 +8241,35 @@
       }
     },
     "node_modules/eslint-plugin-github": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.1.3.tgz",
-      "integrity": "sha512-/0lyEqLLodXW3p+D9eYtmEp6e9DcJmV5FhnE9wNWV1bcqyShuZFXn5kOeJIvxSbFbdbrKiNO8zFiV/VXeSpRSw==",
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.2.tgz",
+      "integrity": "sha512-nMdzWJQ5CimjQDY6SFeJ0KIXuNFf0dgDWEd4eP3UWfuTuP/dXcZJDg7MQRvAFt743T1zUi4+/HdOihfu8xJkLA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@eslint/compat": "^1.2.3",
-        "@eslint/eslintrc": "^3.1.0",
-        "@eslint/js": "^9.14.0",
         "@github/browserslist-config": "^1.0.0",
         "@typescript-eslint/eslint-plugin": "^8.0.0",
         "@typescript-eslint/parser": "^8.0.0",
         "aria-query": "^5.3.0",
         "eslint-config-prettier": ">=8.0.0",
-        "eslint-plugin-escompat": "^3.11.3",
+        "eslint-plugin-escompat": "^3.3.3",
         "eslint-plugin-eslint-comments": "^3.2.0",
         "eslint-plugin-filenames": "^1.3.2",
         "eslint-plugin-i18n-text": "^1.0.1",
         "eslint-plugin-import": "^2.25.2",
         "eslint-plugin-jsx-a11y": "^6.7.1",
         "eslint-plugin-no-only-tests": "^3.0.0",
-        "eslint-plugin-prettier": "^5.2.1",
+        "eslint-plugin-prettier": "^5.0.0",
         "eslint-rule-documentation": ">=1.0.0",
-        "globals": "^15.12.0",
         "jsx-ast-utils": "^3.3.2",
         "prettier": "^3.0.0",
-        "svg-element-attributes": "^1.3.1",
-        "typescript-eslint": "^8.14.0"
+        "svg-element-attributes": "^1.3.1"
       },
       "bin": {
         "eslint-ignore-errors": "bin/eslint-ignore-errors.js"
       },
       "peerDependencies": {
-        "eslint": "^8 || ^9"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz",
-      "integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "peerDependencies": {
-        "eslint": "^9.10.0"
-      },
-      "peerDependenciesMeta": {
-        "eslint": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
-      "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^10.0.1",
-        "globals": "^14.0.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
-        "strip-json-comments": "^3.1.1"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc/node_modules/globals": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
-      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/@eslint/js": {
-      "version": "9.16.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
-      "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/globals": {
-      "version": "15.13.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz",
-      "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=18"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint-plugin-github/node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/eslint-plugin-github/node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
+        "eslint": "^8.0.1"
       }
     },
     "node_modules/eslint-plugin-i18n-text": {
@@ -15178,33 +15047,6 @@
         "node": ">=14.17"
       }
     },
-    "node_modules/typescript-eslint": {
-      "version": "8.17.0",
-      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.17.0.tgz",
-      "integrity": "sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@typescript-eslint/eslint-plugin": "8.17.0",
-        "@typescript-eslint/parser": "8.17.0",
-        "@typescript-eslint/utils": "8.17.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/typescript-eslint"
-      },
-      "peerDependencies": {
-        "eslint": "^8.57.0 || ^9.0.0"
-      },
-      "peerDependenciesMeta": {
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/typo-js": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
diff --git a/package.json b/package.json
index 61e65c1f43eee..f318cbf5bba66 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,7 @@
     "eslint": "8.57.0",
     "eslint-import-resolver-typescript": "3.7.0",
     "eslint-plugin-array-func": "4.0.0",
-    "eslint-plugin-github": "5.1.3",
+    "eslint-plugin-github": "5.0.2",
     "eslint-plugin-import-x": "4.5.0",
     "eslint-plugin-no-jquery": "3.1.0",
     "eslint-plugin-no-use-extend-native": "0.5.0",
diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl
index f451ac06a1a9e..a6f75584a31f4 100644
--- a/templates/repo/commit_statuses.tmpl
+++ b/templates/repo/commit_statuses.tmpl
@@ -1,10 +1,10 @@
 {{if .Statuses}}
 	{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
-		<a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
+		<a class="flex-text-inline tw-no-underline {{.AdditionalClasses}}" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
 			{{template "repo/commit_status" .Status}}
 		</a>
 	{{else}}
-		<span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
+		<span class="flex-text-inline {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
 			{{template "repo/commit_status" .Status}}
 		</span>
 	{{end}}
diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl
index 34a5df8f7762e..b815c7af700d9 100644
--- a/templates/repo/latest_commit.tmpl
+++ b/templates/repo/latest_commit.tmpl
@@ -2,7 +2,7 @@
 	…
 {{else}}
 	{{if .LatestCommitUser}}
-		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
+		{{ctx.AvatarUtils.Avatar .LatestCommitUser 24}}
 		{{if and .LatestCommitUser.FullName DefaultShowFullName}}
 			<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
 		{{else}}
@@ -10,7 +10,7 @@
 		{{end}}
 	{{else}}
 		{{if .LatestCommit.Author}}
-			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}}
+			{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}}
 			<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
 		{{end}}
 	{{end}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 0cac3e9537580..595acd288f1ae 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -120,6 +120,7 @@ td .commit-summary {
   align-items: center;
   overflow: hidden;
   text-overflow: ellipsis;
+  gap: 0.25em;
 }
 
 @media (max-width: 767.98px) {
@@ -1701,6 +1702,10 @@ tbody.commit-list {
   white-space: nowrap;
 }
 
+.latest-commit .message-wrapper {
+  max-width: calc(100% - 2.5rem);
+}
+
 /* in the commit list, messages can wrap so we can use inline */
 .commit-list .message-wrapper {
   display: inline;

From 74159a88552ad19fd7b1e7c00075b16426854af2 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 02:24:22 +0800
Subject: [PATCH 029/206] Fix templating in pull request comparison (#33025)
 (#33038)

Backport #33025 by TheFox0x7

Adds test for expected behavior

Closes: https://github.com/go-gitea/gitea/issues/33013

---

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
---
 templates/base/alert_details.tmpl     | 10 ++--
 tests/integration/pull_create_test.go | 68 +++++++++++++++++++++++++++
 2 files changed, 75 insertions(+), 3 deletions(-)

diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl
index 6801c8240fd7d..6d4c1fb2db220 100644
--- a/templates/base/alert_details.tmpl
+++ b/templates/base/alert_details.tmpl
@@ -1,7 +1,11 @@
 {{.Message}}
+{{if .Details}}
 <details>
 	<summary>{{.Summary}}</summary>
-	<code>
-		{{.Details | SanitizeHTML}}
-	</code>
+	<code>{{.Details | SanitizeHTML}}</code>
 </details>
+{{else}}
+<div>
+	{{.Summary}}
+</div>
+{{end}}
diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go
index 4bc2a1da9ab5f..162ea532c8075 100644
--- a/tests/integration/pull_create_test.go
+++ b/tests/integration/pull_create_test.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
@@ -83,6 +84,30 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b
 	return resp
 }
 
+func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, baseRepoName, baseBranch, headRepoOwner, headRepoName, headBranch, title string) *httptest.ResponseRecorder {
+	headCompare := headBranch
+	if headRepoOwner != "" {
+		if headRepoName != "" {
+			headCompare = fmt.Sprintf("%s/%s:%s", headRepoOwner, headRepoName, headBranch)
+		} else {
+			headCompare = fmt.Sprintf("%s:%s", headRepoOwner, headBranch)
+		}
+	}
+	req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/compare/%s...%s", baseRepoOwner, baseRepoName, baseBranch, headCompare))
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	// Submit the form for creating the pull
+	htmlDoc := NewHTMLParser(t, resp.Body)
+	link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
+	assert.True(t, exists, "The template has changed")
+	req = NewRequestWithValues(t, "POST", link, map[string]string{
+		"_csrf": htmlDoc.GetCSRF(),
+		"title": title,
+	})
+	resp = session.MakeRequest(t, req, http.StatusBadRequest)
+	return resp
+}
+
 func TestPullCreate(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 		session := loginUser(t, "user1")
@@ -245,3 +270,46 @@ func TestCreateAgitPullWithReadPermission(t *testing.T) {
 		})
 	})
 }
+
+/*
+Setup: user2 has repository, user1 forks it
+---
+
+1. User2 blocks User1
+2. User1 adds changes to fork
+3. User1 attempts to create a pull request
+4. User1 sees alert that the action is not allowed because of the block
+*/
+func TestCreatePullWhenBlocked(t *testing.T) {
+	RepoOwner := "user2"
+	ForkOwner := "user16"
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		// Setup
+		// User1 forks repo1 from User2
+		sessionFork := loginUser(t, ForkOwner)
+		testRepoFork(t, sessionFork, RepoOwner, "repo1", ForkOwner, "forkrepo1", "")
+
+		// 1. User2 blocks user1
+		// sessionBase := loginUser(t, "user2")
+		token := getUserToken(t, RepoOwner, auth_model.AccessTokenScopeWriteUser)
+
+		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNotFound)
+		req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		// 2. User1 adds changes to fork
+		testEditFile(t, sessionFork, ForkOwner, "forkrepo1", "master", "README.md", "Hello, World (Edited)\n")
+
+		// 3. User1 attempts to create a pull request
+		testPullCreateFailure(t, sessionFork, RepoOwner, "repo1", "master", ForkOwner, "forkrepo1", "master", "This is a pull title")
+
+		// Teardown
+		// Unblock user
+		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusNoContent)
+	})
+}

From ce6a60a38b522c411fa347241262141095bdcae2 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 30 Dec 2024 10:49:49 +0800
Subject: [PATCH 030/206] Refactor testfixtures (#33028)

Partial backport of #33024
---
 assets/go-licenses.json            |  50 -------
 go.mod                             |   9 --
 go.sum                             |  58 ---------
 models/fixtures/action_run_job.yml |   2 +-
 models/fixtures/protected_tag.yml  |  12 +-
 models/unittest/fixtures.go        |  41 +-----
 models/unittest/fixtures_loader.go | 201 +++++++++++++++++++++++++++++
 modules/system/appstate_test.go    |   6 +-
 8 files changed, 215 insertions(+), 164 deletions(-)
 create mode 100644 models/unittest/fixtures_loader.go

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 64c3b8b51c76d..d7b76eef07f44 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -104,16 +104,6 @@
     "path": "github.com/Azure/go-ntlmssp/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Microsoft\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
   },
-  {
-    "name": "github.com/ClickHouse/ch-go",
-    "path": "github.com/ClickHouse/ch-go/LICENSE",
-    "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2023 ClickHouse, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
-  {
-    "name": "github.com/ClickHouse/clickhouse-go/v2",
-    "path": "github.com/ClickHouse/clickhouse-go/v2/LICENSE",
-    "licenseText": "Copyright 2016-2023 ClickHouse, Inc.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2016-2023 ClickHouse, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "github.com/DataDog/zstd",
     "path": "github.com/DataDog/zstd/LICENSE",
@@ -519,16 +509,6 @@
     "path": "github.com/go-enry/go-enry/v2/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
   },
-  {
-    "name": "github.com/go-faster/city",
-    "path": "github.com/go-faster/city/LICENSE",
-    "licenseText": "MIT License\n\nCopyright (c) 2018 tenfy\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
-  },
-  {
-    "name": "github.com/go-faster/errors",
-    "path": "github.com/go-faster/errors/LICENSE",
-    "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/go-fed/httpsig",
     "path": "github.com/go-fed/httpsig/LICENSE",
@@ -569,11 +549,6 @@
     "path": "github.com/go-sql-driver/mysql/LICENSE",
     "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in \n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
   },
-  {
-    "name": "github.com/go-testfixtures/testfixtures/v3",
-    "path": "github.com/go-testfixtures/testfixtures/v3/LICENSE",
-    "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Andrey Nering\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
-  },
   {
     "name": "github.com/go-webauthn/webauthn",
     "path": "github.com/go-webauthn/webauthn/LICENSE",
@@ -964,11 +939,6 @@
     "path": "github.com/opencontainers/image-spec/specs-go/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright 2016 The Linux Foundation.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
-  {
-    "name": "github.com/paulmach/orb",
-    "path": "github.com/paulmach/orb/LICENSE.md",
-    "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017 Paul Mach\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
-  },
   {
     "name": "github.com/pierrec/lz4/v4",
     "path": "github.com/pierrec/lz4/v4/LICENSE",
@@ -1059,21 +1029,11 @@
     "path": "github.com/sassoftware/go-rpmutils/LICENSE",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
-  {
-    "name": "github.com/segmentio/asm",
-    "path": "github.com/segmentio/asm/LICENSE",
-    "licenseText": "MIT License\n\nCopyright (c) 2021 Segment\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
-  },
   {
     "name": "github.com/sergi/go-diff/diffmatchpatch",
     "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE",
     "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n"
   },
-  {
-    "name": "github.com/shopspring/decimal",
-    "path": "github.com/shopspring/decimal/LICENSE",
-    "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Spring, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n- Based on https://github.com/oguzbilgic/fpd, which has the following license:\n\"\"\"\nThe MIT License (MIT)\n\nCopyright (c) 2013 Oguz Bilgic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\n"
-  },
   {
     "name": "github.com/sirupsen/logrus",
     "path": "github.com/sirupsen/logrus/LICENSE",
@@ -1184,16 +1144,6 @@
     "path": "go.etcd.io/bbolt/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Ben Johnson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
   },
-  {
-    "name": "go.opentelemetry.io/otel",
-    "path": "go.opentelemetry.io/otel/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
-  {
-    "name": "go.opentelemetry.io/otel/trace",
-    "path": "go.opentelemetry.io/otel/trace/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
   {
     "name": "go.uber.org/atomic",
     "path": "go.uber.org/atomic/LICENSE.txt",
diff --git a/go.mod b/go.mod
index 9b99face841ae..96e6b3411345b 100644
--- a/go.mod
+++ b/go.mod
@@ -61,7 +61,6 @@ require (
 	github.com/go-redsync/redsync/v4 v4.13.0
 	github.com/go-sql-driver/mysql v1.8.1
 	github.com/go-swagger/go-swagger v0.31.0
-	github.com/go-testfixtures/testfixtures/v3 v3.11.0
 	github.com/go-webauthn/webauthn v0.11.2
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
@@ -145,8 +144,6 @@ require (
 	filippo.io/edwards25519 v1.1.0 // indirect
 	git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
-	github.com/ClickHouse/ch-go v0.63.1 // indirect
-	github.com/ClickHouse/clickhouse-go/v2 v2.24.0 // indirect
 	github.com/DataDog/zstd v1.5.6 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.3.0 // indirect
@@ -204,8 +201,6 @@ require (
 	github.com/go-ap/errors v0.0.0-20240910140019-1e9d33cc1568 // indirect
 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
 	github.com/go-enry/go-oniguruma v1.2.1 // indirect
-	github.com/go-faster/city v1.0.1 // indirect
-	github.com/go-faster/errors v0.7.1 // indirect
 	github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/go-ini/ini v1.67.0 // indirect
@@ -270,7 +265,6 @@ require (
 	github.com/oklog/ulid v1.3.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/onsi/ginkgo v1.16.5 // indirect
-	github.com/paulmach/orb v0.11.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
@@ -285,7 +279,6 @@ require (
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sagikazarmark/locafero v0.6.0 // indirect
 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
-	github.com/segmentio/asm v1.2.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -310,8 +303,6 @@ require (
 	github.com/zeebo/blake3 v0.2.4 // indirect
 	go.etcd.io/bbolt v1.3.11 // indirect
 	go.mongodb.org/mongo-driver v1.17.1 // indirect
-	go.opentelemetry.io/otel v1.31.0 // indirect
-	go.opentelemetry.io/otel/trace v1.31.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
diff --git a/go.sum b/go.sum
index 93b2f9fa99910..e4698a756d3bc 100644
--- a/go.sum
+++ b/go.sum
@@ -59,10 +59,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM=
-github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0=
-github.com/ClickHouse/clickhouse-go/v2 v2.24.0 h1:L/n/pVVpk95KtkHOiKuSnO7cu2ckeW4gICbbOh5qs74=
-github.com/ClickHouse/clickhouse-go/v2 v2.24.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ=
 github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY=
 github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
 github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
@@ -237,8 +233,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
 github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
-github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
-github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA=
@@ -317,10 +311,6 @@ github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqV
 github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
 github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
 github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
-github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
-github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
-github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
-github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -372,8 +362,6 @@ github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
 github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/go-testfixtures/testfixtures/v3 v3.11.0 h1:XxQr8AnPORcZkyNd7go5UNLPD3dULN8ixYISlzrlfEQ=
-github.com/go-testfixtures/testfixtures/v3 v3.11.0/go.mod h1:THmudHF1Ixq++J2/UodcJpxUphfyEd77m83TvDtryqE=
 github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
 github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
 github.com/go-webauthn/x v0.1.15 h1:eG1OhggBJTkDE8gUeOlGRbRe8E/PSVG26YG4AyFbwkU=
@@ -385,7 +373,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
 github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
 github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
 github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
@@ -410,7 +397,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -497,22 +483,6 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
-github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
-github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
-github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
-github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
-github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
-github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
-github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
-github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
-github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
-github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
-github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
-github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
-github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -535,8 +505,6 @@ github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99r
 github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -550,11 +518,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4=
 github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
 github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
@@ -629,7 +594,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
 github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -670,9 +634,6 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
 github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
-github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
-github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
-github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
@@ -735,8 +696,6 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6Ng
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
 github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=
 github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=
-github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
-github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
@@ -783,7 +742,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -797,7 +755,6 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
-github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
 github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
@@ -823,9 +780,6 @@ github.com/xanzy/go-gitlab v0.112.0 h1:6Z0cqEooCvBMfBIHw+CgO4AKGRV8na/9781xOb0+D
 github.com/xanzy/go-gitlab v0.112.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
-github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
-github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -842,9 +796,7 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
 github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -863,13 +815,8 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l
 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
 go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
-go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
 go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
 go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
-go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
-go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
-go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
-go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@@ -941,7 +888,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -1022,10 +968,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
@@ -1046,8 +990,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
 google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml
index 9b6f5b9a887b4..8837e6ec2d80d 100644
--- a/models/fixtures/action_run_job.yml
+++ b/models/fixtures/action_run_job.yml
@@ -64,7 +64,7 @@
   name: job2
   attempt: 1
   job_id: job2
-  needs: [job1]
+  needs: '["job1"]'
   task_id: 51
   status: 5
   started: 1683636528
diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml
index dbec52c0c23c9..1944e7bd84ca4 100644
--- a/models/fixtures/protected_tag.yml
+++ b/models/fixtures/protected_tag.yml
@@ -2,23 +2,23 @@
   id: 1
   repo_id: 4
   name_pattern: /v.+/
-  allowlist_user_i_ds: []
-  allowlist_team_i_ds: []
+  allowlist_user_i_ds: "[]"
+  allowlist_team_i_ds: "[]"
   created_unix: 1715596037
   updated_unix: 1715596037
 -
   id: 2
   repo_id: 1
   name_pattern: v-*
-  allowlist_user_i_ds: []
-  allowlist_team_i_ds: []
+  allowlist_user_i_ds: "[]"
+  allowlist_team_i_ds: "[]"
   created_unix: 1715596037
   updated_unix: 1715596037
 -
   id: 3
   repo_id: 1
   name_pattern: v-1.1
-  allowlist_user_i_ds: [2]
-  allowlist_team_i_ds: []
+  allowlist_user_i_ds: "[2]"
+  allowlist_team_i_ds: "[]"
   created_unix: 1715596037
   updated_unix: 1715596037
diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go
index 4dde5410d6d8a..8acd72d465fc0 100644
--- a/models/unittest/fixtures.go
+++ b/models/unittest/fixtures.go
@@ -6,19 +6,21 @@ package unittest
 
 import (
 	"fmt"
-	"os"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/auth/password/hash"
 	"code.gitea.io/gitea/modules/setting"
 
-	"github.com/go-testfixtures/testfixtures/v3"
 	"xorm.io/xorm"
 	"xorm.io/xorm/schemas"
 )
 
-var fixturesLoader *testfixtures.Loader
+type FixturesLoader interface {
+	Load() error
+}
+
+var fixturesLoader FixturesLoader
 
 // GetXORMEngine gets the XORM engine
 func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
@@ -31,38 +33,7 @@ func GetXORMEngine(engine ...*xorm.Engine) (x *xorm.Engine) {
 // InitFixtures initialize test fixtures for a test database
 func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
 	e := GetXORMEngine(engine...)
-	var fixtureOptionFiles func(*testfixtures.Loader) error
-	if opts.Dir != "" {
-		fixtureOptionFiles = testfixtures.Directory(opts.Dir)
-	} else {
-		fixtureOptionFiles = testfixtures.Files(opts.Files...)
-	}
-	dialect := "unknown"
-	switch e.Dialect().URI().DBType {
-	case schemas.POSTGRES:
-		dialect = "postgres"
-	case schemas.MYSQL:
-		dialect = "mysql"
-	case schemas.MSSQL:
-		dialect = "mssql"
-	case schemas.SQLITE:
-		dialect = "sqlite3"
-	default:
-		fmt.Println("Unsupported RDBMS for integration tests")
-		os.Exit(1)
-	}
-	loaderOptions := []func(loader *testfixtures.Loader) error{
-		testfixtures.Database(e.DB().DB),
-		testfixtures.Dialect(dialect),
-		testfixtures.DangerousSkipTestDatabaseCheck(),
-		fixtureOptionFiles,
-	}
-
-	if e.Dialect().URI().DBType == schemas.POSTGRES {
-		loaderOptions = append(loaderOptions, testfixtures.SkipResetSequences())
-	}
-
-	fixturesLoader, err = testfixtures.New(loaderOptions...)
+	fixturesLoader, err = NewFixturesLoader(e, opts)
 	if err != nil {
 		return err
 	}
diff --git a/models/unittest/fixtures_loader.go b/models/unittest/fixtures_loader.go
new file mode 100644
index 0000000000000..14686caf63de4
--- /dev/null
+++ b/models/unittest/fixtures_loader.go
@@ -0,0 +1,201 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package unittest
+
+import (
+	"database/sql"
+	"encoding/hex"
+	"fmt"
+	"os"
+	"path/filepath"
+	"slices"
+	"strings"
+
+	"gopkg.in/yaml.v3"
+	"xorm.io/xorm"
+	"xorm.io/xorm/schemas"
+)
+
+type fixtureItem struct {
+	tableName       string
+	tableNameQuoted string
+	sqlInserts      []string
+	sqlInsertArgs   [][]any
+
+	mssqlHasIdentityColumn bool
+}
+
+type fixturesLoaderInternal struct {
+	db               *sql.DB
+	dbType           schemas.DBType
+	files            []string
+	fixtures         map[string]*fixtureItem
+	quoteObject      func(string) string
+	paramPlaceholder func(idx int) string
+}
+
+func (f *fixturesLoaderInternal) mssqlTableHasIdentityColumn(db *sql.DB, tableName string) (bool, error) {
+	row := db.QueryRow(`SELECT COUNT(*) FROM sys.identity_columns WHERE OBJECT_ID = OBJECT_ID(?)`, tableName)
+	var count int
+	if err := row.Scan(&count); err != nil {
+		return false, err
+	}
+	return count > 0, nil
+}
+
+func (f *fixturesLoaderInternal) preprocessFixtureRow(row []map[string]any) (err error) {
+	for _, m := range row {
+		for k, v := range m {
+			if s, ok := v.(string); ok {
+				if strings.HasPrefix(s, "0x") {
+					if m[k], err = hex.DecodeString(s[2:]); err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func (f *fixturesLoaderInternal) prepareFixtureItem(file string) (_ *fixtureItem, err error) {
+	fixture := &fixtureItem{}
+	fixture.tableName, _, _ = strings.Cut(filepath.Base(file), ".")
+	fixture.tableNameQuoted = f.quoteObject(fixture.tableName)
+
+	if f.dbType == schemas.MSSQL {
+		fixture.mssqlHasIdentityColumn, err = f.mssqlTableHasIdentityColumn(f.db, fixture.tableName)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	data, err := os.ReadFile(file)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file %q: %w", file, err)
+	}
+
+	var rows []map[string]any
+	if err = yaml.Unmarshal(data, &rows); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal yaml data from %q: %w", file, err)
+	}
+	if err = f.preprocessFixtureRow(rows); err != nil {
+		return nil, fmt.Errorf("failed to preprocess fixture rows from %q: %w", file, err)
+	}
+
+	var sqlBuf []byte
+	var sqlArguments []any
+	for _, row := range rows {
+		sqlBuf = append(sqlBuf, fmt.Sprintf("INSERT INTO %s (", fixture.tableNameQuoted)...)
+		for k, v := range row {
+			sqlBuf = append(sqlBuf, f.quoteObject(k)...)
+			sqlBuf = append(sqlBuf, ","...)
+			sqlArguments = append(sqlArguments, v)
+		}
+		sqlBuf = sqlBuf[:len(sqlBuf)-1]
+		sqlBuf = append(sqlBuf, ") VALUES ("...)
+		paramIdx := 1
+		for range row {
+			sqlBuf = append(sqlBuf, f.paramPlaceholder(paramIdx)...)
+			sqlBuf = append(sqlBuf, ',')
+			paramIdx++
+		}
+		sqlBuf[len(sqlBuf)-1] = ')'
+		fixture.sqlInserts = append(fixture.sqlInserts, string(sqlBuf))
+		fixture.sqlInsertArgs = append(fixture.sqlInsertArgs, slices.Clone(sqlArguments))
+		sqlBuf = sqlBuf[:0]
+		sqlArguments = sqlArguments[:0]
+	}
+	return fixture, nil
+}
+
+func (f *fixturesLoaderInternal) loadFixtures(tx *sql.Tx, file string) (err error) {
+	fixture := f.fixtures[file]
+	if fixture == nil {
+		if fixture, err = f.prepareFixtureItem(file); err != nil {
+			return err
+		}
+		f.fixtures[file] = fixture
+	}
+
+	_, err = tx.Exec(fmt.Sprintf("DELETE FROM %s", fixture.tableNameQuoted)) // sqlite3 doesn't support truncate
+	if err != nil {
+		return err
+	}
+
+	if fixture.mssqlHasIdentityColumn {
+		_, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s ON", fixture.tableNameQuoted))
+		if err != nil {
+			return err
+		}
+		defer func() { _, err = tx.Exec(fmt.Sprintf("SET IDENTITY_INSERT %s OFF", fixture.tableNameQuoted)) }()
+	}
+	for i := range fixture.sqlInserts {
+		_, err = tx.Exec(fixture.sqlInserts[i], fixture.sqlInsertArgs[i]...)
+	}
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (f *fixturesLoaderInternal) Load() error {
+	tx, err := f.db.Begin()
+	if err != nil {
+		return err
+	}
+	defer func() { _ = tx.Rollback() }()
+
+	for _, file := range f.files {
+		if err := f.loadFixtures(tx, file); err != nil {
+			return fmt.Errorf("failed to load fixtures from %s: %w", file, err)
+		}
+	}
+	return tx.Commit()
+}
+
+func FixturesFileFullPaths(dir string, files []string) ([]string, error) {
+	if files != nil && len(files) == 0 {
+		return nil, nil // load nothing
+	}
+	files = slices.Clone(files)
+	if len(files) == 0 {
+		entries, err := os.ReadDir(dir)
+		if err != nil {
+			return nil, err
+		}
+		for _, e := range entries {
+			files = append(files, e.Name())
+		}
+	}
+	for i, file := range files {
+		if !filepath.IsAbs(file) {
+			files[i] = filepath.Join(dir, file)
+		}
+	}
+	return files, nil
+}
+
+func NewFixturesLoader(x *xorm.Engine, opts FixturesOptions) (FixturesLoader, error) {
+	files, err := FixturesFileFullPaths(opts.Dir, opts.Files)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get fixtures files: %w", err)
+	}
+	f := &fixturesLoaderInternal{db: x.DB().DB, dbType: x.Dialect().URI().DBType, files: files, fixtures: map[string]*fixtureItem{}}
+	switch f.dbType {
+	case schemas.SQLITE:
+		f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
+		f.paramPlaceholder = func(idx int) string { return "?" }
+	case schemas.POSTGRES:
+		f.quoteObject = func(s string) string { return fmt.Sprintf(`"%s"`, s) }
+		f.paramPlaceholder = func(idx int) string { return fmt.Sprintf(`$%d`, idx) }
+	case schemas.MYSQL:
+		f.quoteObject = func(s string) string { return fmt.Sprintf("`%s`", s) }
+		f.paramPlaceholder = func(idx int) string { return "?" }
+	case schemas.MSSQL:
+		f.quoteObject = func(s string) string { return fmt.Sprintf("[%s]", s) }
+		f.paramPlaceholder = func(idx int) string { return "?" }
+	}
+	return f, nil
+}
diff --git a/modules/system/appstate_test.go b/modules/system/appstate_test.go
index d4b9e167c2f93..911319d00a561 100644
--- a/modules/system/appstate_test.go
+++ b/modules/system/appstate_test.go
@@ -13,9 +13,7 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	unittest.MainTest(m, &unittest.TestOptions{
-		FixtureFiles: []string{""}, // load nothing
-	})
+	unittest.MainTest(m, &unittest.TestOptions{FixtureFiles: []string{ /* load nothing */ }})
 }
 
 type testItem1 struct {
@@ -36,8 +34,6 @@ func (*testItem2) Name() string {
 }
 
 func TestAppStateDB(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
 	as := &DBStore{}
 
 	item1 := new(testItem1)

From fef364e7d6ec0d9d5b9fb75b7ac595fdd75f6927 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 11:42:08 +0800
Subject: [PATCH 031/206] Fix bug automerge cannot be chosed when there is only
 1 merge style (#33040) (#33043)

Backport #33040 by @lunny

This is a quick bug fix. Even if there is only 1 merge style, the
dropdown menu will still be displayed to allow users to choose
automerge.

Fix #32448

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 web_src/js/components/PullRequestMergeForm.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index e8bcee70db181..bafeec6c97d07 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -147,7 +147,7 @@ function clearMergeMessage() {
             </template>
           </span>
         </button>
-        <div class="ui dropdown icon button" @click.stop="showMergeStyleMenu = !showMergeStyleMenu" v-if="mergeStyleAllowedCount>1">
+        <div class="ui dropdown icon button" @click.stop="showMergeStyleMenu = !showMergeStyleMenu">
           <svg-icon name="octicon-triangle-down" :size="14"/>
           <div class="menu" :class="{'show':showMergeStyleMenu}">
             <template v-for="msd in mergeForm.mergeStyles">

From 0858a360167f859a326c53ac83e47cc77328b1fe Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 12:17:17 +0800
Subject: [PATCH 032/206] use `-s -w` ldflags for release artifacts (#33041)
 (#33042)

Backport #33041 by @techknowlogick

fix #33030

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
---
 Makefile | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/Makefile b/Makefile
index 4889958c3b63d..978d546eeea5d 100644
--- a/Makefile
+++ b/Makefile
@@ -806,22 +806,22 @@ $(DIST_DIRS):
 
 .PHONY: release-windows
 release-windows: | $(DIST_DIRS)
-	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
 ifeq (,$(findstring gogit,$(TAGS)))
-	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
 endif
 
 .PHONY: release-linux
 release-linux: | $(DIST_DIRS)
-	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
 
 .PHONY: release-darwin
 release-darwin: | $(DIST_DIRS)
-	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
 
 .PHONY: release-freebsd
 release-freebsd: | $(DIST_DIRS)
-	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
+	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
 
 .PHONY: release-copy
 release-copy: | $(DIST_DIRS)

From 610b2fb88d50fdbd13575a1fd81b16a9d2762f33 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 13:17:07 +0800
Subject: [PATCH 033/206] Use gitrepo.GetTreePathLatestCommit to get file
 lastest commit instead from latest commit cache (#32987) (#33046)

---
 modules/git/tree.go          | 11 +++++++++++
 modules/git/tree_test.go     | 15 +++++++++++++++
 routers/api/v1/repo/file.go  | 14 ++++----------
 routers/web/repo/download.go | 16 +++++-----------
 4 files changed, 35 insertions(+), 21 deletions(-)

diff --git a/modules/git/tree.go b/modules/git/tree.go
index 1da4a9fa5debc..d35dc58d8d0ed 100644
--- a/modules/git/tree.go
+++ b/modules/git/tree.go
@@ -62,3 +62,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
 
 	return filelist, err
 }
+
+// GetTreePathLatestCommitID returns the latest commit of a tree path
+func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
+	stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
+		AddDynamicArguments(refName).AddDashesAndList(treePath).
+		RunStdString(&RunOpts{Dir: repo.Path})
+	if err != nil {
+		return nil, err
+	}
+	return repo.GetCommit(strings.TrimSpace(stdout))
+}
diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go
index 6d2b5c84d5069..5fee64b038755 100644
--- a/modules/git/tree_test.go
+++ b/modules/git/tree_test.go
@@ -25,3 +25,18 @@ func TestSubTree_Issue29101(t *testing.T) {
 		assert.True(t, IsErrNotExist(err))
 	}
 }
+
+func Test_GetTreePathLatestCommit(t *testing.T) {
+	repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame"))
+	assert.NoError(t, err)
+	defer repo.Close()
+
+	commitID, err := repo.GetBranchCommitID("master")
+	assert.NoError(t, err)
+	assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID)
+
+	commit, err := repo.GetTreePathLatestCommit("master", "blame.txt")
+	assert.NoError(t, err)
+	assert.NotNil(t, commit)
+	assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String())
+}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 959a4b952a18b..4aed2e5e92c9b 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -11,7 +11,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"path"
 	"strings"
 	"time"
 
@@ -242,19 +241,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn
 		return nil, nil, nil
 	}
 
-	info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
+	latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
 	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
+		ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err)
 		return nil, nil, nil
 	}
+	when := &latestCommit.Committer.When
 
-	if len(info) == 1 {
-		// Not Modified
-		lastModified = &info[0].Commit.Committer.When
-	}
-	blob = entry.Blob()
-
-	return blob, entry, lastModified
+	return entry.Blob(), entry, when
 }
 
 // GetArchive get archive of a repository
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 1ed907b2f97c2..cb1163c70b7ed 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -5,7 +5,6 @@
 package repo
 
 import (
-	"path"
 	"time"
 
 	git_model "code.gitea.io/gitea/models/git"
@@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
 	return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
 }
 
-func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) {
+func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
 	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
 	if err != nil {
 		if git.IsErrNotExist(err) {
@@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T
 		return nil, nil
 	}
 
-	info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
+	latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
 	if err != nil {
-		ctx.ServerError("GetCommitsInfo", err)
+		ctx.ServerError("GetTreePathLatestCommit", err)
 		return nil, nil
 	}
+	lastModified := &latestCommit.Committer.When
 
-	if len(info) == 1 {
-		// Not Modified
-		lastModified = &info[0].Commit.Committer.When
-	}
-	blob = entry.Blob()
-
-	return blob, lastModified
+	return entry.Blob(), lastModified
 }
 
 // SingleDownload download a file by repos path

From d2d763318c8fe3c8d7c1d42b8038d0968026cbe7 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 14:55:06 +0800
Subject: [PATCH 034/206] Remove aws go sdk package dependency (#33029)
 (#33047)

Backport #33029 by lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 assets/go-licenses.json           | 10 ----------
 go.mod                            |  1 -
 go.sum                            |  4 ----
 services/migrations/codecommit.go | 10 +++++-----
 4 files changed, 5 insertions(+), 20 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index d7b76eef07f44..8c155ceadb1a0 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -169,16 +169,6 @@
     "path": "github.com/aws/aws-sdk-go-v2/service/codecommit/LICENSE.txt",
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
-  {
-    "name": "github.com/aws/aws-sdk-go",
-    "path": "github.com/aws/aws-sdk-go/LICENSE.txt",
-    "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
-  },
-  {
-    "name": "github.com/aws/aws-sdk-go/internal/sync/singleflight",
-    "path": "github.com/aws/aws-sdk-go/internal/sync/singleflight/LICENSE",
-    "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/aws/smithy-go",
     "path": "github.com/aws/smithy-go/LICENSE",
diff --git a/go.mod b/go.mod
index 96e6b3411345b..dc80d2ca2bb3a 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,6 @@ require (
 	github.com/PuerkitoBio/goquery v1.10.0
 	github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
 	github.com/alecthomas/chroma/v2 v2.14.0
-	github.com/aws/aws-sdk-go v1.55.5
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.42
 	github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
diff --git a/go.sum b/go.sum
index e4698a756d3bc..b5e64321b53ed 100644
--- a/go.sum
+++ b/go.sum
@@ -105,8 +105,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
-github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk=
 github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
 github.com/aws/aws-sdk-go-v2/credentials v1.17.42 h1:sBP0RPjBU4neGpIYyx8mkU2QqLPl5u9cmdTWVzIpHkM=
@@ -503,8 +501,6 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB
 github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
 github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
 github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go
index ccda62fc3d9fc..fead527f5b9d8 100644
--- a/services/migrations/codecommit.go
+++ b/services/migrations/codecommit.go
@@ -14,11 +14,11 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	base "code.gitea.io/gitea/modules/migration"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/aws/aws-sdk-go-v2/credentials"
 	"github.com/aws/aws-sdk-go-v2/service/codecommit"
 	"github.com/aws/aws-sdk-go-v2/service/codecommit/types"
-	"github.com/aws/aws-sdk-go/aws"
 )
 
 var (
@@ -94,7 +94,7 @@ func (c *CodeCommitDownloader) SetContext(ctx context.Context) {
 // GetRepoInfo returns a repository information
 func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) {
 	output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{
-		RepositoryName: aws.String(c.repoName),
+		RepositoryName: util.ToPointer(c.repoName),
 	})
 	if err != nil {
 		return nil, err
@@ -126,7 +126,7 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas
 	for {
 		resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{
 			NextToken:     nextToken,
-			PullRequestId: aws.String(strconv.FormatInt(commentable.GetForeignIndex(), 10)),
+			PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)),
 		})
 		if err != nil {
 			return nil, false, err
@@ -171,7 +171,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR
 	prs := make([]*base.PullRequest, 0, len(batch))
 	for _, id := range batch {
 		output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{
-			PullRequestId: aws.String(id),
+			PullRequestId: util.ToPointer(id),
 		})
 		if err != nil {
 			return nil, false, err
@@ -243,7 +243,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) {
 
 	for {
 		output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{
-			RepositoryName: aws.String(c.repoName),
+			RepositoryName: util.ToPointer(c.repoName),
 			NextToken:      nextToken,
 		})
 		if err != nil {

From 27de60381d5e75507e956da276331e4be577ac81 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 30 Dec 2024 15:39:59 +0800
Subject: [PATCH 035/206] Fix settings not being loaded at CLI (#26402)
 (#33048)

Backport #26402 by cassiozareck

Closes #25898

Signed-off-by: cassiozareck <cassiomilczareck@gmail.com>
Co-authored-by: cassio zareck <121526696+cassiozareck@users.noreply.github.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 cmd/admin_user_create.go   |  4 ++++
 cmd/web.go                 | 11 +++++++++++
 modules/setting/cors.go    |  5 -----
 modules/setting/indexer.go |  2 +-
 modules/setting/mailer.go  |  4 ----
 modules/setting/session.go |  2 --
 modules/setting/time.go    |  1 -
 7 files changed, 16 insertions(+), 13 deletions(-)

diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index 106d14b25a75f..bf8cbc7c4c0b0 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{
 }
 
 func runCreateUser(c *cli.Context) error {
+	// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
+	// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
+	setting.LoadSettings()
+
 	if err := argsSet(c, "email"); err != nil {
 		return err
 	}
diff --git a/cmd/web.go b/cmd/web.go
index ef8a7426c14d9..f8217758e559e 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"time"
 
 	_ "net/http/pprof" // Used for debugging if enabled and a web server is running
 
@@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) {
 	log.Info("* CustomPath: %s", setting.CustomPath)
 	log.Info("* ConfigFile: %s", setting.CustomConf)
 	log.Info("%s", msg) // show startup message
+
+	if setting.CORSConfig.Enabled {
+		log.Info("CORS Service Enabled")
+	}
+	if setting.DefaultUILocation != time.Local {
+		log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
+	}
+	if setting.MailService != nil {
+		log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
+	}
 }
 
 func serveInstall(ctx *cli.Context) error {
diff --git a/modules/setting/cors.go b/modules/setting/cors.go
index 63daaad60ba75..5260887d9d97b 100644
--- a/modules/setting/cors.go
+++ b/modules/setting/cors.go
@@ -5,8 +5,6 @@ package setting
 
 import (
 	"time"
-
-	"code.gitea.io/gitea/modules/log"
 )
 
 // CORSConfig defines CORS settings
@@ -28,7 +26,4 @@ var CORSConfig = struct {
 
 func loadCorsFrom(rootCfg ConfigProvider) {
 	mustMapSetting(rootCfg, "cors", &CORSConfig)
-	if CORSConfig.Enabled {
-		log.Info("CORS Service Enabled")
-	}
 }
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 18585602c3dd2..34b4eac15b971 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -97,7 +97,7 @@ func IndexerGlobFromString(globstr string) []*GlobMatcher {
 		expr = strings.TrimSpace(expr)
 		if expr != "" {
 			if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
-				log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
+				log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err)
 			} else {
 				extarr = append(extarr, g)
 			}
diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go
index d4db55dc7bc70..4c3dff6850947 100644
--- a/modules/setting/mailer.go
+++ b/modules/setting/mailer.go
@@ -255,8 +255,6 @@ func loadMailerFrom(rootCfg ConfigProvider) {
 		MailService.OverrideEnvelopeFrom = true
 		MailService.EnvelopeFrom = parsed.Address
 	}
-
-	log.Info("Mail Service Enabled")
 }
 
 func loadRegisterMailFrom(rootCfg ConfigProvider) {
@@ -267,7 +265,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) {
 		return
 	}
 	Service.RegisterEmailConfirm = true
-	log.Info("Register Mail Service Enabled")
 }
 
 func loadNotifyMailFrom(rootCfg ConfigProvider) {
@@ -278,7 +275,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) {
 		return
 	}
 	Service.EnableNotifyMail = true
-	log.Info("Notify Mail Service Enabled")
 }
 
 func tryResolveAddr(addr string) []net.IPAddr {
diff --git a/modules/setting/session.go b/modules/setting/session.go
index afe63bfdb7832..19a05ce2c2a5f 100644
--- a/modules/setting/session.go
+++ b/modules/setting/session.go
@@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) {
 	SessionConfig.ProviderConfig = string(shadowConfig)
 	SessionConfig.OriginalProvider = SessionConfig.Provider
 	SessionConfig.Provider = "VirtualSession"
-
-	log.Info("Session Service Enabled")
 }
diff --git a/modules/setting/time.go b/modules/setting/time.go
index 39acba12ef14e..97988211a989a 100644
--- a/modules/setting/time.go
+++ b/modules/setting/time.go
@@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) {
 		if err != nil {
 			log.Fatal("Load time zone failed: %v", err)
 		}
-		log.Info("Default UI Location is %v", zone)
 	}
 	if DefaultUILocation == nil {
 		DefaultUILocation = time.Local

From 968c04c7da20d6ab9d7cf5529562ca668a062e07 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 31 Dec 2024 07:54:40 +0800
Subject: [PATCH 036/206] Fix issue comment number (#30556) (#33055)

Backport #30556 by @lunny

Fix #22419

Only comments with types `CommentTypeComment` and `CommentTypeReview`
will be counted as conversations of the pull request.
`CommentTypeReview` was missed in the previous implementation.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/issues/comment.go      | 38 +++++++++++++++++++++++++++----
 models/issues/comment_test.go |  9 ++++++++
 models/issues/review.go       |  4 ++++
 models/repo.go                | 43 +++++++++++++++++++++++------------
 models/repo_test.go           | 14 ++++++++++++
 5 files changed, 89 insertions(+), 19 deletions(-)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index e4537aa872e9f..729fc1b9e54ec 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -197,6 +197,20 @@ func (t CommentType) HasMailReplySupport() bool {
 	return false
 }
 
+func (t CommentType) CountedAsConversation() bool {
+	for _, ct := range ConversationCountedCommentType() {
+		if t == ct {
+			return true
+		}
+	}
+	return false
+}
+
+// ConversationCountedCommentType returns the comment types that are counted as a conversation
+func ConversationCountedCommentType() []CommentType {
+	return []CommentType{CommentTypeComment, CommentTypeReview}
+}
+
 // RoleInRepo presents the user's participation in the repo
 type RoleInRepo string
 
@@ -893,7 +907,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 		}
 		fallthrough
 	case CommentTypeComment:
-		if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
+		if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil {
 			return err
 		}
 		fallthrough
@@ -1182,8 +1196,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
 		return err
 	}
 
-	if comment.Type == CommentTypeComment {
-		if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil {
+	if comment.Type.CountedAsConversation() {
+		if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil {
 			return err
 		}
 	}
@@ -1300,6 +1314,21 @@ func (c *Comment) HasOriginalAuthor() bool {
 	return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
 }
 
+func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder {
+	subQuery := builder.Select("COUNT(*)").From("`comment`").Where(
+		builder.Eq{"issue_id": issueID}.And(
+			builder.In("`type`", ConversationCountedCommentType()),
+		))
+
+	return builder.Update(builder.Eq{"num_comments": subQuery}).
+		From("`issue`").Where(builder.Eq{"id": issueID})
+}
+
+func UpdateIssueNumComments(ctx context.Context, issueID int64) error {
+	_, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID))
+	return err
+}
+
 // InsertIssueComments inserts many comments of issues.
 func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 	if len(comments) == 0 {
@@ -1332,8 +1361,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
 	}
 
 	for _, issueID := range issueIDs {
-		if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
-			issueID, CommentTypeComment, issueID); err != nil {
+		if err := UpdateIssueNumComments(ctx, issueID); err != nil {
 			return err
 		}
 	}
diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go
index d81f33f953d0e..cb31a21f92105 100644
--- a/models/issues/comment_test.go
+++ b/models/issues/comment_test.go
@@ -97,3 +97,12 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
 
 	unittest.CheckConsistencyFor(t, &issues_model.Issue{})
 }
+
+func Test_UpdateIssueNumComments(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+
+	assert.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID))
+	issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+	assert.EqualValues(t, 1, issue2.NumComments)
+}
diff --git a/models/issues/review.go b/models/issues/review.go
index 8b345e5fd8002..3e787273be8e1 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -639,6 +639,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
 				return err
 			}
 		}
+
+		if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil {
+			return err
+		}
 	}
 
 	return committer.Commit()
diff --git a/models/repo.go b/models/repo.go
index 0dc8ee5df33e7..598f8df6a43ee 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -19,6 +19,8 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+
+	"xorm.io/builder"
 )
 
 // Init initialize model
@@ -27,7 +29,7 @@ func Init(ctx context.Context) error {
 }
 
 type repoChecker struct {
-	querySQL   func(ctx context.Context) ([]map[string][]byte, error)
+	querySQL   func(ctx context.Context) ([]int64, error)
 	correctSQL func(ctx context.Context, id int64) error
 	desc       string
 }
@@ -38,8 +40,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
 		log.Error("Select %s: %v", checker.desc, err)
 		return
 	}
-	for _, result := range results {
-		id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
+	for _, id := range results {
 		select {
 		case <-ctx.Done():
 			log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id)
@@ -54,21 +55,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
 	}
 }
 
-func StatsCorrectSQL(ctx context.Context, sql string, id int64) error {
-	_, err := db.GetEngine(ctx).Exec(sql, id, id)
+func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error {
+	args := []any{sql}
+	args = append(args, ids...)
+	_, err := db.GetEngine(ctx).Exec(args...)
 	return err
 }
 
 func repoStatsCorrectNumWatches(ctx context.Context, id int64) error {
-	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id)
+	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id)
 }
 
 func repoStatsCorrectNumStars(ctx context.Context, id int64) error {
-	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id)
+	return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id)
 }
 
 func labelStatsCorrectNumIssues(ctx context.Context, id int64) error {
-	return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id)
+	return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id)
 }
 
 func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
@@ -105,11 +108,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
 }
 
 func userStatsCorrectNumRepos(ctx context.Context, id int64) error {
-	return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id)
+	return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id)
 }
 
 func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error {
-	return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id)
+	return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id))
 }
 
 func repoStatsCorrectNumIssues(ctx context.Context, id int64) error {
@@ -128,9 +131,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error {
 	return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true)
 }
 
-func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) {
-	return func(ctx context.Context) ([]map[string][]byte, error) {
-		return db.GetEngine(ctx).Query(args...)
+// statsQuery returns a function that queries the database for a list of IDs
+// sql could be a string or a *builder.Builder
+func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) {
+	return func(ctx context.Context) ([]int64, error) {
+		var ids []int64
+		return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids)
 	}
 }
 
@@ -201,7 +207,16 @@ func CheckRepoStats(ctx context.Context) error {
 		},
 		// Issue.NumComments
 		{
-			statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"),
+			statsQuery(builder.Select("`issue`.id").From("`issue`").Where(
+				builder.Neq{
+					"`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where(
+						builder.Expr("issue_id = `issue`.id").And(
+							builder.In("type", issues_model.ConversationCountedCommentType()),
+						),
+					),
+				},
+			),
+			),
 			repoStatsCorrectIssueNumComments,
 			"issue count 'num_comments'",
 		},
diff --git a/models/repo_test.go b/models/repo_test.go
index 2a8a4a743ed27..bcf62237f08d7 100644
--- a/models/repo_test.go
+++ b/models/repo_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unittest"
 
 	"github.com/stretchr/testify/assert"
@@ -22,3 +23,16 @@ func TestDoctorUserStarNum(t *testing.T) {
 
 	assert.NoError(t, DoctorUserStarNum(db.DefaultContext))
 }
+
+func Test_repoStatsCorrectIssueNumComments(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+	assert.NotNil(t, issue2)
+	assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here
+
+	assert.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2))
+	// reload the issue
+	issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
+	assert.EqualValues(t, 1, issue2.NumComments)
+}

From 96fff862dc56b1bdfd987df69e310de00b419062 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 31 Dec 2024 11:04:47 +0800
Subject: [PATCH 037/206] Fix duplicate co-author in squashed merge commit
 messages (#33020) (#33054)

---
 services/pull/merge_squash.go | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go
index 197d8102dd960..6dda46eaece80 100644
--- a/services/pull/merge_squash.go
+++ b/services/pull/merge_squash.go
@@ -5,6 +5,7 @@ package pull
 
 import (
 	"fmt"
+	"strings"
 
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -66,7 +67,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
 
 	if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
 		// add trailer
-		message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String())
+		if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) {
+			message += fmt.Sprintf("\nCo-authored-by: %s", sig.String())
+		}
+		message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String())
 	}
 	cmdCommit := git.NewCommand(ctx, "commit").
 		AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).

From 3df11c07a835c17ae2d32efd7c4f302f49a0a75c Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 31 Dec 2024 11:56:34 +0800
Subject: [PATCH 038/206] Make issue suggestion work for new PR page (#33035)
 (#33056)

Backport #33035 by wxiaoguang

Fix #33026

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/utils.test.ts | 1 +
 web_src/js/utils.ts      | 5 +++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts
index ac9d4fab91b23..bbe328f658df0 100644
--- a/web_src/js/utils.test.ts
+++ b/web_src/js/utils.test.ts
@@ -49,6 +49,7 @@ test('parseIssueNewHref', () => {
   expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
   expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
   expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
+  expect(parseIssueNewHref('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls'});
 });
 
 test('parseUrl', () => {
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index 997a4d1ff3f90..efa144c3c762f 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -39,8 +39,9 @@ export function parseIssueHref(href: string): IssuePathInfo {
 
 export function parseIssueNewHref(href: string): IssuePathInfo {
   const path = (href || '').replace(/[#?].*$/, '');
-  const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/new/.exec(path) || [];
-  return {ownerName, repoName, pathType, indexString};
+  const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || [];
+  const pathType = pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls';
+  return {ownerName, repoName, pathType};
 }
 
 export function parseIssuePageInfo(): IssuePageInfo {

From 68736ec292bf54f16be4f28d93b944366ef8a9e4 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 31 Dec 2024 15:22:09 +0800
Subject: [PATCH 039/206] Refactor maven package registry (#33049) (#33057)

Backport #33049
---
 models/packages/package.go                   |  12 +++
 routers/api/packages/api.go                  |   2 -
 routers/api/packages/maven/maven.go          |  76 +++++++++----
 tests/integration/api_packages_maven_test.go | 108 ++++++++++++-------
 4 files changed, 138 insertions(+), 60 deletions(-)

diff --git a/models/packages/package.go b/models/packages/package.go
index 417d62d199310..b7464f914063a 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) {
 	return p, nil
 }
 
+// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages
+func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error {
+	var cond builder.Cond = builder.Eq{
+		"package.id":          packageID,
+		"package.owner_id":    ownerID,
+		"package.type":        packageType,
+		"package.is_internal": false,
+	}
+	_, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)})
+	return err
+}
+
 // GetPackageByName gets a package by name
 func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) {
 	var cond builder.Cond = builder.Eq{
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 93c29678b000b..47ea7137b8250 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -465,8 +465,6 @@ func CommonRoutes() *web.Router {
 			r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
 		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/maven", func() {
-			// FIXME: this path design is not right.
-			// It should be `/.../{groupId}/{artifactId}/{version}`, but not `/.../{groupId}-{artifactId}/{version}`
 			r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)
 			r.Get("/*", maven.DownloadPackageFile)
 			r.Head("/*", maven.ProvidePackageFileHeader)
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 9474b17bc79d1..4d04d4d1e9f32 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -13,7 +13,7 @@ import (
 	"errors"
 	"io"
 	"net/http"
-	"path/filepath"
+	"path"
 	"regexp"
 	"sort"
 	"strconv"
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
 	maven_module "code.gitea.io/gitea/modules/packages/maven"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers/api/packages/helper"
 	"code.gitea.io/gitea/services/context"
 	packages_service "code.gitea.io/gitea/services/packages"
@@ -44,7 +45,7 @@ const (
 
 var (
 	errInvalidParameters = errors.New("request parameters are invalid")
-	illegalCharacters    = regexp.MustCompile(`[\\/:"<>|?\*]`)
+	illegalCharacters    = regexp.MustCompile(`[\\/:"<>|?*]`)
 )
 
 func apiError(ctx *context.Context, status int, obj any) {
@@ -85,8 +86,10 @@ func handlePackageFile(ctx *context.Context, serveContent bool) {
 func serveMavenMetadata(ctx *context.Context, params parameters) {
 	// /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
 
-	packageName := params.GroupID + "-" + params.ArtifactID
-	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName)
+	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName())
+	if errors.Is(err, util.ErrNotExist) {
+		pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
+	}
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
 		return
@@ -116,10 +119,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 
 	latest := pds[len(pds)-1]
 	// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
-	lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat)
-	ctx.Resp.Header().Set("Last-Modified", lastModifed)
+	lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat)
+	ctx.Resp.Header().Set("Last-Modified", lastModified)
 
-	ext := strings.ToLower(filepath.Ext(params.Filename))
+	ext := strings.ToLower(path.Ext(params.Filename))
 	if isChecksumExtension(ext) {
 		var hash []byte
 		switch ext {
@@ -147,11 +150,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 }
 
 func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {
-	packageName := params.GroupID + "-" + params.ArtifactID
-
-	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version)
+	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version)
+	if errors.Is(err, util.ErrNotExist) {
+		pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version)
+	}
 	if err != nil {
-		if err == packages_model.ErrPackageNotExist {
+		if errors.Is(err, packages_model.ErrPackageNotExist) {
 			apiError(ctx, http.StatusNotFound, err)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -161,14 +165,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool
 
 	filename := params.Filename
 
-	ext := strings.ToLower(filepath.Ext(filename))
+	ext := strings.ToLower(path.Ext(filename))
 	if isChecksumExtension(ext) {
 		filename = filename[:len(filename)-len(ext)]
 	}
 
 	pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey)
 	if err != nil {
-		if err == packages_model.ErrPackageFileNotExist {
+		if errors.Is(err, packages_model.ErrPackageFileNotExist) {
 			apiError(ctx, http.StatusNotFound, err)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -238,15 +242,17 @@ func UploadPackageFile(ctx *context.Context) {
 		return
 	}
 
-	log.Trace("Parameters: %+v", params)
-
 	// Ignore the package index /<name>/maven-metadata.xml
 	if params.IsMeta && params.Version == "" {
 		ctx.Status(http.StatusOK)
 		return
 	}
 
-	packageName := params.GroupID + "-" + params.ArtifactID
+	packageName := params.toInternalPackageName()
+	if ctx.FormBool("use_legacy_package_name") {
+		// for testing purpose only
+		packageName = params.toInternalPackageNameLegacy()
+	}
 
 	// for the same package, only one upload at a time
 	releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName))
@@ -274,13 +280,26 @@ func UploadPackageFile(ctx *context.Context) {
 		Creator:          ctx.Doer,
 	}
 
-	ext := filepath.Ext(params.Filename)
+	// old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId"
+	legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
+	if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	} else if legacyPackage != nil {
+		err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName)
+		if err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+	}
+
+	ext := path.Ext(params.Filename)
 
 	// Do not upload checksum files but compare the hashes.
 	if isChecksumExtension(ext) {
 		pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
 		if err != nil {
-			if err == packages_model.ErrPackageNotExist {
+			if errors.Is(err, packages_model.ErrPackageNotExist) {
 				apiError(ctx, http.StatusNotFound, err)
 				return
 			}
@@ -289,7 +308,7 @@ func UploadPackageFile(ctx *context.Context) {
 		}
 		pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey)
 		if err != nil {
-			if err == packages_model.ErrPackageFileNotExist {
+			if errors.Is(err, packages_model.ErrPackageFileNotExist) {
 				apiError(ctx, http.StatusNotFound, err)
 				return
 			}
@@ -343,7 +362,7 @@ func UploadPackageFile(ctx *context.Context) {
 
 		if pvci.Metadata != nil {
 			pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
-			if err != nil && err != packages_model.ErrPackageNotExist {
+			if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
 				apiError(ctx, http.StatusInternalServerError, err)
 				return
 			}
@@ -399,9 +418,26 @@ type parameters struct {
 	IsMeta     bool
 }
 
+func (p *parameters) toInternalPackageName() string {
+	// there cuold be 2 choices: "/" or ":"
+	// Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates
+	// but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id"
+	return p.GroupID + ":" + p.ArtifactID
+}
+
+func (p *parameters) toInternalPackageNameLegacy() string {
+	return p.GroupID + "-" + p.ArtifactID
+}
+
 func extractPathParameters(ctx *context.Context) (parameters, error) {
 	parts := strings.Split(ctx.PathParam("*"), "/")
 
+	// formats:
+	// * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512]
+	// * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512]
+	// * /com/group/id/artifactId/version/any-file
+	// * /com/group/id/artifactId/version-SNAPSHOT/any-file
+
 	p := parameters{
 		Filename: parts[len(parts)-1],
 	}
diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go
index e54238858c2ea..486a5af93e17b 100644
--- a/tests/integration/api_packages_maven_test.go
+++ b/tests/integration/api_packages_maven_test.go
@@ -6,6 +6,7 @@ package integration
 import (
 	"fmt"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 	"sync"
@@ -20,6 +21,7 @@ import (
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestPackageMaven(t *testing.T) {
@@ -29,16 +31,14 @@ func TestPackageMaven(t *testing.T) {
 
 	groupID := "com.gitea"
 	artifactID := "test-project"
-	packageName := groupID + "-" + artifactID
 	packageVersion := "1.0.1"
 	packageDescription := "Test Description"
 
-	root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID)
-	filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion)
+	root := "/api/packages/user2/maven/com/gitea/test-project"
+	filename := "any-name.jar"
 
 	putFile := func(t *testing.T, path, content string, expectedStatus int) {
-		req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).
-			AddBasicAuth(user.Name)
+		req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name)
 		MakeRequest(t, req, expectedStatus)
 	}
 
@@ -56,27 +56,67 @@ func TestPackageMaven(t *testing.T) {
 		putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
 
 		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pvs, 1)
 
 		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Nil(t, pd.SemVer)
 		assert.Nil(t, pd.Metadata)
-		assert.Equal(t, packageName, pd.Package.Name)
+		assert.Equal(t, groupID+":"+artifactID, pd.Package.Name)
 		assert.Equal(t, packageVersion, pd.Version.Version)
 
 		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pfs, 1)
 		assert.Equal(t, filename, pfs[0].Name)
 		assert.False(t, pfs[0].IsLead)
 
 		pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Equal(t, int64(4), pb.Size)
 	})
 
+	t.Run("UploadLegacy", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project"
+		req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+		p, err := packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project")
+		require.NoError(t, err)
+		assert.Equal(t, "com.gitea-legacy-project", p.Name)
+
+		req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusOK)
+
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusOK)
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusNotFound)
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusNotFound)
+
+		req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+		_, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project")
+		require.ErrorIs(t, err, packages.ErrPackageNotExist)
+		p, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea:legacy-project")
+		require.NoError(t, err)
+		assert.Equal(t, "com.gitea:legacy-project", p.Name)
+		req = NewRequest(t, "HEAD", legacyRootLink+"/1.0.2/any-file-name").AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusOK)
+
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea-legacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusNotFound)
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea:legacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusOK)
+		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
+		MakeRequest(t, req, http.StatusOK)
+
+		require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID))
+	})
+
 	t.Run("UploadExists", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
@@ -86,14 +126,12 @@ func TestPackageMaven(t *testing.T) {
 	t.Run("Download", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).
-			AddBasicAuth(user.Name)
+		req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name)
 		resp := MakeRequest(t, req, http.StatusOK)
 
 		checkHeaders(t, resp.Header(), "application/java-archive", 4)
 
-		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).
-			AddBasicAuth(user.Name)
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)).AddBasicAuth(user.Name)
 		resp = MakeRequest(t, req, http.StatusOK)
 
 		checkHeaders(t, resp.Header(), "application/java-archive", 4)
@@ -101,7 +139,7 @@ func TestPackageMaven(t *testing.T) {
 		assert.Equal(t, []byte("test"), resp.Body.Bytes())
 
 		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pvs, 1)
 		assert.Equal(t, int64(0), pvs[0].DownloadCount)
 	})
@@ -133,26 +171,26 @@ func TestPackageMaven(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
 		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pvs, 1)
 
 		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Nil(t, pd.Metadata)
 
 		putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated)
 
 		pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pvs, 1)
 
 		pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.IsType(t, &maven.Metadata{}, pd.Metadata)
 		assert.Equal(t, packageDescription, pd.Metadata.(*maven.Metadata).Description)
 
 		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pfs, 2)
 		for _, pf := range pfs {
 			if strings.HasSuffix(pf.Name, ".pom") {
@@ -167,14 +205,12 @@ func TestPackageMaven(t *testing.T) {
 	t.Run("DownloadPOM", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).
-			AddBasicAuth(user.Name)
+		req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name)
 		resp := MakeRequest(t, req, http.StatusOK)
 
 		checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent)))
 
-		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).
-			AddBasicAuth(user.Name)
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)).AddBasicAuth(user.Name)
 		resp = MakeRequest(t, req, http.StatusOK)
 
 		checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent)))
@@ -182,7 +218,7 @@ func TestPackageMaven(t *testing.T) {
 		assert.Equal(t, []byte(pomContent), resp.Body.Bytes())
 
 		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
-		assert.NoError(t, err)
+		require.NoError(t, err)
 		assert.Len(t, pvs, 1)
 		assert.Equal(t, int64(1), pvs[0].DownloadCount)
 	})
@@ -190,8 +226,7 @@ func TestPackageMaven(t *testing.T) {
 	t.Run("DownloadChecksums", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)).
-			AddBasicAuth(user.Name)
+		req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename)).AddBasicAuth(user.Name)
 		MakeRequest(t, req, http.StatusNotFound)
 
 		for key, checksum := range map[string]string{
@@ -200,8 +235,7 @@ func TestPackageMaven(t *testing.T) {
 			"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
 			"sha512": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
 		} {
-			req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)).
-				AddBasicAuth(user.Name)
+			req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key)).AddBasicAuth(user.Name)
 			resp := MakeRequest(t, req, http.StatusOK)
 
 			assert.Equal(t, checksum, resp.Body.String())
@@ -211,8 +245,7 @@ func TestPackageMaven(t *testing.T) {
 	t.Run("DownloadMetadata", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "GET", root+"/maven-metadata.xml").
-			AddBasicAuth(user.Name)
+		req := NewRequest(t, "GET", root+"/maven-metadata.xml").AddBasicAuth(user.Name)
 		resp := MakeRequest(t, req, http.StatusOK)
 
 		expectedMetadata := `<?xml version="1.0" encoding="UTF-8"?>` + "\n<metadata><groupId>com.gitea</groupId><artifactId>test-project</artifactId><versioning><release>1.0.1</release><latest>1.0.1</latest><versions><version>1.0.1</version></versions></versioning></metadata>"
@@ -227,8 +260,7 @@ func TestPackageMaven(t *testing.T) {
 			"sha256": "3f48322f81c4b2c3bb8649ae1e5c9801476162b520e1c2734ac06b2c06143208",
 			"sha512": "cb075aa2e2ef1a83cdc14dd1e08c505b72d633399b39e73a21f00f0deecb39a3e2c79f157c1163f8a3854828750706e0dec3a0f5e4778e91f8ec2cf351a855f2",
 		} {
-			req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)).
-				AddBasicAuth(user.Name)
+			req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key)).AddBasicAuth(user.Name)
 			resp := MakeRequest(t, req, http.StatusOK)
 
 			assert.Equal(t, checksum, resp.Body.String())
@@ -245,9 +277,10 @@ func TestPackageMaven(t *testing.T) {
 	})
 
 	t.Run("InvalidFile", func(t *testing.T) {
-		ver := packageVersion + "-invalid"
-		putFile(t, fmt.Sprintf("/%s/%s", ver, filename), "any invalid content", http.StatusCreated)
-		req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s-%s/%s", user.Name, groupID, artifactID, ver)
+		invalidVersion := packageVersion + "-invalid"
+		putFile(t, fmt.Sprintf("/%s/%s", invalidVersion, filename), "any invalid content", http.StatusCreated)
+
+		req := NewRequestf(t, "GET", "/%s/-/packages/maven/%s/%s", user.Name, url.QueryEscape(groupID+":"+artifactID), invalidVersion)
 		resp := MakeRequest(t, req, http.StatusOK)
 		assert.Contains(t, resp.Body.String(), "No metadata.")
 		assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
@@ -266,8 +299,7 @@ func TestPackageMavenConcurrent(t *testing.T) {
 	root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID)
 
 	putFile := func(t *testing.T, path, content string, expectedStatus int) {
-		req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).
-			AddBasicAuth(user.Name)
+		req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).AddBasicAuth(user.Name)
 		MakeRequest(t, req, expectedStatus)
 	}
 

From bc83fb26ef228f4c6201292561f79d8a6a91c1de Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 31 Dec 2024 20:06:35 +0800
Subject: [PATCH 040/206] Use project's redirect url instead of composing url
 (#33058) (#33064)

Backport #33058 by lunny

Fix #32992

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/project/project.go     | 12 ++++++++++--
 routers/web/org/projects.go   |  6 +++---
 routers/web/repo/issue_new.go | 13 ++++++++++---
 routers/web/repo/projects.go  |  6 +++---
 4 files changed, 26 insertions(+), 11 deletions(-)

diff --git a/models/project/project.go b/models/project/project.go
index 9779908b9d5be..7385efd39d10b 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) {
 	return err
 }
 
+func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint
+	return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID)
+}
+
+func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint
+	return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID)
+}
+
 // Link returns the project's relative URL.
 func (p *Project) Link(ctx context.Context) string {
 	if p.OwnerID > 0 {
@@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string {
 			log.Error("LoadOwner: %v", err)
 			return ""
 		}
-		return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID)
+		return ProjectLinkForOrg(p.Owner, p.ID)
 	}
 	if p.RepoID > 0 {
 		err := p.LoadRepo(ctx)
@@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string {
 			log.Error("LoadRepo: %v", err)
 			return ""
 		}
-		return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID)
+		return ProjectLinkForRepo(p.Repo, p.ID)
 	}
 	return ""
 }
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 3b9ec2a7b85f0..ebfbc22db161a 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -212,7 +212,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
 		return
 	}
-	ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id))
+	ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id))
 }
 
 // DeleteProject delete a project
@@ -262,7 +262,7 @@ func RenderEditProject(ctx *context.Context) {
 	ctx.Data["redirect"] = ctx.FormString("redirect")
 	ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
 	ctx.Data["card_type"] = p.CardType
-	ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID)
+	ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID)
 
 	ctx.HTML(http.StatusOK, tplProjectsNew)
 }
@@ -276,7 +276,7 @@ func EditProjectPost(ctx *context.Context) {
 	ctx.Data["PageIsViewProjects"] = true
 	ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
 	ctx.Data["CardTypes"] = project_model.GetCardConfig()
-	ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
+	ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID)
 
 	shared_user.RenderUserHeader(ctx)
 
diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go
index 9a941ce85714b..32daa3e48fabd 100644
--- a/routers/web/repo/issue_new.go
+++ b/routers/web/repo/issue_new.go
@@ -396,8 +396,15 @@ func NewIssuePost(ctx *context.Context) {
 
 	log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
 	if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
-		ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
-	} else {
-		ctx.JSONRedirect(issue.Link())
+		project, err := project_model.GetProjectByID(ctx, projectID)
+		if err == nil {
+			if project.Type == project_model.TypeOrganization {
+				ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID))
+			} else {
+				ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID))
+			}
+			return
+		}
 	}
+	ctx.JSONRedirect(issue.Link())
 }
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 3be9578670602..d07ff4bbbec79 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -181,7 +181,7 @@ func ChangeProjectStatus(ctx *context.Context) {
 		ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
 		return
 	}
-	ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id))
+	ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id))
 }
 
 // DeleteProject delete a project
@@ -235,7 +235,7 @@ func RenderEditProject(ctx *context.Context) {
 	ctx.Data["content"] = p.Description
 	ctx.Data["card_type"] = p.CardType
 	ctx.Data["redirect"] = ctx.FormString("redirect")
-	ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID)
+	ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID)
 
 	ctx.HTML(http.StatusOK, tplProjectsNew)
 }
@@ -249,7 +249,7 @@ func EditProjectPost(ctx *context.Context) {
 	ctx.Data["PageIsEditProjects"] = true
 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
 	ctx.Data["CardTypes"] = project_model.GetCardConfig()
-	ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID)
+	ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
 
 	if ctx.HasError() {
 		ctx.HTML(http.StatusOK, tplProjectsNew)

From 39cc72562b1f9607d9eb8141daa8050d4468aade Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 31 Dec 2024 20:33:08 +0800
Subject: [PATCH 041/206] feat(action): issue change title notifications
 (#33050) (#33065)

Backport #33050 by appleboy

action file as below:

```yaml
name: Semantic Pull Request

on:
  pull_request_target:
    types: [edited]
```

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/actions/notifier.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index a4ebdf9e888ee..67e33e7cce0a8 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -58,7 +58,15 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu
 // IssueChangeContent notifies change content of issue
 func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) {
 	ctx = withMethod(ctx, "IssueChangeContent")
+	n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue)
+}
+
+func (n *actionsNotifier) IssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string) {
+	ctx = withMethod(ctx, "IssueChangeTitle")
+	n.notifyIssueChangeWithTitleOrContent(ctx, doer, issue)
+}
 
+func (n *actionsNotifier) notifyIssueChangeWithTitleOrContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) {
 	var err error
 	if err = issue.LoadRepo(ctx); err != nil {
 		log.Error("LoadRepo: %v", err)

From 81768675d4e3bca55c2df81121b0b3b1a3a4c5cd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 2 Jan 2025 12:17:05 +0800
Subject: [PATCH 042/206] Inherit submodules from template repository content
 (#16237) (#33068)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Backport #16237 (it more likely a bug fix)

Co-authored-by: Steffen Schröter <steffen@vexar.de>
---
 modules/git/batch_reader.go                   |   6 +-
 modules/git/parse.go                          |  78 +++++++++++
 modules/git/parse_nogogit.go                  |  67 ++-------
 modules/git/pipeline/lfs_nogogit.go           |   2 +-
 modules/git/submodule.go                      |  66 +++++++++
 modules/git/submodule_test.go                 |  48 +++++++
 modules/git/tests/repos/repo4_submodules/HEAD |   1 +
 .../git/tests/repos/repo4_submodules/config   |   4 +
 .../97/c3d30df0e6492348292600920a6482feaebb74 | Bin 0 -> 110 bytes
 .../c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 | Bin 0 -> 112 bytes
 .../e1/e59caba97193d48862d6809912043871f37437 |   2 +
 .../repos/repo4_submodules/refs/heads/master  |   1 +
 modules/git/tree.go                           |   4 +-
 modules/git/tree_blob_nogogit.go              |   1 -
 modules/git/tree_entry_nogogit.go             |  12 +-
 services/repository/generate.go               | 127 +++++++++---------
 templates/repo/view_list.tmpl                 |   4 +-
 17 files changed, 289 insertions(+), 134 deletions(-)
 create mode 100644 modules/git/parse.go
 create mode 100644 modules/git/submodule.go
 create mode 100644 modules/git/submodule_test.go
 create mode 100644 modules/git/tests/repos/repo4_submodules/HEAD
 create mode 100644 modules/git/tests/repos/repo4_submodules/config
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34
 create mode 100644 modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
 create mode 100644 modules/git/tests/repos/repo4_submodules/refs/heads/master

diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go
index 7dfda721554dd..4e9f854ad9dc6 100644
--- a/modules/git/batch_reader.go
+++ b/modules/git/batch_reader.go
@@ -253,7 +253,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
 	return out
 }
 
-// ParseTreeLine reads an entry from a tree in a cat-file --batch stream
+// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream
 // This carefully avoids allocations - except where fnameBuf is too small.
 // It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
 //
@@ -261,7 +261,7 @@ func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
 // <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
 //
 // We don't attempt to convert the raw HASH to save a lot of time
-func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
+func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
 	var readBytes []byte
 
 	// Read the Mode & fname
@@ -271,7 +271,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu
 	}
 	idx := bytes.IndexByte(readBytes, ' ')
 	if idx < 0 {
-		log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes)
+		log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes)
 		return mode, fname, sha, n, &ErrNotExist{}
 	}
 
diff --git a/modules/git/parse.go b/modules/git/parse.go
new file mode 100644
index 0000000000000..eb26632cc0e5c
--- /dev/null
+++ b/modules/git/parse.go
@@ -0,0 +1,78 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/optional"
+)
+
+var sepSpace = []byte{' '}
+
+type LsTreeEntry struct {
+	ID        ObjectID
+	EntryMode EntryMode
+	Name      string
+	Size      optional.Option[int64]
+}
+
+func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
+	// expect line to be of the form:
+	// <mode> <type> <sha> <space-padded-size>\t<filename>
+	// <mode> <type> <sha>\t<filename>
+
+	var err error
+	posTab := bytes.IndexByte(line, '\t')
+	if posTab == -1 {
+		return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
+	}
+
+	entry := new(LsTreeEntry)
+
+	entryAttrs := line[:posTab]
+	entryName := line[posTab+1:]
+
+	entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
+	_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
+	entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
+	if len(entryAttrs) > 0 {
+		entrySize := entryAttrs // the last field is the space-padded-size
+		size, _ := strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
+		entry.Size = optional.Some(size)
+	}
+
+	switch string(entryMode) {
+	case "100644":
+		entry.EntryMode = EntryModeBlob
+	case "100755":
+		entry.EntryMode = EntryModeExec
+	case "120000":
+		entry.EntryMode = EntryModeSymlink
+	case "160000":
+		entry.EntryMode = EntryModeCommit
+	case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
+		entry.EntryMode = EntryModeTree
+	default:
+		return nil, fmt.Errorf("unknown type: %v", string(entryMode))
+	}
+
+	entry.ID, err = NewIDFromString(string(entryObjectID))
+	if err != nil {
+		return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
+	}
+
+	if len(entryName) > 0 && entryName[0] == '"' {
+		entry.Name, err = strconv.Unquote(string(entryName))
+		if err != nil {
+			return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
+		}
+	} else {
+		entry.Name = string(entryName)
+	}
+	return entry, nil
+}
diff --git a/modules/git/parse_nogogit.go b/modules/git/parse_nogogit.go
index 546b38be37964..676bb3c76c09f 100644
--- a/modules/git/parse_nogogit.go
+++ b/modules/git/parse_nogogit.go
@@ -10,8 +10,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"strconv"
-	"strings"
 
 	"code.gitea.io/gitea/modules/log"
 )
@@ -21,71 +19,30 @@ func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
 	return parseTreeEntries(data, nil)
 }
 
-var sepSpace = []byte{' '}
-
+// parseTreeEntries FIXME this function's design is not right, it should make the caller read all data into memory
 func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
-	var err error
 	entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
 	for pos := 0; pos < len(data); {
-		// expect line to be of the form:
-		// <mode> <type> <sha> <space-padded-size>\t<filename>
-		// <mode> <type> <sha>\t<filename>
 		posEnd := bytes.IndexByte(data[pos:], '\n')
 		if posEnd == -1 {
 			posEnd = len(data)
 		} else {
 			posEnd += pos
 		}
-		line := data[pos:posEnd]
-		posTab := bytes.IndexByte(line, '\t')
-		if posTab == -1 {
-			return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
-		}
-
-		entry := new(TreeEntry)
-		entry.ptree = ptree
-
-		entryAttrs := line[:posTab]
-		entryName := line[posTab+1:]
-
-		entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
-		_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
-		entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
-		if len(entryAttrs) > 0 {
-			entrySize := entryAttrs // the last field is the space-padded-size
-			entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
-			entry.sized = true
-		}
 
-		switch string(entryMode) {
-		case "100644":
-			entry.entryMode = EntryModeBlob
-		case "100755":
-			entry.entryMode = EntryModeExec
-		case "120000":
-			entry.entryMode = EntryModeSymlink
-		case "160000":
-			entry.entryMode = EntryModeCommit
-		case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
-			entry.entryMode = EntryModeTree
-		default:
-			return nil, fmt.Errorf("unknown type: %v", string(entryMode))
-		}
-
-		entry.ID, err = NewIDFromString(string(entryObjectID))
+		line := data[pos:posEnd]
+		lsTreeLine, err := parseLsTreeLine(line)
 		if err != nil {
-			return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
+			return nil, err
 		}
-
-		if len(entryName) > 0 && entryName[0] == '"' {
-			entry.name, err = strconv.Unquote(string(entryName))
-			if err != nil {
-				return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
-			}
-		} else {
-			entry.name = string(entryName)
+		entry := &TreeEntry{
+			ptree:     ptree,
+			ID:        lsTreeLine.ID,
+			entryMode: lsTreeLine.EntryMode,
+			name:      lsTreeLine.Name,
+			size:      lsTreeLine.Size.Value(),
+			sized:     lsTreeLine.Size.Has(),
 		}
-
 		pos = posEnd + 1
 		entries = append(entries, entry)
 	}
@@ -100,7 +57,7 @@ func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.
 
 loop:
 	for sz > 0 {
-		mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
+		mode, fname, sha, count, err := ParseCatFileTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
 		if err != nil {
 			if err == io.EOF {
 				break loop
diff --git a/modules/git/pipeline/lfs_nogogit.go b/modules/git/pipeline/lfs_nogogit.go
index b22805c1327e9..92e35c5a1028a 100644
--- a/modules/git/pipeline/lfs_nogogit.go
+++ b/modules/git/pipeline/lfs_nogogit.go
@@ -114,7 +114,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
 			case "tree":
 				var n int64
 				for n < size {
-					mode, fname, binObjectID, count, err := git.ParseTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
+					mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf)
 					if err != nil {
 						return nil, err
 					}
diff --git a/modules/git/submodule.go b/modules/git/submodule.go
new file mode 100644
index 0000000000000..017b644052b93
--- /dev/null
+++ b/modules/git/submodule.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"os"
+
+	"code.gitea.io/gitea/modules/log"
+)
+
+type TemplateSubmoduleCommit struct {
+	Path   string
+	Commit string
+}
+
+// GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository
+// This function is only for generating new repos based on existing template, the template couldn't be too large.
+func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) {
+	stdoutReader, stdoutWriter, err := os.Pipe()
+	if err != nil {
+		return nil, err
+	}
+	opts := &RunOpts{
+		Dir:    repoPath,
+		Stdout: stdoutWriter,
+		PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+			_ = stdoutWriter.Close()
+			defer stdoutReader.Close()
+
+			scanner := bufio.NewScanner(stdoutReader)
+			for scanner.Scan() {
+				entry, err := parseLsTreeLine(scanner.Bytes())
+				if err != nil {
+					cancel()
+					return err
+				}
+				if entry.EntryMode == EntryModeCommit {
+					submoduleCommits = append(submoduleCommits, TemplateSubmoduleCommit{Path: entry.Name, Commit: entry.ID.String()})
+				}
+			}
+			return scanner.Err()
+		},
+	}
+	err = NewCommand(ctx, "ls-tree", "-r", "--", "HEAD").Run(opts)
+	if err != nil {
+		return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err)
+	}
+	return submoduleCommits, nil
+}
+
+// AddTemplateSubmoduleIndexes Adds the given submodules to the git index.
+// It is only for generating new repos based on existing template, requires the .gitmodules file to be already present in the work dir.
+func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error {
+	for _, submodule := range submodules {
+		cmd := NewCommand(ctx, "update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path)
+		if stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}); err != nil {
+			log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err)
+			return err
+		}
+	}
+	return nil
+}
diff --git a/modules/git/submodule_test.go b/modules/git/submodule_test.go
new file mode 100644
index 0000000000000..d53946a27d40f
--- /dev/null
+++ b/modules/git/submodule_test.go
@@ -0,0 +1,48 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package git
+
+import (
+	"context"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetTemplateSubmoduleCommits(t *testing.T) {
+	testRepoPath := filepath.Join(testReposDir, "repo4_submodules")
+	submodules, err := GetTemplateSubmoduleCommits(DefaultContext, testRepoPath)
+	require.NoError(t, err)
+
+	assert.Len(t, submodules, 2)
+
+	assert.EqualValues(t, "<°)))><", submodules[0].Path)
+	assert.EqualValues(t, "d2932de67963f23d43e1c7ecf20173e92ee6c43c", submodules[0].Commit)
+
+	assert.EqualValues(t, "libtest", submodules[1].Path)
+	assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[1].Commit)
+}
+
+func TestAddTemplateSubmoduleIndexes(t *testing.T) {
+	ctx := context.Background()
+	tmpDir := t.TempDir()
+	var err error
+	_, _, err = NewCommand(ctx, "init").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	_ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755)
+	err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}})
+	require.NoError(t, err)
+	_, _, err = NewCommand(ctx, "add", "--all").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	_, _, err = NewCommand(ctx, "-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(&RunOpts{Dir: tmpDir})
+	require.NoError(t, err)
+	submodules, err := GetTemplateSubmoduleCommits(DefaultContext, tmpDir)
+	require.NoError(t, err)
+	assert.Len(t, submodules, 1)
+	assert.EqualValues(t, "new-dir", submodules[0].Path)
+	assert.EqualValues(t, "1234567890123456789012345678901234567890", submodules[0].Commit)
+}
diff --git a/modules/git/tests/repos/repo4_submodules/HEAD b/modules/git/tests/repos/repo4_submodules/HEAD
new file mode 100644
index 0000000000000..cb089cd89a7d7
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/modules/git/tests/repos/repo4_submodules/config b/modules/git/tests/repos/repo4_submodules/config
new file mode 100644
index 0000000000000..07d359d07cf1e
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/config
@@ -0,0 +1,4 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = true
diff --git a/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74 b/modules/git/tests/repos/repo4_submodules/objects/97/c3d30df0e6492348292600920a6482feaebb74
new file mode 100644
index 0000000000000000000000000000000000000000..7596090b49fc8304d4c5599e09fa97e65fd86ceb
GIT binary patch
diff --git a/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34 b/modules/git/tests/repos/repo4_submodules/objects/c7/e064ed49b44523cba8a5dfbc37d2ce1bb41d34
new file mode 100644
index 0000000000000000000000000000000000000000..e3a13c156dce4d306921d33f4414867aea4b58c0
GIT binary patch
diff --git a/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437 b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
new file mode 100644
index 0000000000000..a8d6e5c17c8f2
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/objects/e1/e59caba97193d48862d6809912043871f37437
@@ -0,0 +1,2 @@
+x��[
+�0E��*�_��$M�5tifBk Iŕ�7�k~��9ܘ��ܠ���.j��	�O���"z�`�#I�irF��͹��$%����|4)��?t��=��:K��#[$D����^�����Ӓy�HU/�f?G
\ No newline at end of file
diff --git a/modules/git/tests/repos/repo4_submodules/refs/heads/master b/modules/git/tests/repos/repo4_submodules/refs/heads/master
new file mode 100644
index 0000000000000..102bc34da8ca0
--- /dev/null
+++ b/modules/git/tests/repos/repo4_submodules/refs/heads/master
@@ -0,0 +1 @@
+e1e59caba97193d48862d6809912043871f37437
diff --git a/modules/git/tree.go b/modules/git/tree.go
index d35dc58d8d0ed..5a644f6c87aa1 100644
--- a/modules/git/tree.go
+++ b/modules/git/tree.go
@@ -17,7 +17,7 @@ func NewTree(repo *Repository, id ObjectID) *Tree {
 	}
 }
 
-// SubTree get a sub tree by the sub dir path
+// SubTree get a subtree by the sub dir path
 func (t *Tree) SubTree(rpath string) (*Tree, error) {
 	if len(rpath) == 0 {
 		return t, nil
@@ -63,7 +63,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
 	return filelist, err
 }
 
-// GetTreePathLatestCommitID returns the latest commit of a tree path
+// GetTreePathLatestCommit returns the latest commit of a tree path
 func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
 	stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
 		AddDynamicArguments(refName).AddDashesAndList(treePath).
diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go
index 92d3d107a7425..b7bcf40edd2a9 100644
--- a/modules/git/tree_blob_nogogit.go
+++ b/modules/git/tree_blob_nogogit.go
@@ -17,7 +17,6 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
 			ptree:     t,
 			ID:        t.ID,
 			name:      "",
-			fullName:  "",
 			entryMode: EntryModeTree,
 		}, nil
 	}
diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go
index 1c3bcd197a01d..81fb638d56fbe 100644
--- a/modules/git/tree_entry_nogogit.go
+++ b/modules/git/tree_entry_nogogit.go
@@ -9,23 +9,17 @@ import "code.gitea.io/gitea/modules/log"
 
 // TreeEntry the leaf in the git tree
 type TreeEntry struct {
-	ID ObjectID
-
+	ID    ObjectID
 	ptree *Tree
 
 	entryMode EntryMode
 	name      string
-
-	size     int64
-	sized    bool
-	fullName string
+	size      int64
+	sized     bool
 }
 
 // Name returns the name of the entry
 func (te *TreeEntry) Name() string {
-	if te.fullName != "" {
-		return te.fullName
-	}
 	return te.name
 }
 
diff --git a/services/repository/generate.go b/services/repository/generate.go
index f2280de8b214e..d3b750f7d4830 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -9,7 +9,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"path"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -123,7 +122,7 @@ func (gt *GiteaTemplate) Globs() []glob.Glob {
 	return gt.globs
 }
 
-func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
+func readGiteaTemplateFile(tmpDir string) (*GiteaTemplate, error) {
 	gtPath := filepath.Join(tmpDir, ".gitea", "template")
 	if _, err := os.Stat(gtPath); os.IsNotExist(err) {
 		return nil, nil
@@ -136,12 +135,55 @@ func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
 		return nil, err
 	}
 
-	gt := &GiteaTemplate{
-		Path:    gtPath,
-		Content: content,
+	return &GiteaTemplate{Path: gtPath, Content: content}, nil
+}
+
+func processGiteaTemplateFile(tmpDir string, templateRepo, generateRepo *repo_model.Repository, giteaTemplateFile *GiteaTemplate) error {
+	if err := util.Remove(giteaTemplateFile.Path); err != nil {
+		return fmt.Errorf("remove .giteatemplate: %w", err)
 	}
+	if len(giteaTemplateFile.Globs()) == 0 {
+		return nil // Avoid walking tree if there are no globs
+	}
+	tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
+	return filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
+		if walkErr != nil {
+			return walkErr
+		}
+
+		if d.IsDir() {
+			return nil
+		}
+
+		base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
+		for _, g := range giteaTemplateFile.Globs() {
+			if g.Match(base) {
+				content, err := os.ReadFile(path)
+				if err != nil {
+					return err
+				}
 
-	return gt, nil
+				generatedContent := []byte(generateExpansion(string(content), templateRepo, generateRepo, false))
+				if err := os.WriteFile(path, generatedContent, 0o644); err != nil {
+					return err
+				}
+
+				substPath := filepath.FromSlash(filepath.Join(tmpDirSlash, generateExpansion(base, templateRepo, generateRepo, true)))
+
+				// Create parent subdirectories if needed or continue silently if it exists
+				if err = os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
+					return err
+				}
+
+				// Substitute filename variables
+				if err = os.Rename(path, substPath); err != nil {
+					return err
+				}
+				break
+			}
+		}
+		return nil
+	}) // end: WalkDir
 }
 
 func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
@@ -167,71 +209,30 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		return fmt.Errorf("git clone: %w", err)
 	}
 
-	if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
+	// Get active submodules from the template
+	submodules, err := git.GetTemplateSubmoduleCommits(ctx, tmpDir)
+	if err != nil {
+		return fmt.Errorf("GetTemplateSubmoduleCommits: %w", err)
+	}
+
+	if err = util.RemoveAll(filepath.Join(tmpDir, ".git")); err != nil {
 		return fmt.Errorf("remove git dir: %w", err)
 	}
 
 	// Variable expansion
-	gt, err := checkGiteaTemplate(tmpDir)
+	giteaTemplateFile, err := readGiteaTemplateFile(tmpDir)
 	if err != nil {
-		return fmt.Errorf("checkGiteaTemplate: %w", err)
+		return fmt.Errorf("readGiteaTemplateFile: %w", err)
 	}
 
-	if gt != nil {
-		if err := util.Remove(gt.Path); err != nil {
-			return fmt.Errorf("remove .giteatemplate: %w", err)
-		}
-
-		// Avoid walking tree if there are no globs
-		if len(gt.Globs()) > 0 {
-			tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
-			if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
-				if walkErr != nil {
-					return walkErr
-				}
-
-				if d.IsDir() {
-					return nil
-				}
-
-				base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
-				for _, g := range gt.Globs() {
-					if g.Match(base) {
-						content, err := os.ReadFile(path)
-						if err != nil {
-							return err
-						}
-
-						if err := os.WriteFile(path,
-							[]byte(generateExpansion(string(content), templateRepo, generateRepo, false)),
-							0o644); err != nil {
-							return err
-						}
-
-						substPath := filepath.FromSlash(filepath.Join(tmpDirSlash,
-							generateExpansion(base, templateRepo, generateRepo, true)))
-
-						// Create parent subdirectories if needed or continue silently if it exists
-						if err := os.MkdirAll(filepath.Dir(substPath), 0o755); err != nil {
-							return err
-						}
-
-						// Substitute filename variables
-						if err := os.Rename(path, substPath); err != nil {
-							return err
-						}
-
-						break
-					}
-				}
-				return nil
-			}); err != nil {
-				return err
-			}
+	if giteaTemplateFile != nil {
+		err = processGiteaTemplateFile(tmpDir, templateRepo, generateRepo, giteaTemplateFile)
+		if err != nil {
+			return err
 		}
 	}
 
-	if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
+	if err = git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
 		return err
 	}
 
@@ -243,6 +244,10 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r
 		return fmt.Errorf("git remote add: %w", err)
 	}
 
+	if err = git.AddTemplateSubmoduleIndexes(ctx, tmpDir, submodules); err != nil {
+		return fmt.Errorf("failed to add submodules: %v", err)
+	}
+
 	// set default branch based on whether it's specified in the newly generated repo or not
 	defaultBranch := repo.DefaultBranch
 	if strings.TrimSpace(defaultBranch) == "" {
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 2d555e4c2e9ec..c50a0d312f8c8 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -19,9 +19,9 @@
 					{{svg "octicon-file-submodule"}}
 					{{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{/* FIXME: the usage of AppUrl seems incorrect, it would be fixed in the future, use AppSubUrl instead */}}
 					{{if $refURL}}
-						<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a><span class="at">@</span><a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
+						<a class="muted" href="{{$refURL}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$refURL}}/commit/{{PathEscape $subModuleFile.RefID}}">{{ShortSha $subModuleFile.RefID}}</a>
 					{{else}}
-						{{$entry.Name}}<span class="at">@</span>{{ShortSha $subModuleFile.RefID}}
+						{{$entry.Name}} <span class="at">@</span> {{ShortSha $subModuleFile.RefID}}
 					{{end}}
 				{{else}}
 					{{if $entry.IsDir}}

From d371aa30318e98b080049ca55121d82c09e3138b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 2 Jan 2025 13:02:46 +0800
Subject: [PATCH 043/206] Try to fix ACME directory problem (#33072) (#33077)

Backport #33072 by wxiaoguang

Haven't really confirmed, but I think it might fix #32191

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 cmd/web_acme.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/web_acme.go b/cmd/web_acme.go
index 90e4a02764b67..2fe14c1f54c04 100644
--- a/cmd/web_acme.go
+++ b/cmd/web_acme.go
@@ -54,7 +54,7 @@ func runACME(listenAddr string, m http.Handler) error {
 		altTLSALPNPort = p
 	}
 
-	magic := certmagic.NewDefault()
+	magic := &certmagic.Default
 	magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
 	// Try to use private CA root if provided, otherwise defaults to system's trust
 	var certPool *x509.CertPool

From 7a35f90b2971a7d569af14a6426db5b1c6a74c1f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 3 Jan 2025 03:57:34 +0800
Subject: [PATCH 044/206] Fix broken forms (#33082)

---
 routers/web/repo/repo.go | 4 ++++
 web_src/css/form.css     | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index f5e59b0357b02..0b57547c9c5c4 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -228,8 +228,12 @@ func CreatePost(ctx *context.Context) {
 	ctx.Data["Licenses"] = repo_module.Licenses
 	ctx.Data["Readmes"] = repo_module.Readmes
 
+	// the logic is still buggy, the complete fix is in 1.24
+	ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
 	ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
 	ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
+	ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
+	ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
 
 	ctxUser := checkContextUser(ctx, form.UID)
 	if ctx.Written() {
diff --git a/web_src/css/form.css b/web_src/css/form.css
index 5dd5e05bec7f0..26f00e4d994e9 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -483,7 +483,7 @@ textarea:focus,
     margin-bottom: 1em;
     width: 100%;
   }
-  .new.org .ui.form .field input {
+  .new.org .ui.form .field input:not([type="checkbox"], [type="radio"]) {
     width: 100% !important;
   }
 }

From e10d222434d441d7847716eaea95577a48e31ca3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 3 Jan 2025 05:45:14 +0800
Subject: [PATCH 045/206] Fix bleve fuzziness search (#33078) (#33087)

---
 custom/conf/app.example.ini                 |  4 +++
 modules/indexer/code/indexer.go             |  3 +-
 modules/indexer/code/indexer_test.go        |  4 ++-
 modules/indexer/internal/bleve/util.go      |  9 ++---
 modules/indexer/internal/bleve/util_test.go |  7 +++-
 modules/setting/indexer.go                  |  3 ++
 routers/common/codesearch.go                | 39 +++++++++++++++++++++
 routers/web/explore/code.go                 | 21 ++++-------
 routers/web/repo/search.go                  | 26 +++++---------
 routers/web/user/code.go                    | 22 ++++--------
 templates/shared/search/code/search.tmpl    |  3 +-
 11 files changed, 86 insertions(+), 55 deletions(-)
 create mode 100644 routers/common/codesearch.go

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 6377ebf9d2ac2..6896b073e1f51 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -1482,6 +1482,10 @@ LEVEL = Info
 ;REPO_INDEXER_EXCLUDE =
 ;;
 ;MAX_FILE_SIZE = 1048576
+;;
+;; Bleve engine has performance problems with fuzzy search, so we limit the fuzziness to 0 by default to disable it.
+;; If you'd like to enable it, you can set it to a value between 0 and 2.
+;TYPE_BLEVE_MAX_FUZZINESS = 0
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go
index c1ab26569c6f4..728b37fab6ecd 100644
--- a/modules/indexer/code/indexer.go
+++ b/modules/indexer/code/indexer.go
@@ -123,13 +123,12 @@ func Init() {
 			for _, indexerData := range items {
 				log.Trace("IndexerData Process Repo: %d", indexerData.RepoID)
 				if err := index(ctx, indexer, indexerData.RepoID); err != nil {
-					unhandled = append(unhandled, indexerData)
 					if !setting.IsInTesting {
 						log.Error("Codes indexer handler: index error for repo %v: %v", indexerData.RepoID, err)
 					}
 				}
 			}
-			return unhandled
+			return nil // do not re-queue the failed items, otherwise some broken repo will block the queue
 		}
 
 		indexerQueue = queue.CreateUniqueQueue(ctx, "code_indexer", handler)
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index d04088531ac28..f358bbe785110 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -15,6 +15,8 @@ import (
 	"code.gitea.io/gitea/modules/indexer/code/bleve"
 	"code.gitea.io/gitea/modules/indexer/code/elasticsearch"
 	"code.gitea.io/gitea/modules/indexer/code/internal"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 
 	_ "code.gitea.io/gitea/models"
 	_ "code.gitea.io/gitea/models/actions"
@@ -279,7 +281,7 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 
 func TestBleveIndexAndSearch(t *testing.T) {
 	unittest.PrepareTestEnv(t)
-
+	defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)()
 	dir := t.TempDir()
 
 	idx := bleve.NewIndexer(dir)
diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go
index a0c3dc4ad459e..b6daa9e14b46a 100644
--- a/modules/indexer/internal/bleve/util.go
+++ b/modules/indexer/internal/bleve/util.go
@@ -9,6 +9,7 @@ import (
 	"unicode"
 
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 
 	"github.com/blevesearch/bleve/v2"
@@ -54,9 +55,9 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) {
 	return index, 0, nil
 }
 
-// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars
-// may be different on two string and they still be considered equivalent.
-// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero.
+// GuessFuzzinessByKeyword guesses fuzziness based on the levenshtein distance and determines how many chars
+// may be different on two string, and they still be considered equivalent.
+// Given a phrase, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero.
 func GuessFuzzinessByKeyword(s string) int {
 	tokenizer := unicode_tokenizer.NewUnicodeTokenizer()
 	tokens := tokenizer.Tokenize([]byte(s))
@@ -85,5 +86,5 @@ func guessFuzzinessByKeyword(s string) int {
 			return 0
 		}
 	}
-	return min(maxFuzziness, len(s)/4)
+	return min(min(setting.Indexer.TypeBleveMaxFuzzniess, maxFuzziness), len(s)/4)
 }
diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go
index 8f7844464e739..1a7e4db0f4234 100644
--- a/modules/indexer/internal/bleve/util_test.go
+++ b/modules/indexer/internal/bleve/util_test.go
@@ -7,10 +7,15 @@ import (
 	"fmt"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
 	"github.com/stretchr/testify/assert"
 )
 
 func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
+	defer test.MockVariableValue(&setting.Indexer.TypeBleveMaxFuzzniess, 2)()
+
 	scenarios := []struct {
 		Input     string
 		Fuzziness int // See util.go for the definition of fuzziness in this particular context
@@ -46,7 +51,7 @@ func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
 	}
 
 	for _, scenario := range scenarios {
-		t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) {
+		t.Run(fmt.Sprintf("Fuziniess:%s=%d", scenario.Input, scenario.Fuzziness), func(t *testing.T) {
 			assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input))
 		})
 	}
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index 34b4eac15b971..e34baae012b32 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -31,6 +31,8 @@ var Indexer = struct {
 	IncludePatterns      []*GlobMatcher
 	ExcludePatterns      []*GlobMatcher
 	ExcludeVendored      bool
+
+	TypeBleveMaxFuzzniess int
 }{
 	IssueType:        "bleve",
 	IssuePath:        "indexers/issues.bleve",
@@ -88,6 +90,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
 	Indexer.ExcludeVendored = sec.Key("REPO_INDEXER_EXCLUDE_VENDORED").MustBool(true)
 	Indexer.MaxIndexerFileSize = sec.Key("MAX_FILE_SIZE").MustInt64(1024 * 1024)
 	Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second)
+	Indexer.TypeBleveMaxFuzzniess = sec.Key("TYPE_BLEVE_MAX_FUZZINESS").MustInt(0)
 }
 
 // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go
new file mode 100644
index 0000000000000..a14af126e55bc
--- /dev/null
+++ b/routers/common/codesearch.go
@@ -0,0 +1,39 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/services/context"
+)
+
+func PrepareCodeSearch(ctx *context.Context) (ret struct {
+	Keyword  string
+	Language string
+	IsFuzzy  bool
+},
+) {
+	ret.Language = ctx.FormTrim("l")
+	ret.Keyword = ctx.FormTrim("q")
+
+	fuzzyDefault := setting.Indexer.RepoIndexerEnabled
+	fuzzyAllow := true
+	if setting.Indexer.RepoType == "bleve" && setting.Indexer.TypeBleveMaxFuzzniess == 0 {
+		fuzzyDefault = false
+		fuzzyAllow = false
+	}
+	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault)
+	if isFuzzy && !fuzzyAllow {
+		ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons")
+		isFuzzy = false
+	}
+
+	ctx.Data["IsBleveFuzzyDisabled"] = true
+	ctx.Data["Keyword"] = ret.Keyword
+	ctx.Data["Language"] = ret.Language
+	ctx.Data["IsFuzzy"] = isFuzzy
+
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	return ret
+}
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 48f890332bd33..3fca36c9abaaf 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -32,18 +33,10 @@ func Code(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("explore")
 	ctx.Data["PageIsExplore"] = true
 	ctx.Data["PageIsExploreCode"] = true
-
-	language := ctx.FormTrim("l")
-	keyword := ctx.FormTrim("q")
-
-	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
-
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Language"] = language
-	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
 
-	if keyword == "" {
+	prepareSearch := common.PrepareCodeSearch(ctx)
+	if prepareSearch.Keyword == "" {
 		ctx.HTML(http.StatusOK, tplExploreCode)
 		return
 	}
@@ -80,9 +73,9 @@ func Code(ctx *context.Context) {
 	if (len(repoIDs) > 0) || isAdmin {
 		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
 			RepoIDs:        repoIDs,
-			Keyword:        keyword,
-			IsKeywordFuzzy: isFuzzy,
-			Language:       language,
+			Keyword:        prepareSearch.Keyword,
+			IsKeywordFuzzy: prepareSearch.IsFuzzy,
+			Language:       prepareSearch.Language,
 			Paginator: &db.ListOptions{
 				Page:     page,
 				PageSize: setting.UI.RepoSearchPagingNum,
@@ -138,7 +131,7 @@ func Code(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamString("l", language)
+	pager.AddParamString("l", prepareSearch.Language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplExploreCode)
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 920a865555bec..d5772ff79d2fa 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -12,6 +12,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/context"
 )
 
@@ -29,18 +30,9 @@ func indexSettingToGitGrepPathspecList() (list []string) {
 
 // Search render repository search page
 func Search(ctx *context.Context) {
-	language := ctx.FormTrim("l")
-	keyword := ctx.FormTrim("q")
-
-	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
-
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Language"] = language
-	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["PageIsViewCode"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	if keyword == "" {
+	prepareSearch := common.PrepareCodeSearch(ctx)
+	if prepareSearch.Keyword == "" {
 		ctx.HTML(http.StatusOK, tplSearch)
 		return
 	}
@@ -57,9 +49,9 @@ func Search(ctx *context.Context) {
 		var err error
 		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
 			RepoIDs:        []int64{ctx.Repo.Repository.ID},
-			Keyword:        keyword,
-			IsKeywordFuzzy: isFuzzy,
-			Language:       language,
+			Keyword:        prepareSearch.Keyword,
+			IsKeywordFuzzy: prepareSearch.IsFuzzy,
+			Language:       prepareSearch.Language,
 			Paginator: &db.ListOptions{
 				Page:     page,
 				PageSize: setting.UI.RepoSearchPagingNum,
@@ -75,9 +67,9 @@ func Search(ctx *context.Context) {
 			ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
 		}
 	} else {
-		res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{
+		res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{
 			ContextLineNumber: 1,
-			IsFuzzy:           isFuzzy,
+			IsFuzzy:           prepareSearch.IsFuzzy,
 			RefName:           git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch
 			PathspecList:      indexSettingToGitGrepPathspecList(),
 		})
@@ -109,7 +101,7 @@ func Search(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamString("l", language)
+	pager.AddParamString("l", prepareSearch.Language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 785c37b1243c6..8f4b4b2d9ac7c 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -11,6 +11,7 @@ import (
 	"code.gitea.io/gitea/modules/base"
 	code_indexer "code.gitea.io/gitea/modules/indexer/code"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/routers/common"
 	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	"code.gitea.io/gitea/services/context"
 )
@@ -34,20 +35,11 @@ func CodeSearch(ctx *context.Context) {
 	}
 
 	ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 	ctx.Data["Title"] = ctx.Tr("explore.code")
-
-	language := ctx.FormTrim("l")
-	keyword := ctx.FormTrim("q")
-
-	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true)
-
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Language"] = language
-	ctx.Data["IsFuzzy"] = isFuzzy
 	ctx.Data["IsCodePage"] = true
 
-	if keyword == "" {
+	prepareSearch := common.PrepareCodeSearch(ctx)
+	if prepareSearch.Keyword == "" {
 		ctx.HTML(http.StatusOK, tplUserCode)
 		return
 	}
@@ -77,9 +69,9 @@ func CodeSearch(ctx *context.Context) {
 	if len(repoIDs) > 0 {
 		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
 			RepoIDs:        repoIDs,
-			Keyword:        keyword,
-			IsKeywordFuzzy: isFuzzy,
-			Language:       language,
+			Keyword:        prepareSearch.Keyword,
+			IsKeywordFuzzy: prepareSearch.IsFuzzy,
+			Language:       prepareSearch.Language,
 			Paginator: &db.ListOptions{
 				Page:     page,
 				PageSize: setting.UI.RepoSearchPagingNum,
@@ -122,7 +114,7 @@ func CodeSearch(ctx *context.Context) {
 
 	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
 	pager.SetDefaultParams(ctx)
-	pager.AddParamString("l", language)
+	pager.AddParamString("l", prepareSearch.Language)
 	ctx.Data["Page"] = pager
 
 	ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/templates/shared/search/code/search.tmpl b/templates/shared/search/code/search.tmpl
index e49ea47e03482..dde45c0fbfa01 100644
--- a/templates/shared/search/code/search.tmpl
+++ b/templates/shared/search/code/search.tmpl
@@ -2,7 +2,8 @@
 	{{template "shared/search/combo_fuzzy" dict "Value" .Keyword "Disabled" .CodeIndexerUnavailable "IsFuzzy" .IsFuzzy "Placeholder" (ctx.Locale.Tr "search.code_kind")}}
 </form>
 <div class="divider"></div>
-<div class="ui user list">
+<div class="ui list">
+	{{template "base/alert" .}}
 	{{if .CodeIndexerUnavailable}}
 		<div class="ui error message">
 			<p>{{ctx.Locale.Tr "search.code_search_unavailable"}}</p>

From fd281518aecb85be4984ad31a90698a2386d62a5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 4 Jan 2025 05:50:39 +0800
Subject: [PATCH 046/206] Fix line-number and scroll bugs (#33094) (#33095)

Partially backport #33094

Fix the scroll bug in issue/pr view page.

Fix a JS error when line number exceeds the max
---
 web_src/css/base.css              |  1 +
 web_src/js/features/repo-code.ts  |  2 +-
 web_src/js/features/repo-issue.ts | 22 +++++-----------------
 3 files changed, 7 insertions(+), 18 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 04f3678f3a71a..a0bfefebf5ac1 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -693,6 +693,7 @@ input:-webkit-autofill:active,
 
 .ui.dropdown .menu > .header {
   font-size: 0.8em;
+  text-transform: none; /* reset fomantic's "uppercase" */
 }
 
 .ui .text.left {
diff --git a/web_src/js/features/repo-code.ts b/web_src/js/features/repo-code.ts
index a8d6e8f97ddd5..7b5b348db5d50 100644
--- a/web_src/js/features/repo-code.ts
+++ b/web_src/js/features/repo-code.ts
@@ -59,7 +59,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) {
     copyPermalink.setAttribute('data-url', link);
   };
 
-  if ($selectionStartEls) {
+  if ($selectionStartEls && $selectionStartEls[0]) {
     let a = parseInt($selectionEndEl[0].getAttribute('rel').slice(1));
     let b = parseInt($selectionStartEls[0].getAttribute('rel').slice(1));
     let c;
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index 7541039786830..39eff21347c3d 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -371,10 +371,6 @@ export async function handleReply(el) {
 
 export function initRepoPullRequestReview() {
   if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) {
-    // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
-    if (window.history.scrollRestoration !== 'manual') {
-      window.history.scrollRestoration = 'manual';
-    }
     const commentDiv = document.querySelector(window.location.hash);
     if (commentDiv) {
       // get the name of the parent id
@@ -382,14 +378,6 @@ export function initRepoPullRequestReview() {
       if (groupID && groupID.startsWith('code-comments-')) {
         const id = groupID.slice(14);
         const ancestorDiffBox = commentDiv.closest('.diff-file-box');
-        // on pages like conversation, there is no diff header
-        const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
-
-        // offset is for scrolling
-        let offset = 30;
-        if (diffHeader) {
-          offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
-        }
 
         hideElem(`#show-outdated-${id}`);
         showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
@@ -397,12 +385,12 @@ export function initRepoPullRequestReview() {
         if (ancestorDiffBox?.getAttribute('data-folded') === 'true') {
           setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
         }
-
-        window.scrollTo({
-          top: $(commentDiv).offset().top - offset,
-          behavior: 'instant',
-        });
       }
+
+      // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
+      if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual';
+      // wait for a while because some elements (eg: image, editor, etc.) may change the viewport's height.
+      setTimeout(() => commentDiv.scrollIntoView({block: 'start'}), 100);
     }
   }
 

From e637008fe3908a0027606846eab367e697ccc71d Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 5 Jan 2025 23:18:02 +0800
Subject: [PATCH 047/206] Fix empty git repo handling logic and fix mobile view
 (#33101) (#33102)

Backport #33101 and UI fix from main (including #33108)
---
 models/repo/repo.go                  |  2 ++
 options/locale/locale_en-US.ini      |  1 +
 routers/web/repo/view_home.go        | 46 ++++++++++++++++++----------
 services/context/repo.go             |  3 --
 templates/base/head_navbar.tmpl      |  6 ++--
 templates/repo/commit_page.tmpl      |  2 +-
 templates/repo/empty.tmpl            | 21 ++++++-------
 templates/repo/header.tmpl           |  2 +-
 templates/user/dashboard/navbar.tmpl |  4 +--
 web_src/css/base.css                 | 10 +++---
 web_src/css/modules/navbar.css       |  3 +-
 11 files changed, 56 insertions(+), 44 deletions(-)

diff --git a/models/repo/repo.go b/models/repo/repo.go
index 8d211caf40b90..c5060a419f458 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -276,6 +276,8 @@ func (repo *Repository) IsBroken() bool {
 }
 
 // MarkAsBrokenEmpty marks the repo as broken and empty
+// FIXME: the status "broken" and "is_empty" were abused,
+// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
 func (repo *Repository) MarkAsBrokenEmpty() {
 	repo.Status = RepositoryBroken
 	repo.IsEmpty = true
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 16bd0fe8b6edd..2c10a4dbe1f8e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1231,6 +1231,7 @@ create_new_repo_command = Creating a new repository on the command line
 push_exist_repo = Pushing an existing repository from the command line
 empty_message = This repository does not contain any content.
 broken_message = The Git data underlying this repository cannot be read. Contact the administrator of this instance or delete this repository.
+no_branch = This repository doesn’t have any branches.
 
 code = Code
 code.desc = Access source code, files, commits and branches.
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index b318c4a621a60..1d886a5ffb4ca 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -223,16 +223,37 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
 	}
 }
 
+func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
+	ctx.Repo.Repository.IsEmpty = empty
+	if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken {
+		ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is
+	}
+	if err := repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
+		ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err)
+		return
+	}
+}
+
 func handleRepoEmptyOrBroken(ctx *context.Context) {
 	showEmpty := true
-	var err error
 	if ctx.Repo.GitRepo != nil {
-		showEmpty, err = ctx.Repo.GitRepo.IsEmpty()
+		reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
 		if err != nil {
+			showEmpty = true // the repo is broken
+			updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryBroken)
 			log.Error("GitRepo.IsEmpty: %v", err)
-			ctx.Repo.Repository.Status = repo_model.RepositoryBroken
-			showEmpty = true
 			ctx.Flash.Error(ctx.Tr("error.occurred"), true)
+		} else if reallyEmpty {
+			showEmpty = true // the repo is really empty
+			updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
+		} else if ctx.Repo.Commit == nil {
+			showEmpty = true // it is not really empty, but there is no branch
+			// at the moment, other repo units like "actions" are not able to handle such case,
+			// so we just mark the repo as empty to prevent from displaying these units.
+			updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
+		} else {
+			// the repo is actually not empty and has branches, need to update the database later
+			showEmpty = false
 		}
 	}
 	if showEmpty {
@@ -240,18 +261,11 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
 		return
 	}
 
-	// the repo is not really empty, so we should update the modal in database
-	// such problem may be caused by:
-	// 1) an error occurs during pushing/receiving.  2) the user replaces an empty git repo manually
-	// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos.
-	// it's possible for a repository to be non-empty by that flag but still 500
-	// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed.
-	ctx.Repo.Repository.IsEmpty = false
-	if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil {
-		ctx.ServerError("UpdateRepositoryCols", err)
-		return
-	}
-	if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
+	// The repo is not really empty, so we should update the model in database, such problem may be caused by:
+	// 1) an error occurs during pushing/receiving.
+	// 2) the user replaces an empty git repo manually.
+	updateContextRepoEmptyAndStatus(ctx, false, repo_model.RepositoryReady)
+	if err := repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil {
 		ctx.ServerError("UpdateRepoSize", err)
 		return
 	}
diff --git a/services/context/repo.go b/services/context/repo.go
index 9b5443911030a..1e7c4303471e0 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -908,10 +908,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
 					refName = brs[0].Name
 				} else if len(brs) == 0 {
 					log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
-					ctx.Repo.Repository.MarkAsBrokenEmpty()
 				} else {
 					log.Error("GetBranches error: %v", err)
-					ctx.Repo.Repository.MarkAsBrokenEmpty()
 				}
 			}
 			ctx.Repo.RefName = refName
@@ -922,7 +920,6 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
 			} else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") {
 				// if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users
 				log.Error("GetBranchCommit: %v", err)
-				ctx.Repo.Repository.MarkAsBrokenEmpty()
 			} else {
 				ctx.ServerError("GetBranchCommit", err)
 				return cancel
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 951ee590d1cc7..bbb2a7ef0172f 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -11,7 +11,7 @@
 		</a>
 
 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
-		<div class="ui secondary menu item navbar-mobile-right only-mobile">
+		<div class="ui secondary menu navbar-mobile-right only-mobile">
 			{{if and .IsSigned EnableTimetracking .ActiveStopwatch}}
 			<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}">
 				<div class="tw-relative">
@@ -70,7 +70,7 @@
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
-					<div class="ui header">
+					<div class="header">
 						{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
 					</div>
 
@@ -128,7 +128,7 @@
 					<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
 				</span>
 				<div class="menu user-menu">
-					<div class="ui header">
+					<div class="header">
 						{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
 					</div>
 
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 71f77154fb923..38e3df8e709ef 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -30,7 +30,7 @@
 								{{ctx.Locale.Tr "repo.commit.operations"}}
 								{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 								<div class="menu">
-									<div class="ui header">{{ctx.Locale.Tr "repo.commit.operations"}}</div>
+									<div class="header">{{ctx.Locale.Tr "repo.commit.operations"}}</div>
 									<div class="divider"></div>
 									<div class="item show-create-branch-modal"
 										data-content="{{ctx.Locale.Tr "repo.branch.new_branch_from" (.CommitID)}}" {{/* used by the form */}}
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index 7170fe360203d..dfda5b7b2b4d2 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -14,14 +14,13 @@
 						{{end}}
 					</div>
 				{{end}}
+
 				{{if .Repository.IsBroken}}
-						<div class="ui segment center">
-							{{ctx.Locale.Tr "repo.broken_message"}}
-						</div>
+					<div class="ui segment center">{{ctx.Locale.Tr "repo.broken_message"}}</div>
+				{{else if .Repository.IsEmpty}}
+					<div class="ui segment center">{{ctx.Locale.Tr "repo.no_branch"}}</div>
 				{{else if .CanWriteCode}}
-					<h4 class="ui top attached header">
-						{{ctx.Locale.Tr "repo.quick_guide"}}
-					</h4>
+					<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.quick_guide"}}</h4>
 					<div class="ui attached guide table segment empty-repo-guide">
 						<div class="item">
 							<h3>{{ctx.Locale.Tr "repo.clone_this_repo"}} <small>{{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}</small></h3>
@@ -66,12 +65,10 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre>
 								</div>
 							</div>
 						{{end}}
-					{{else}}
-						<div class="ui segment center">
-							{{ctx.Locale.Tr "repo.empty_message"}}
-						</div>
-					{{end}}
-				</div>
+					</div>
+				{{else}}
+					<div class="ui segment center">{{ctx.Locale.Tr "repo.empty_message"}}</div>
+				{{end}}
 			</div>
 		</div>
 	</div>
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index c3ae697f31fbf..e187ef1a87dd7 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -162,7 +162,7 @@
 						</a>
 					{{end}}
 
-					{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}}
+					{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
 						<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
 							{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
 							{{if .Repository.NumOpenActionRuns}}
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl
index 7982cbd9509af..a828bc90e44c5 100644
--- a/templates/user/dashboard/navbar.tmpl
+++ b/templates/user/dashboard/navbar.tmpl
@@ -12,7 +12,7 @@
 					{{svg "octicon-triangle-down" 14 "dropdown icon tw-ml-1"}}
 				</span>
 				<div class="context user overflow menu">
-					<div class="ui header">
+					<div class="header">
 						{{ctx.Locale.Tr "home.switch_dashboard_context"}}
 					</div>
 					<div class="scrolling menu items">
@@ -56,7 +56,7 @@
 					</span>
 					{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 					<div class="context user overflow menu">
-						<div class="ui header">
+						<div class="header">
 							{{ctx.Locale.Tr "home.filter_by_team_repositories"}}
 						</div>
 						<div class="scrolling menu items">
diff --git a/web_src/css/base.css b/web_src/css/base.css
index a0bfefebf5ac1..1a354c96633fe 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -336,8 +336,13 @@ a.label,
   border-color: var(--color-secondary);
 }
 
+.ui.dropdown .menu > .header {
+  text-transform: none; /* reset fomantic's "uppercase" */
+}
+
 .ui.dropdown .menu > .header:not(.ui) {
   color: var(--color-text);
+  font-size: 0.95em; /* reset fomantic's small font-size */
 }
 
 .ui.dropdown .menu > .item {
@@ -691,11 +696,6 @@ input:-webkit-autofill:active,
   box-shadow: 0 6px 18px var(--color-shadow) !important;
 }
 
-.ui.dropdown .menu > .header {
-  font-size: 0.8em;
-  text-transform: none; /* reset fomantic's "uppercase" */
-}
-
 .ui .text.left {
   text-align: left !important;
 }
diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index 556da2df3b051..73914d9a21590 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -66,7 +66,8 @@
     align-items: stretch;
   }
   /* hide all items */
-  #navbar .item {
+  #navbar .navbar-left > .item,
+  #navbar .navbar-right > .item {
     display: none;
   }
   #navbar #navbar-logo {

From 0fad40dd8c5d0b0769292fa80d693ccbeb5bf183 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 6 Jan 2025 14:17:28 +0800
Subject: [PATCH 048/206] Fix package error handling and npm meta and empty
 repo guide (#33112)

---
 modules/packages/npm/creator.go             |  2 +
 modules/packages/npm/metadata.go            |  1 +
 routers/api/packages/container/blob.go      |  9 ++--
 routers/api/packages/container/container.go | 18 ++++----
 routers/api/packages/container/manifest.go  | 31 +++++++-------
 routers/api/packages/npm/api.go             |  1 +
 routers/web/repo/view_home.go               |  1 +
 templates/repo/empty.tmpl                   |  2 +-
 tests/integration/api_packages_npm_test.go  | 47 +++++++++++++--------
 tests/integration/empty_repo_test.go        |  6 ++-
 10 files changed, 69 insertions(+), 49 deletions(-)

diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go
index 7d3d7cd6b55f3..8ba4dbfba710c 100644
--- a/modules/packages/npm/creator.go
+++ b/modules/packages/npm/creator.go
@@ -81,6 +81,7 @@ type PackageMetadataVersion struct {
 	BundleDependencies   []string            `json:"bundleDependencies,omitempty"`
 	DevDependencies      map[string]string   `json:"devDependencies,omitempty"`
 	PeerDependencies     map[string]string   `json:"peerDependencies,omitempty"`
+	PeerDependenciesMeta map[string]any      `json:"peerDependenciesMeta,omitempty"`
 	Bin                  map[string]string   `json:"bin,omitempty"`
 	OptionalDependencies map[string]string   `json:"optionalDependencies,omitempty"`
 	Readme               string              `json:"readme,omitempty"`
@@ -222,6 +223,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
 				BundleDependencies:      meta.BundleDependencies,
 				DevelopmentDependencies: meta.DevDependencies,
 				PeerDependencies:        meta.PeerDependencies,
+				PeerDependenciesMeta:    meta.PeerDependenciesMeta,
 				OptionalDependencies:    meta.OptionalDependencies,
 				Bin:                     meta.Bin,
 				Readme:                  meta.Readme,
diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go
index 6bb77f302bb16..d1d026338780c 100644
--- a/modules/packages/npm/metadata.go
+++ b/modules/packages/npm/metadata.go
@@ -19,6 +19,7 @@ type Metadata struct {
 	BundleDependencies      []string          `json:"bundleDependencies,omitempty"`
 	DevelopmentDependencies map[string]string `json:"development_dependencies,omitempty"`
 	PeerDependencies        map[string]string `json:"peer_dependencies,omitempty"`
+	PeerDependenciesMeta    map[string]any    `json:"peer_dependencies_meta,omitempty"`
 	OptionalDependencies    map[string]string `json:"optional_dependencies,omitempty"`
 	Bin                     map[string]string `json:"bin,omitempty"`
 	Readme                  string            `json:"readme,omitempty"`
diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go
index 4595b9a33deb4..671803788aa34 100644
--- a/routers/api/packages/container/blob.go
+++ b/routers/api/packages/container/blob.go
@@ -111,12 +111,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
 		}
 		var err error
 		if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-			if err == packages_model.ErrDuplicatePackage {
-				created = false
-			} else {
+			if !errors.Is(err, packages_model.ErrDuplicatePackage) {
 				log.Error("Error inserting package: %v", err)
 				return err
 			}
+			created = false
 		}
 
 		if created {
@@ -135,7 +134,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
 			MetadataJSON: "null",
 		}
 		if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
-			if err != packages_model.ErrDuplicatePackageVersion {
+			if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
 				log.Error("Error inserting package: %v", err)
 				return err
 			}
@@ -161,7 +160,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p
 	}
 	var err error
 	if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
-		if err == packages_model.ErrDuplicatePackageFile {
+		if errors.Is(err, packages_model.ErrDuplicatePackageFile) {
 			return nil
 		}
 		log.Error("Error inserting package file: %v", err)
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 3a470ad68523a..bb14db9db79d2 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -324,7 +324,7 @@ func GetUploadBlob(ctx *context.Context) {
 
 	upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
 	if err != nil {
-		if err == packages_model.ErrPackageBlobUploadNotExist {
+		if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
 			apiErrorDefined(ctx, errBlobUploadUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -345,7 +345,7 @@ func UploadBlob(ctx *context.Context) {
 
 	uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
 	if err != nil {
-		if err == packages_model.ErrPackageBlobUploadNotExist {
+		if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
 			apiErrorDefined(ctx, errBlobUploadUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -396,7 +396,7 @@ func EndUploadBlob(ctx *context.Context) {
 
 	uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
 	if err != nil {
-		if err == packages_model.ErrPackageBlobUploadNotExist {
+		if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
 			apiErrorDefined(ctx, errBlobUploadUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -465,7 +465,7 @@ func CancelUploadBlob(ctx *context.Context) {
 
 	_, err := packages_model.GetBlobUploadByID(ctx, uuid)
 	if err != nil {
-		if err == packages_model.ErrPackageBlobUploadNotExist {
+		if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) {
 			apiErrorDefined(ctx, errBlobUploadUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -501,7 +501,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri
 func HeadBlob(ctx *context.Context) {
 	blob, err := getBlobFromContext(ctx)
 	if err != nil {
-		if err == container_model.ErrContainerBlobNotExist {
+		if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 			apiErrorDefined(ctx, errBlobUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -520,7 +520,7 @@ func HeadBlob(ctx *context.Context) {
 func GetBlob(ctx *context.Context) {
 	blob, err := getBlobFromContext(ctx)
 	if err != nil {
-		if err == container_model.ErrContainerBlobNotExist {
+		if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 			apiErrorDefined(ctx, errBlobUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -639,7 +639,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe
 func HeadManifest(ctx *context.Context) {
 	manifest, err := getManifestFromContext(ctx)
 	if err != nil {
-		if err == container_model.ErrContainerBlobNotExist {
+		if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 			apiErrorDefined(ctx, errManifestUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -659,7 +659,7 @@ func HeadManifest(ctx *context.Context) {
 func GetManifest(ctx *context.Context) {
 	manifest, err := getManifestFromContext(ctx)
 	if err != nil {
-		if err == container_model.ErrContainerBlobNotExist {
+		if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 			apiErrorDefined(ctx, errManifestUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
@@ -739,7 +739,7 @@ func GetTagList(ctx *context.Context) {
 	image := ctx.PathParam("image")
 
 	if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
-		if err == packages_model.ErrPackageNotExist {
+		if errors.Is(err, packages_model.ErrPackageNotExist) {
 			apiErrorDefined(ctx, errNameUnknown)
 		} else {
 			apiError(ctx, http.StatusInternalServerError, err)
diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go
index 4a79a58f51f09..ad035cf473e7c 100644
--- a/routers/api/packages/container/manifest.go
+++ b/routers/api/packages/container/manifest.go
@@ -240,7 +240,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b
 				IsManifest: true,
 			})
 			if err != nil {
-				if err == container_model.ErrContainerBlobNotExist {
+				if errors.Is(err, container_model.ErrContainerBlobNotExist) {
 					return errManifestBlobUnknown
 				}
 				return err
@@ -321,12 +321,11 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
 	}
 	var err error
 	if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
-		if err == packages_model.ErrDuplicatePackage {
-			created = false
-		} else {
+		if !errors.Is(err, packages_model.ErrDuplicatePackage) {
 			log.Error("Error inserting package: %v", err)
 			return nil, err
 		}
+		created = false
 	}
 
 	if created {
@@ -352,21 +351,23 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
 	}
 	var pv *packages_model.PackageVersion
 	if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
-		if err == packages_model.ErrDuplicatePackageVersion {
-			if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
-				return nil, err
-			}
+		if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
+			log.Error("Error inserting package: %v", err)
+			return nil, err
+		}
 
-			// keep download count on overwrite
-			_pv.DownloadCount = pv.DownloadCount
+		if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
+			return nil, err
+		}
+
+		// keep download count on overwrite
+		_pv.DownloadCount = pv.DownloadCount
 
-			if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
+		if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
+			if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
 				log.Error("Error inserting package: %v", err)
 				return nil, err
 			}
-		} else {
-			log.Error("Error inserting package: %v", err)
-			return nil, err
 		}
 	}
 
@@ -417,7 +418,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package
 	}
 	var err error
 	if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
-		if err == packages_model.ErrDuplicatePackageFile {
+		if errors.Is(err, packages_model.ErrDuplicatePackageFile) {
 			// Skip this blob because the manifest contains the same filesystem layer multiple times.
 			return nil
 		}
diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go
index b4379f3f49702..636680242a480 100644
--- a/routers/api/packages/npm/api.go
+++ b/routers/api/packages/npm/api.go
@@ -67,6 +67,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
 		BundleDependencies:   metadata.BundleDependencies,
 		DevDependencies:      metadata.DevelopmentDependencies,
 		PeerDependencies:     metadata.PeerDependencies,
+		PeerDependenciesMeta: metadata.PeerDependenciesMeta,
 		OptionalDependencies: metadata.OptionalDependencies,
 		Readme:               metadata.Readme,
 		Bin:                  metadata.Bin,
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index 1d886a5ffb4ca..a5b541f214a60 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -250,6 +250,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
 			showEmpty = true // it is not really empty, but there is no branch
 			// at the moment, other repo units like "actions" are not able to handle such case,
 			// so we just mark the repo as empty to prevent from displaying these units.
+			ctx.Data["RepoHasContentsWithoutBranch"] = true
 			updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
 		} else {
 			// the repo is actually not empty and has branches, need to update the database later
diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl
index dfda5b7b2b4d2..ae3f95045bc58 100644
--- a/templates/repo/empty.tmpl
+++ b/templates/repo/empty.tmpl
@@ -17,7 +17,7 @@
 
 				{{if .Repository.IsBroken}}
 					<div class="ui segment center">{{ctx.Locale.Tr "repo.broken_message"}}</div>
-				{{else if .Repository.IsEmpty}}
+				{{else if .RepoHasContentsWithoutBranch}}
 					<div class="ui segment center">{{ctx.Locale.Tr "repo.no_branch"}}</div>
 				{{else if .CanWriteCode}}
 					<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.quick_guide"}}</h4>
diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go
index b9660aeeb9ab6..ae1dd876f7ef3 100644
--- a/tests/integration/api_packages_npm_test.go
+++ b/tests/integration/api_packages_npm_test.go
@@ -52,24 +52,33 @@ func TestPackageNpm(t *testing.T) {
 			  "` + packageTag + `": "` + version + `"
 			},
 			"versions": {
-			  "` + version + `": {
-				"name": "` + packageName + `",
-				"version": "` + version + `",
-				"description": "` + packageDescription + `",
-				"author": {
-				  "name": "` + packageAuthor + `"
-				},
-        "bin": {
-          "` + packageBinName + `": "` + packageBinPath + `"
-        },
-				"dist": {
-				  "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
-				  "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
-				},
-				"repository": {
-					"type": "` + repoType + `",
-					"url": "` + repoURL + `"
-				}
+			  	"` + version + `": {
+					"name": "` + packageName + `",
+					"version": "` + version + `",
+					"description": "` + packageDescription + `",
+					"author": {
+				  	"name": "` + packageAuthor + `"
+					},
+        	"bin": {
+        	  "` + packageBinName + `": "` + packageBinPath + `"
+      	  },
+					"dist": {
+					  "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
+					  "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
+					},
+					"repository": {
+						"type": "` + repoType + `",
+						"url": "` + repoURL + `"
+					},
+					"peerDependencies": {
+						"tea": "2.x",
+						"soy-milk": "1.2"
+					},
+					"peerDependenciesMeta": {
+						"soy-milk": {
+							"optional": true
+						}
+					}
 			  }
 			},
 			"_attachments": {
@@ -178,6 +187,8 @@ func TestPackageNpm(t *testing.T) {
 		assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball)
 		assert.Equal(t, repoType, result.Repository.Type)
 		assert.Equal(t, repoURL, result.Repository.URL)
+		assert.Equal(t, map[string]string{"tea": "2.x", "soy-milk": "1.2"}, pmv.PeerDependencies)
+		assert.Equal(t, map[string]any{"soy-milk": map[string]any{"optional": true}}, pmv.PeerDependenciesMeta)
 	})
 
 	t.Run("AddTag", func(t *testing.T) {
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index 630a3c03af89b..0801b093dfb07 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -58,8 +58,12 @@ func TestEmptyRepoAddFile(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user30")
-	req := NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
+	req := NewRequest(t, "GET", "/user30/empty")
 	resp := session.MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "empty-repo-guide")
+
+	req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
+	resp = session.MakeRequest(t, req, http.StatusOK)
 	doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`)
 	assert.Empty(t, doc.AttrOr("checked", "_no_"))
 	req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{

From 9899989eceb581de42d1eb1a746da82eb1777e7d Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 6 Jan 2025 10:21:14 -0800
Subject: [PATCH 049/206] Add missing transaction when set merge (#33113)

backport from #33079

`SetMerged` should be in a database transaction otherwise it's possible
to have dirty data.
---
 services/pull/check.go | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/services/pull/check.go b/services/pull/check.go
index 736be4611bedd..baca1511a2a39 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -301,11 +301,16 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
 	pr.Merger = merger
 	pr.MergerID = merger.ID
 
-	if merged, err := pr.SetMerged(ctx); err != nil {
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		if merged, err := pr.SetMerged(ctx); err != nil {
+			return err
+		} else if !merged {
+			return errors.New("setMerged failed")
+		}
+		return nil
+	}); err != nil {
 		log.Error("%-v setMerged : %v", pr, err)
 		return false
-	} else if !merged {
-		return false
 	}
 
 	notify_service.MergePullRequest(ctx, merger, pr)

From 63b3a33bf2f1de6318e269cfd0d9315472d796d2 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 7 Jan 2025 07:55:04 +0800
Subject: [PATCH 050/206] fix empty repo updated time (#33120) (#33124)

Backport #33120 by changchaishi

fixes #33119


Co-authored-by: Chai-Shi <changchaishi@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/repo/update.go         | 6 ++++++
 routers/web/repo/view_home.go | 5 ++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/models/repo/update.go b/models/repo/update.go
index e7ca2240282f2..fce357a1acf35 100644
--- a/models/repo/update.go
+++ b/models/repo/update.go
@@ -46,6 +46,12 @@ func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string)
 	return err
 }
 
+// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically
+func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
+	_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo)
+	return err
+}
+
 // ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
 type ErrReachLimitOfRepo struct {
 	Limit int
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index a5b541f214a60..169c1a277f2fd 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -224,11 +224,14 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
 }
 
 func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status repo_model.RepositoryStatus) {
+	if ctx.Repo.Repository.IsEmpty == empty && ctx.Repo.Repository.Status == status {
+		return
+	}
 	ctx.Repo.Repository.IsEmpty = empty
 	if ctx.Repo.Repository.Status == repo_model.RepositoryReady || ctx.Repo.Repository.Status == repo_model.RepositoryBroken {
 		ctx.Repo.Repository.Status = status // only handle ready and broken status, leave other status as-is
 	}
-	if err := repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
+	if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, ctx.Repo.Repository, "is_empty", "status"); err != nil {
 		ctx.ServerError("updateContextRepoEmptyAndStatus: UpdateRepositoryCols", err)
 		return
 	}

From b4f0eed9698c648f5c0b1b7a907ccce0b627b4ac Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 8 Jan 2025 10:43:46 +0800
Subject: [PATCH 051/206] Filter reviews of one pull request in memory instead
 of database to reduce slow response because of lacking database index
 (#33106) (#33128)

Backport #33106 by @lunny

This PR fixes a performance problem when reviewing a pull request in a
big instance which have many records in the `review` table.
Traditionally, we should add more indexes in that table. But since
dismissed reviews of 1 pull request will not be too many as expected in
a common repository. Filtering reviews in the memory should be more
quick .

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/pull.go               |  4 +-
 models/issues/review_list.go        | 75 ++++++++++++++++++-----------
 models/issues/review_test.go        |  3 +-
 routers/web/repo/issue_page_meta.go | 10 +---
 4 files changed, 53 insertions(+), 39 deletions(-)

diff --git a/models/issues/pull.go b/models/issues/pull.go
index 853e2a69e6273..319ead5dbd32c 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -301,7 +301,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
 		return nil
 	}
 
-	reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
+	reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
 	if err != nil {
 		return err
 	}
@@ -320,7 +320,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
 
 // LoadRequestedReviewersTeams loads the requested reviewers teams.
 func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error {
-	reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
+	reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
 	if err != nil {
 		return err
 	}
diff --git a/models/issues/review_list.go b/models/issues/review_list.go
index bc7d7ec0f0168..928f24fb2dcd7 100644
--- a/models/issues/review_list.go
+++ b/models/issues/review_list.go
@@ -5,6 +5,8 @@ package issues
 
 import (
 	"context"
+	"slices"
+	"sort"
 
 	"code.gitea.io/gitea/models/db"
 	organization_model "code.gitea.io/gitea/models/organization"
@@ -153,43 +155,60 @@ func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) {
 	return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{})
 }
 
-// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
-func GetReviewersFromOriginalAuthorsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) {
+// GetReviewsByIssueID gets the latest review of each reviewer for a pull request
+// The first returned parameter is the latest review of each individual reviewer or team
+// The second returned parameter is the latest review of each original author which is migrated from other systems
+// The reviews are sorted by updated time
+func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) {
 	reviews := make([]*Review, 0, 10)
 
-	// Get latest review of each reviewer, sorted in order they were made
-	if err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC",
-		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
-		Find(&reviews); err != nil {
-		return nil, err
+	// Get all reviews for the issue id
+	if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil {
+		return nil, nil, err
 	}
 
-	return reviews, nil
-}
-
-// GetReviewsByIssueID gets the latest review of each reviewer for a pull request
-func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) {
-	reviews := make([]*Review, 0, 10)
-
-	sess := db.GetEngine(ctx)
+	// filter them in memory to get the latest review of each reviewer
+	// Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory
+	// And since there are too less indexes in review table, it will be very slow to filter in the database
+	reviewersMap := make(map[int64][]*Review)         // key is reviewer id
+	originalReviewersMap := make(map[int64][]*Review) // key is original author id
+	reviewTeamsMap := make(map[int64][]*Review)       // key is reviewer team id
+	countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}
+	for _, review := range reviews {
+		if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
+			if review.OriginalAuthorID != 0 {
+				originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review)
+			} else {
+				reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review)
+			}
+		} else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 {
+			reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review)
+		}
+	}
 
-	// Get latest review of each reviewer, sorted in order they were made
-	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
-		issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
-		Find(&reviews); err != nil {
-		return nil, err
+	individualReviews := make([]*Review, 0, 10)
+	for _, reviews := range reviewersMap {
+		individualReviews = append(individualReviews, reviews[len(reviews)-1])
 	}
+	sort.Slice(individualReviews, func(i, j int) bool {
+		return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix
+	})
 
-	teamReviewRequests := make([]*Review, 0, 5)
-	if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC",
-		issueID).
-		Find(&teamReviewRequests); err != nil {
-		return nil, err
+	originalReviews := make([]*Review, 0, 10)
+	for _, reviews := range originalReviewersMap {
+		originalReviews = append(originalReviews, reviews[len(reviews)-1])
 	}
+	sort.Slice(originalReviews, func(i, j int) bool {
+		return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix
+	})
 
-	if len(teamReviewRequests) > 0 {
-		reviews = append(reviews, teamReviewRequests...)
+	teamReviewRequests := make([]*Review, 0, 5)
+	for _, reviews := range reviewTeamsMap {
+		teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1])
 	}
+	sort.Slice(teamReviewRequests, func(i, j int) bool {
+		return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix
+	})
 
-	return reviews, nil
+	return append(individualReviews, teamReviewRequests...), originalReviews, nil
 }
diff --git a/models/issues/review_test.go b/models/issues/review_test.go
index 50330e3ff2c60..2588b8ba41b05 100644
--- a/models/issues/review_test.go
+++ b/models/issues/review_test.go
@@ -162,8 +162,9 @@ func TestGetReviewersByIssueID(t *testing.T) {
 		},
 	)
 
-	allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
+	allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
 	assert.NoError(t, err)
+	assert.Empty(t, migratedReviews)
 	for _, review := range allReviews {
 		assert.NoError(t, review.LoadReviewer(db.DefaultContext))
 	}
diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go
index b536b04d7c603..272343f460ff3 100644
--- a/routers/web/repo/issue_page_meta.go
+++ b/routers/web/repo/issue_page_meta.go
@@ -193,6 +193,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
 	var posterID int64
 	var isClosed bool
 	var reviews issues_model.ReviewList
+	var err error
 
 	if d.Issue == nil {
 		if ctx.Doer != nil {
@@ -206,14 +207,7 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
 
 		isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
 
-		originalAuthorReviews, err := issues_model.GetReviewersFromOriginalAuthorsByIssueID(ctx, d.Issue.ID)
-		if err != nil {
-			ctx.ServerError("GetReviewersFromOriginalAuthorsByIssueID", err)
-			return
-		}
-		data.OriginalReviews = originalAuthorReviews
-
-		reviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
+		reviews, data.OriginalReviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
 		if err != nil {
 			ctx.ServerError("GetReviewersByIssueID", err)
 			return

From 531f36ea4a21745fc8b258190a12a44deef73317 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 8 Jan 2025 13:08:44 +0800
Subject: [PATCH 052/206] Fix `git remote` error check, fix dependencies, fix
 js error (#33129) (#33133)

And update some dependencies to fix bugs.

Backport  #33129, #33136

Fix #32889
Fix #33141
Fix #33139

---------

Co-authored-by: yp05327 <576951401@qq.com>
---
 assets/go-licenses.json                | 12 +++-
 go.mod                                 | 27 ++++-----
 go.sum                                 | 78 +++++++++++---------------
 modules/git/remote.go                  | 10 ++++
 modules/markup/orgmode/orgmode_test.go |  4 +-
 modules/repository/branch.go           |  4 ++
 services/mirror/mirror_pull.go         |  8 +--
 services/repository/migrate.go         |  7 +--
 web_src/js/utils.ts                    |  2 +-
 9 files changed, 83 insertions(+), 69 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 8c155ceadb1a0..60e39b6694d02 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -874,6 +874,11 @@
     "path": "github.com/mitchellh/mapstructure/LICENSE",
     "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
   },
+  {
+    "name": "github.com/mmcloughlin/avo",
+    "path": "github.com/mmcloughlin/avo/LICENSE",
+    "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, Michael McLoughlin\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "github.com/modern-go/concurrent",
     "path": "github.com/modern-go/concurrent/LICENSE",
@@ -937,7 +942,7 @@
   {
     "name": "github.com/pjbgf/sha1cd",
     "path": "github.com/pjbgf/sha1cd/LICENSE",
-    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
+    "licenseText": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2023 pjbgf\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
   {
     "name": "github.com/pkg/errors",
@@ -1194,6 +1199,11 @@
     "path": "golang.org/x/time/rate/LICENSE",
     "licenseText": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
+  {
+    "name": "golang.org/x/tools",
+    "path": "golang.org/x/tools/LICENSE",
+    "licenseText": "Copyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
+  },
   {
     "name": "google.golang.org/genproto/googleapis/rpc/status",
     "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE",
diff --git a/go.mod b/go.mod
index dc80d2ca2bb3a..d8e7e37152804 100644
--- a/go.mod
+++ b/go.mod
@@ -24,10 +24,10 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
-	github.com/ProtonMail/go-crypto v1.0.0
+	github.com/ProtonMail/go-crypto v1.1.4
 	github.com/PuerkitoBio/goquery v1.10.0
 	github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
-	github.com/alecthomas/chroma/v2 v2.14.0
+	github.com/alecthomas/chroma/v2 v2.15.0
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.42
 	github.com/aws/aws-sdk-go-v2/service/codecommit v1.27.3
 	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
@@ -54,8 +54,8 @@ require (
 	github.com/go-chi/cors v1.2.1
 	github.com/go-co-op/gocron v1.37.0
 	github.com/go-enry/go-enry/v2 v2.9.1
-	github.com/go-git/go-billy/v5 v5.6.0
-	github.com/go-git/go-git/v5 v5.12.0
+	github.com/go-git/go-billy/v5 v5.6.1
+	github.com/go-git/go-git/v5 v5.13.1
 	github.com/go-ldap/ldap/v3 v3.4.8
 	github.com/go-redsync/redsync/v4 v4.13.0
 	github.com/go-sql-driver/mysql v1.8.1
@@ -107,7 +107,7 @@ require (
 	github.com/sassoftware/go-rpmutils v0.4.0
 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
 	github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	github.com/syndtr/goleveldb v1.0.0
 	github.com/tstranex/u2f v1.0.0
 	github.com/ulikunitz/xz v0.5.12
@@ -119,14 +119,14 @@ require (
 	github.com/yuin/goldmark v1.7.8
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.31.0
+	golang.org/x/crypto v0.32.0
 	golang.org/x/image v0.21.0
-	golang.org/x/net v0.33.0
+	golang.org/x/net v0.34.0
 	golang.org/x/oauth2 v0.23.0
 	golang.org/x/sync v0.10.0
-	golang.org/x/sys v0.28.0
+	golang.org/x/sys v0.29.0
 	golang.org/x/text v0.21.0
-	golang.org/x/tools v0.26.0
+	golang.org/x/tools v0.29.0
 	google.golang.org/grpc v1.67.1
 	google.golang.org/protobuf v1.35.1
 	gopkg.in/ini.v1 v1.67.0
@@ -187,7 +187,7 @@ require (
 	github.com/couchbase/gomemcached v0.3.2 // indirect
 	github.com/couchbase/goutils v0.1.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
-	github.com/cyphar/filepath-securejoin v0.3.4 // indirect
+	github.com/cyphar/filepath-securejoin v0.3.6 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/davidmz/go-pageant v1.0.2 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -220,7 +220,7 @@ require (
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/btree v1.1.3 // indirect
@@ -255,6 +255,7 @@ require (
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/mmcloughlin/avo v0.6.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
@@ -266,7 +267,7 @@ require (
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/pierrec/lz4/v4 v4.1.21 // indirect
-	github.com/pjbgf/sha1cd v0.3.0 // indirect
+	github.com/pjbgf/sha1cd v0.3.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.60.1 // indirect
@@ -306,7 +307,7 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
-	golang.org/x/mod v0.21.0 // indirect
+	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index b5e64321b53ed..968001e7f91ae 100644
--- a/go.sum
+++ b/go.sum
@@ -71,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
-github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE=
+github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
 github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
 github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
@@ -81,11 +81,11 @@ github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv
 github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3 h1:BP0HiyNT3AQEYi+if3wkRcIdQFHtsw6xX3Kx0glckgA=
 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3/go.mod h1:hMNtySovKkn2gdDuLqnqveP+mfhUSaBdoBcr2I7Zt0E=
-github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
-github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
 github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
-github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
-github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
+github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -188,7 +188,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk=
 github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0=
-github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
 github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
 github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
@@ -205,7 +204,6 @@ github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moA
 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
-github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
 github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
 github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -223,8 +221,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc
 github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
 github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
-github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -254,8 +252,8 @@ github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJ
 github.com/editorconfig/editorconfig-core-go/v2 v2.6.2 h1:dKG8sc7n321deIVRcQtwlMNoBEra7j0qQ8RwxO8RN0w=
 github.com/editorconfig/editorconfig-core-go/v2 v2.6.2/go.mod h1:7dvD3GCm7eBw53xZ/lsiq72LqobdMg3ITbMBxnmJmqY=
 github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
-github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
-github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
+github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
 github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
 github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
 github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
@@ -313,12 +311,12 @@ github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5Hql
 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
-github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
+github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
+github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
-github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
+github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
+github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
 github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
 github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
@@ -385,8 +383,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
 github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -585,6 +583,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
+github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -637,8 +637,8 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
 github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
-github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI=
+github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -743,8 +743,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
 github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -829,16 +829,14 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
@@ -852,8 +850,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -865,18 +863,16 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
 golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -916,8 +912,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -927,14 +921,12 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
@@ -942,15 +934,13 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@@ -970,8 +960,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
-golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/modules/git/remote.go b/modules/git/remote.go
index 7b10e6b663155..a872b3b82e8bf 100644
--- a/modules/git/remote.go
+++ b/modules/git/remote.go
@@ -5,6 +5,7 @@ package git
 
 import (
 	"context"
+	"strings"
 
 	giturl "code.gitea.io/gitea/modules/git/url"
 )
@@ -37,3 +38,12 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git
 	}
 	return giturl.Parse(addr)
 }
+
+// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
+func IsRemoteNotExistError(err error) bool {
+	// see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216
+	// Should not add space in the end, sometimes git will add a `:`
+	prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30
+	prefix2 := "exit status 2 - error: No such remote"   // git >= 2.30
+	return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2)
+}
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index e3cc05b4f022c..de39bafebeabe 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -103,8 +103,8 @@ func HelloWorld() {
 }
 #+end_src
 `, `<div class="src src-go">
-<pre><code class="chroma language-go"><span class="c1">// HelloWorld prints &#34;Hello World&#34;
-</span><span class="c1"></span><span class="kd">func</span> <span class="nf">HelloWorld</span><span class="p">()</span> <span class="p">{</span>
+<pre><code class="chroma language-go"><span class="c1">// HelloWorld prints &#34;Hello World&#34;</span>
+<span class="kd">func</span> <span class="nf">HelloWorld</span><span class="p">()</span> <span class="p">{</span>
 	<span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Hello World&#34;</span><span class="p">)</span>
 <span class="p">}</span></code></pre>
 </div>`)
diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index 2bf9930f19fd3..4630e70aa8f39 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -6,6 +6,7 @@ package repository
 import (
 	"context"
 	"fmt"
+	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
@@ -51,6 +52,9 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 	{
 		branches, _, err := gitRepo.GetBranchNames(0, 0)
 		if err != nil {
+			if strings.Contains(err.Error(), "ref file is empty") {
+				return 0, nil
+			}
 			return 0, err
 		}
 		log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index a81fe6e9bd321..34879ddd16b9e 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -41,7 +41,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
 	repoPath := m.GetRepository(ctx).RepoPath()
 	// Remove old remote
 	_, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath})
-	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+	if err != nil && !git.IsRemoteNotExistError(err) {
 		return err
 	}
 
@@ -52,7 +52,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
 		cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, addr, repoPath))
 	}
 	_, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath})
-	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+	if err != nil && !git.IsRemoteNotExistError(err) {
 		return err
 	}
 
@@ -61,7 +61,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
 		wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr)
 		// Remove old remote of wiki
 		_, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath})
-		if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+		if err != nil && !git.IsRemoteNotExistError(err) {
 			return err
 		}
 
@@ -72,7 +72,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
 			cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, wikiRemotePath, wikiPath))
 		}
 		_, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath})
-		if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+		if err != nil && !git.IsRemoteNotExistError(err) {
 			return err
 		}
 	}
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
index c627b46fab56f..262d3bd51a9a2 100644
--- a/services/repository/migrate.go
+++ b/services/repository/migrate.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/db"
@@ -255,10 +254,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
 func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
 	cmd := git.NewCommand(ctx, "remote", "rm", "origin")
 	// if the origin does not exist
-	_, stderr, err := cmd.RunStdString(&git.RunOpts{
+	_, _, err := cmd.RunStdString(&git.RunOpts{
 		Dir: repoPath,
 	})
-	if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
+	if err != nil && !git.IsRemoteNotExistError(err) {
 		return err
 	}
 	return nil
@@ -277,7 +276,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo
 	}
 
 	_, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
-	if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") {
+	if err != nil && !git.IsRemoteNotExistError(err) {
 		return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err)
 	}
 
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index efa144c3c762f..2a2bdc60f9577 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -40,7 +40,7 @@ export function parseIssueHref(href: string): IssuePathInfo {
 export function parseIssueNewHref(href: string): IssuePathInfo {
   const path = (href || '').replace(/[#?].*$/, '');
   const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || [];
-  const pathType = pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls';
+  const pathType = pathTypeField ? (pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls') : undefined;
   return {ownerName, repoName, pathType};
 }
 

From a330f42f016e5a69dc204cdf858d8283ba75c229 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 8 Jan 2025 17:24:34 -0800
Subject: [PATCH 053/206] Update changelog for v1.23.0 (#33130)

---
 CHANGELOG.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 46 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 204d51dbaf675..e4a846078fac2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
-## [1.23.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0-rc0) - 2024-12-16
+## [1.23.0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0) - 2025-01-08
 
 * BREAKING
   * Rename config option `[camo].Allways` to `[camo].Always` (#32097)
@@ -41,6 +41,8 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * GitHub like repo home page (#32213 & #32847)
   * Rearrange Clone Panel (#31142)
   * Enhancing Gitea OAuth2 Provider with Granular Scopes for Resource Access (#32573)
+  * Use env GITEA_RUNNER_REGISTRATION_TOKEN as global runner token (#32946) #32964
+  * Update i18n.go - Language Picker (#32933) #32935
 
 * PERFORMANCE
   * Perf: add extra index to notification table (#32395)
@@ -51,6 +53,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Improve get feed with pagination (#31821)
   * Performance improvements for pull request list API (#30490)
   * Use batch database operations instead of one by one to optimze api pulls (#32680)
+  * Use gitrepo.GetTreePathLatestCommit to get file lastest commit instead from latest commit cache (#32987) #33046
 
 * ENHANCEMENTS
   * Code
@@ -149,6 +152,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
     * Add typescript guideline and typescript-specific eslint plugins and fix issues (#31521)
     * Make toast support preventDuplicates (#31501)
     * Fix tautological conditions (#30735)
+    * Issue change title notifications (#33050) #33065
 
 * API
   * Implement update branch API (#32433)
@@ -241,6 +245,8 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Replace DateTime with proper functions (#32402)
   * Replace DateTime with DateUtils (#32383)
   * Convert frontend code to typescript (#31559)
+  * Refactor maven package registry (#33049) #33057
+  * Refactor testfixtures #33028
 
 * BUGFIXES
   * Fix issues with inconsistent spacing in areas (#32607)
@@ -267,6 +273,41 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Fix some function names in comment (#32300)
   * Fix absolute-date (#32375)
   * Clarify Actions resources ownership (#31724)
+  * Try to fix ACME directory problem (#33072) #33077
+  * Inherit submodules from template repository content (#16237) #33068
+  * Use project's redirect url instead of composing url (#33058) #33064
+  * Fix toggle commit body button ui when latest commit message is long (#32997) #33034
+  * Fix package error handling and npm meta and empty repo guide #33112
+  * Fix empty git repo handling logic and fix mobile view (#33101) #33102
+  * Fix line-number and scroll bugs (#33094) #33095
+  * Fix bleve fuzziness search (#33078) #33087
+  * Fix broken forms #33082
+  * Fix empty repo updated time (#33120) #33124
+  * Add missing transaction when set merge #33113
+  * Fix issue comment number (#30556) #33055
+  * Fix duplicate co-author in squashed merge commit messages (#33020) #33054
+  * Fix Agit pull request permission check (#32999) #33005
+  * Fix scoped label ui when contains emoji (#33007) #33014
+  * Fix bug on activities (#33008) #33016
+  * Fix review code comment avatar alignment (#33031) #33032
+  * Fix templating in pull request comparison (#33025) #33038
+  * Fix bug automerge cannot be chosed when there is only 1 merge style (#33040) #33043
+  * Fix settings not being loaded at CLI (#26402) #33048
+  * Support for email addresses containing uppercase characters when activating user account (#32998) #33001
+  * Support org labels when adding labels by label names (#32988) #32996
+  * Do not render truncated links in markdown (#32980) #32983
+  * Demilestone should not include milestone (#32923) #32979
+  * Fix Azure blob object Seek (#32974) #32975
+  * Fix maven pom inheritance (#32943) #32976
+  * Fix textarea newline handle (#32966) #32977
+  * Fix outdated tmpl code (#32953) #32961
+  * Fix commit range paging (#32944) #32962
+  * Fix repo avatar conflict (#32958) #32960
+  * Fix trailing comma not matched in the case of alphanumeric issue (#32945)
+  * Relax the version checking for Arch packages (#32908) #32913
+  * Add more load functions to make sure the reference object loaded (#32901) #32912
+  * Filter reviews of one pull request in memory instead of database to reduce slow response because of lacking database index (#33106) #33128
+  * Fix git remote error check, fix dependencies, fix js error (#33129) #33133
 
 * MISC
   * Optimize branch protection rule loading (#32280)
@@ -351,6 +392,10 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Upgrade xorm to v1.3.9 and improve some migrations Sync (#29899)
   * Added default sorting milestones by name (#27084)
   * Enable `unparam` linter (#31277)
+  * Use Alpine 3.21 for the docker images (#32924) #32951
+  * Bump x/net (#32896) #32899
+  * Use -s -w ldflags for release artifacts (#33041) #33042
+  * Remove aws go sdk package dependency (#33029) #33047
 
 ## [1.22.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14
 

From 12c24c21894ecbe46977aca52ec3d07dc1289df0 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 9 Jan 2025 15:40:35 +0800
Subject: [PATCH 054/206] Fix fuzz test (#33156) (#33158)

Backport #33156 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 tests/fuzz/fuzz_test.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/fuzz/fuzz_test.go b/tests/fuzz/fuzz_test.go
index 01d562d995c90..e7b832412abb1 100644
--- a/tests/fuzz/fuzz_test.go
+++ b/tests/fuzz/fuzz_test.go
@@ -19,6 +19,7 @@ func newFuzzRenderContext() *markup.RenderContext {
 
 func FuzzMarkdownRenderRaw(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
+		setting.IsInTesting = true
 		setting.AppURL = "http://localhost:3000/"
 		markdown.RenderRaw(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
 	})
@@ -26,6 +27,7 @@ func FuzzMarkdownRenderRaw(f *testing.F) {
 
 func FuzzMarkupPostProcess(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
+		setting.IsInTesting = true
 		setting.AppURL = "http://localhost:3000/"
 		markup.PostProcessDefault(newFuzzRenderContext(), bytes.NewReader(data), io.Discard)
 	})

From d65af69c2bb8035b13721cb6fc301361cc30f50c Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 9 Jan 2025 21:33:50 +0800
Subject: [PATCH 055/206] Fix pam auth test regression (#33169) (#33174)

Backport #33169 by TheFox0x7

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

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
---
 modules/auth/pam/pam_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go
index 7265b5d0c1507..d4ab058ec7878 100644
--- a/modules/auth/pam/pam_test.go
+++ b/modules/auth/pam/pam_test.go
@@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) {
 	result, err := Auth("gitea", "user1", "false-pwd")
 	assert.Error(t, err)
 	assert.EqualError(t, err, "Authentication failure")
-	assert.Len(t, result)
+	assert.Empty(t, result)
 }

From 6f6c66a07d5cda1b4276ab8936d13bad556fc2d8 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 02:36:32 +0800
Subject: [PATCH 056/206] Fix assignee list overlapping in Issue sidebar
 (#33176) (#33181)

Backport #33176 by wxiaoguang

Fix  #33170

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/issue/sidebar/assignee_list.tmpl | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl
index d8ccd73387de7..4fe8043f3473d 100644
--- a/templates/repo/issue/sidebar/assignee_list.tmpl
+++ b/templates/repo/issue/sidebar/assignee_list.tmpl
@@ -16,9 +16,9 @@
 				<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
 			</div>
 			<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
-			<div class="scrolling menu">
+			<div class="scrolling menu flex-items-block">
 				{{range $data.CandidateAssignees}}
-					<a class="item muted" href="#" data-value="{{.ID}}">
+					<a class="item" href="#" data-value="{{.ID}}">
 						<span class="item-check-mark">{{svg "octicon-check"}}</span>
 						{{ctx.AvatarUtils.Avatar . 20}} {{template "repo/search_name" .}}
 					</a>
@@ -26,10 +26,10 @@
 			</div>
 		</div>
 	</div>
-	<div class="ui list tw-flex tw-flex-row tw-gap-2">
+	<div class="ui list muted-links flex-items-block tw-flex tw-flex-col tw-gap-2">
 		<span class="item empty-list {{if $issueAssignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span>
 		{{range $issueAssignees}}
-			<a class="item muted" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
+			<a class="item" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}">
 					{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
 			</a>
 		{{end}}

From 3cfff5af0d3cf544540efe42e6e6d2564140c0c9 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 04:29:37 +0800
Subject: [PATCH 057/206] Move repo size to sidebar (#33155) (#33182)

Backport #33155 by @yp05327


![image](https://github.com/user-attachments/assets/8b14dbb7-ec36-4596-a6aa-72c14d93309d)

Co-authored-by: yp05327 <576951401@qq.com>
---
 templates/repo/home_sidebar_top.tmpl | 5 +++++
 templates/repo/sub_menu.tmpl         | 5 -----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl
index 607dc62e2e717..d7c2b8336f37a 100644
--- a/templates/repo/home_sidebar_top.tmpl
+++ b/templates/repo/home_sidebar_top.tmpl
@@ -62,6 +62,11 @@
 								{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
 							</a>
 						{{end}}
+						<span class="flex-text-block muted" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
+							{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
+							{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
+							{{svg "octicon-database"}} <b>{{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}}</b> {{index $fileSizeFields 1}}
+						</span>
 					</div>
 				</div>
 			</div>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index ccb98b94ad2f8..1e83b53ef048f 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -13,11 +13,6 @@
 					{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}
 				</a>
 			{{end}}
-			<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
-				{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
-				{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
-				{{svg "octicon-database"}} <b>{{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}}</b> {{index $fileSizeFields 1}}
-			</span>
 		{{end}}
 	</div>
 </div>

From ed1828ca92b08563845f453b4f7785753102b9ea Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 09:06:40 +0800
Subject: [PATCH 058/206] Fix ACME panic (#33178) (#33186)

Backport #33178 by @wxiaoguang

Fix #33177, Manually tested:

````
1.7364311850484018e+09	info	maintenance	started background certificate maintenance	{"cache": "0x1400ca64180"}
1.736431185054049e+09	info	obtain	acquiring lock	{"identifier": "example.com"}
1.736431185058073e+09	info	obtain	lock acquired	{"identifier": "example.com"}
1.736431185058133e+09	info	obtain	obtaining certificate	{"identifier": "example.com"}
````

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 cmd/web_acme.go | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/cmd/web_acme.go b/cmd/web_acme.go
index 2fe14c1f54c04..5daf0f55f243f 100644
--- a/cmd/web_acme.go
+++ b/cmd/web_acme.go
@@ -54,8 +54,10 @@ func runACME(listenAddr string, m http.Handler) error {
 		altTLSALPNPort = p
 	}
 
-	magic := &certmagic.Default
-	magic.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
+	// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
+	// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
+	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
+	magic := certmagic.NewDefault()
 	// Try to use private CA root if provided, otherwise defaults to system's trust
 	var certPool *x509.CertPool
 	if setting.AcmeCARoot != "" {

From 15ad001aefae9fa4e6c917a3dd7d22237572e3fa Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 09:41:52 +0800
Subject: [PATCH 059/206] Fix branch dropdown not display ref name (#33159)
 (#33183)

Backport #33159 by @yp05327

Before:

![image](https://github.com/user-attachments/assets/899d25a9-80e9-48d5-a820-79c911c858e9)
After:

![image](https://github.com/user-attachments/assets/cf2a7407-909a-41db-9957-19d9214af57e)

Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/branch_dropdown.tmpl |  4 ++--
 templates/repo/commits.tmpl         | 24 ++++++++++++++----------
 templates/repo/home.tmpl            | 22 ++++++++++++++--------
 3 files changed, 30 insertions(+), 20 deletions(-)

diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl
index b68c34a02af0a..6efed3427f44d 100644
--- a/templates/repo/branch_dropdown.tmpl
+++ b/templates/repo/branch_dropdown.tmpl
@@ -1,8 +1,8 @@
 {{/* Attributes:
 * ContainerClasses
 * Repository
-* CurrentRefType: eg. "branch", "tag"
-* CurrentRefShortName: eg. "master", "v1.0"
+* CurrentRefType: eg. "branch", "tag", "commit"
+* CurrentRefShortName: eg. "master", "v1.0", "abcdef0123"
 * CurrentTreePath
 * RefLinkTemplate: redirect to the link when a branch/tag is selected
 * RefFormActionTemplate: change the parent form's action when a branch/tag is selected
diff --git a/templates/repo/commits.tmpl b/templates/repo/commits.tmpl
index 7065bf33f4db1..d80d4ac276474 100644
--- a/templates/repo/commits.tmpl
+++ b/templates/repo/commits.tmpl
@@ -5,14 +5,19 @@
 		{{template "repo/sub_menu" .}}
 		<div class="repo-button-row">
 			<div class="repo-button-row-left">
-
-				{{$branchDropdownCurrentRefType := "branch"}}
-				{{$branchDropdownCurrentRefShortName := .BranchName}}
-				{{if .IsViewTag}}
-					{{$branchDropdownCurrentRefType = "tag"}}
-					{{$branchDropdownCurrentRefShortName = .TagName}}
-				{{end}}
-				{{template "repo/branch_dropdown" dict
+				{{- /* for /owner/repo/commits/branch/the-name */ -}}
+				{{- $branchDropdownCurrentRefType := "branch" -}}
+				{{- $branchDropdownCurrentRefShortName := .BranchName -}}
+				{{- if .IsViewTag -}}
+					{{- /* for /owner/repo/commits/tag/the-name */ -}}
+					{{- $branchDropdownCurrentRefType = "tag" -}}
+					{{- $branchDropdownCurrentRefShortName = .TagName -}}
+				{{- else if .IsViewCommit -}}
+					{{- /* for /owner/repo/commits/commit/000000 */ -}}
+					{{- $branchDropdownCurrentRefType = "commit" -}}
+					{{- $branchDropdownCurrentRefShortName = ShortSha .CommitID -}}
+				{{- end -}}
+				{{- template "repo/branch_dropdown" dict
 					"Repository" .Repository
 					"ShowTabBranches" true
 					"ShowTabTags" true
@@ -21,8 +26,7 @@
 					"CurrentTreePath" .TreePath
 					"RefLinkTemplate" "{RepoLink}/commits/{RefType}/{RefShortName}/{TreePath}"
 					"AllowCreateNewRef" .CanCreateBranch
-				}}
-
+				-}}
 				<a href="{{.RepoLink}}/graph" class="ui basic small compact button">
 					{{svg "octicon-git-branch"}}
 					{{ctx.Locale.Tr "repo.commit_graph"}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index d73b7470bc5cd..f80c9c1f4b37d 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -24,13 +24,19 @@
 				{{template "repo/sub_menu" .}}
 				<div class="repo-button-row">
 					<div class="repo-button-row-left">
-						{{$branchDropdownCurrentRefType := "branch"}}
-						{{$branchDropdownCurrentRefShortName := .BranchName}}
-						{{if .IsViewTag}}
-							{{$branchDropdownCurrentRefType = "tag"}}
-							{{$branchDropdownCurrentRefShortName = .TagName}}
-						{{end}}
-						{{template "repo/branch_dropdown" dict
+						{{- /* for repo home (default branch) and /owner/repo/src/branch/the-name */ -}}
+						{{- $branchDropdownCurrentRefType := "branch" -}}
+						{{- $branchDropdownCurrentRefShortName := .BranchName -}}
+						{{- if .IsViewTag -}}
+							{{- /* for /owner/repo/src/tag/the-name */ -}}
+							{{- $branchDropdownCurrentRefType = "tag" -}}
+							{{- $branchDropdownCurrentRefShortName = .TagName -}}
+						{{- else if .IsViewCommit -}}
+							{{- /* for /owner/repo/src/commit/000000 */ -}}
+							{{- $branchDropdownCurrentRefType = "commit" -}}
+							{{- $branchDropdownCurrentRefShortName = ShortSha .CommitID -}}
+						{{- end -}}
+						{{- template "repo/branch_dropdown" dict
 							"Repository" .Repository
 							"ShowTabBranches" true
 							"ShowTabTags" true
@@ -40,7 +46,7 @@
 							"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
 							"AllowCreateNewRef" .CanCreateBranch
 							"ShowViewAllRefsEntry" true
-						}}
+						-}}
 						{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 							{{$cmpBranch := ""}}
 							{{if ne .Repository.ID .BaseRepo.ID}}

From 45d21a0d5c9bdf8ea09a402c0ad9fa7c9d0fd4ab Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 10:41:28 +0800
Subject: [PATCH 060/206] Fix raw file API ref handling (#33172) (#33189)

Backport #33172 by wxiaoguang

Fix #33164 and add more tests

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/context/api.go                       |  3 +-
 services/context/repo.go                      | 38 ++++++------
 .../integration/api_repo_get_contents_test.go | 58 +++++++++++--------
 3 files changed, 52 insertions(+), 47 deletions(-)

diff --git a/services/context/api.go b/services/context/api.go
index b45e80a329789..50ec0ee40b4a6 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -305,8 +305,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
 			return
 		}
 
-		// NOTICE: the "ref" here for internal usage only (e.g. woodpecker)
-		refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.FormTrim("ref"))
+		refName, _ := getRefNameLegacy(ctx.Base, ctx.Repo, ctx.PathParam("*"), ctx.FormTrim("ref"))
 		var err error
 
 		if ctx.Repo.GitRepo.IsBranchExist(refName) {
diff --git a/services/context/repo.go b/services/context/repo.go
index 1e7c4303471e0..879b3fcb865c7 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -769,35 +769,30 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool
 	return ""
 }
 
-func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) {
-	extraRef := util.OptionalArg(optionalExtraRef)
-	reqPath := ctx.PathParam("*")
-	reqPath = path.Join(extraRef, reqPath)
-
-	if refName := getRefName(ctx, repo, RepoRefBranch); refName != "" {
+func getRefNameLegacy(ctx *Base, repo *Repository, reqPath, extraRef string) (string, RepoRefType) {
+	reqRefPath := path.Join(extraRef, reqPath)
+	reqRefPathParts := strings.Split(reqRefPath, "/")
+	if refName := getRefName(ctx, repo, reqRefPath, RepoRefBranch); refName != "" {
 		return refName, RepoRefBranch
 	}
-	if refName := getRefName(ctx, repo, RepoRefTag); refName != "" {
+	if refName := getRefName(ctx, repo, reqRefPath, RepoRefTag); refName != "" {
 		return refName, RepoRefTag
 	}
-
-	// For legacy support only full commit sha
-	parts := strings.Split(reqPath, "/")
-	if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) {
+	if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), reqRefPathParts[0]) {
 		// FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists
-		repo.TreePath = strings.Join(parts[1:], "/")
-		return parts[0], RepoRefCommit
+		repo.TreePath = strings.Join(reqRefPathParts[1:], "/")
+		return reqRefPathParts[0], RepoRefCommit
 	}
-
-	if refName := getRefName(ctx, repo, RepoRefBlob); len(refName) > 0 {
+	if refName := getRefName(ctx, repo, reqPath, RepoRefBlob); refName != "" {
 		return refName, RepoRefBlob
 	}
+	// FIXME: the old code falls back to default branch if "ref" doesn't exist, there could be an edge case:
+	// "README?ref=no-such" would read the README file from the default branch, but the user might expect a 404
 	repo.TreePath = reqPath
 	return repo.Repository.DefaultBranch, RepoRefBranch
 }
 
-func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
-	path := ctx.PathParam("*")
+func getRefName(ctx *Base, repo *Repository, path string, pathType RepoRefType) string {
 	switch pathType {
 	case RepoRefBranch:
 		ref := getRefNameFromPath(repo, path, repo.GitRepo.IsBranchExist)
@@ -900,7 +895,8 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
 		}
 
 		// Get default branch.
-		if len(ctx.PathParam("*")) == 0 {
+		reqPath := ctx.PathParam("*")
+		if reqPath == "" {
 			refName = ctx.Repo.Repository.DefaultBranch
 			if !ctx.Repo.GitRepo.IsBranchExist(refName) {
 				brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 1)
@@ -925,12 +921,12 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
 				return cancel
 			}
 			ctx.Repo.IsViewBranch = true
-		} else {
+		} else { // there is a path in request
 			guessLegacyPath := refType == RepoRefUnknown
 			if guessLegacyPath {
-				refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo)
+				refName, refType = getRefNameLegacy(ctx.Base, ctx.Repo, reqPath, "")
 			} else {
-				refName = getRefName(ctx.Base, ctx.Repo, refType)
+				refName = getRefName(ctx.Base, ctx.Repo, reqPath, refType)
 			}
 			ctx.Repo.RefName = refName
 			isRenamedBranch, has := ctx.Data["IsRenamedBranch"].(bool)
diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go
index 68a8608117208..d0f61da0c0402 100644
--- a/tests/integration/api_repo_get_contents_test.go
+++ b/tests/integration/api_repo_get_contents_test.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	repo_service "code.gitea.io/gitea/services/repository"
+	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -168,28 +169,37 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
 }
 
 func TestAPIGetContentsRefFormats(t *testing.T) {
-	onGiteaRun(t, func(t *testing.T, u *url.URL) {
-		file := "README.md"
-		sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
-		content := "# repo1\n\nDescription for repo1"
-
-		noRef := setting.AppURL + "api/v1/repos/user2/repo1/raw/" + file
-		refInPath := setting.AppURL + "api/v1/repos/user2/repo1/raw/" + sha + "/" + file
-		refInQuery := setting.AppURL + "api/v1/repos/user2/repo1/raw/" + file + "?ref=" + sha
-
-		resp := MakeRequest(t, NewRequest(t, http.MethodGet, noRef), http.StatusOK)
-		raw, err := io.ReadAll(resp.Body)
-		assert.NoError(t, err)
-		assert.EqualValues(t, content, string(raw))
-
-		resp = MakeRequest(t, NewRequest(t, http.MethodGet, refInPath), http.StatusOK)
-		raw, err = io.ReadAll(resp.Body)
-		assert.NoError(t, err)
-		assert.EqualValues(t, content, string(raw))
-
-		resp = MakeRequest(t, NewRequest(t, http.MethodGet, refInQuery), http.StatusOK)
-		raw, err = io.ReadAll(resp.Body)
-		assert.NoError(t, err)
-		assert.EqualValues(t, content, string(raw))
-	})
+	defer tests.PrepareTestEnv(t)()
+
+	file := "README.md"
+	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
+	content := "# repo1\n\nDescription for repo1"
+
+	resp := MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/"+file), http.StatusOK)
+	raw, err := io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	assert.EqualValues(t, content, string(raw))
+
+	resp = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/"+sha+"/"+file), http.StatusOK)
+	raw, err = io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	assert.EqualValues(t, content, string(raw))
+
+	resp = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/"+file+"?ref="+sha), http.StatusOK)
+	raw, err = io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	assert.EqualValues(t, content, string(raw))
+
+	resp = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/"+file+"?ref=master"), http.StatusOK)
+	raw, err = io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	assert.EqualValues(t, content, string(raw))
+
+	_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/docs/README.md?ref=main"), http.StatusNotFound)
+	_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=main"), http.StatusOK)
+	_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/docs/README.md?ref=sub-home-md-img-check"), http.StatusOK)
+	_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=sub-home-md-img-check"), http.StatusNotFound)
+
+	// FIXME: this is an incorrect behavior, non-existing branch falls back to default branch
+	_ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=no-such"), http.StatusOK)
 }

From 940a930d1367b6b45b246c4b61bd16e768993849 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 10:48:52 +0800
Subject: [PATCH 061/206] Use updated path to s6-svscan after alpine upgrade
 (#33185) (#33188)

Backport #33185 by @techknowlogick

Fix #33163

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 docker/root/usr/bin/entrypoint | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docker/root/usr/bin/entrypoint b/docker/root/usr/bin/entrypoint
index d9dbb3ebe0b3d..08587fc4f4dcf 100755
--- a/docker/root/usr/bin/entrypoint
+++ b/docker/root/usr/bin/entrypoint
@@ -37,5 +37,5 @@ done
 if [ $# -gt 0 ]; then
     exec "$@"
 else
-    exec /bin/s6-svscan /etc/s6
+    exec /usr/bin/s6-svscan /etc/s6
 fi

From d70348836bd0fa8eed378941062408fbbf4816c3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 10 Jan 2025 15:11:45 +0800
Subject: [PATCH 062/206] Fix sync fork for consistency (#33147) (#33192)

Backport #33147 by changchaishi

Fixes #33145

An integration test could be added.

---------

Co-authored-by: Chai-Shi <changchaishi@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/structs/repo_branch.go                |   8 ++
 routers/api/v1/api.go                         |   1 +
 routers/api/v1/repo/branch.go                 |  45 +++++++
 routers/api/v1/swagger/repo.go                |  12 ++
 services/repository/merge_upstream.go         |  49 +++++--
 .../repo/code/upstream_diverging_info.tmpl    |   6 +-
 templates/swagger/v1_json.tmpl                |  78 +++++++++++
 tests/integration/repo_merge_upstream_test.go | 122 ++++++++++++++++++
 8 files changed, 309 insertions(+), 12 deletions(-)
 create mode 100644 tests/integration/repo_merge_upstream_test.go

diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go
index a9aa1d330a184..55c98d60b9124 100644
--- a/modules/structs/repo_branch.go
+++ b/modules/structs/repo_branch.go
@@ -133,3 +133,11 @@ type EditBranchProtectionOption struct {
 type UpdateBranchProtectionPriories struct {
 	IDs []int64 `json:"ids"`
 }
+
+type MergeUpstreamRequest struct {
+	Branch string `json:"branch"`
+}
+
+type MergeUpstreamResponse struct {
+	MergeStyle string `json:"merge_type"`
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 96365e7c14dfc..d0a2bd8a2786e 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1190,6 +1190,7 @@ func Routes() *web.Router {
 				m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
 				m.Combo("/forks").Get(repo.ListForks).
 					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.Group("/branches", func() {
 					m.Get("", repo.ListBranches)
 					m.Get("/*", repo.GetBranch)
diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go
index 946203e97ec0a..da0391150bd73 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/optional"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
 	"code.gitea.io/gitea/services/context"
@@ -1194,3 +1195,47 @@ func UpdateBranchProtectionPriories(ctx *context.APIContext) {
 
 	ctx.Status(http.StatusNoContent)
 }
+
+func MergeUpstream(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/merge-upstream repository repoMergeUpstream
+	// ---
+	// summary: Merge a branch from upstream
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repo
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repo
+	//   type: string
+	//   required: true
+	// - name: body
+	//   in: body
+	//   schema:
+	//     "$ref": "#/definitions/MergeUpstreamRequest"
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/MergeUpstreamResponse"
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	form := web.GetForm(ctx).(*api.MergeUpstreamRequest)
+	mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch)
+	if err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) {
+			ctx.Error(http.StatusBadRequest, "MergeUpstream", err)
+			return
+		} else if errors.Is(err, util.ErrNotExist) {
+			ctx.Error(http.StatusNotFound, "MergeUpstream", err)
+			return
+		}
+		ctx.Error(http.StatusInternalServerError, "MergeUpstream", err)
+		return
+	}
+	ctx.JSON(http.StatusOK, &api.MergeUpstreamResponse{MergeStyle: mergeStyle})
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index b9d2a0217cd4f..f754c80a5b3d5 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -448,3 +448,15 @@ type swaggerCompare struct {
 	// in:body
 	Body api.Compare `json:"body"`
 }
+
+// swagger:response MergeUpstreamRequest
+type swaggerMergeUpstreamRequest struct {
+	// in:body
+	Body api.MergeUpstreamRequest `json:"body"`
+}
+
+// swagger:response MergeUpstreamResponse
+type swaggerMergeUpstreamResponse struct {
+	// in:body
+	Body api.MergeUpstreamResponse `json:"body"`
+}
diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go
index 85ca8f7e31a7d..bfa2eeca970f8 100644
--- a/services/repository/merge_upstream.go
+++ b/services/repository/merge_upstream.go
@@ -12,17 +12,19 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/gitrepo"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/pull"
 )
 
 type UpstreamDivergingInfo struct {
-	BaseIsNewer   bool
-	CommitsBehind int
-	CommitsAhead  int
+	BaseHasNewCommits bool
+	CommitsBehind     int
+	CommitsAhead      int
 }
 
+// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
 func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
 	if err = repo.MustNotBeArchived(); err != nil {
 		return "", err
@@ -32,7 +34,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 	}
 	err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
 		Remote: repo.RepoPath(),
-		Branch: fmt.Sprintf("%s:%s", branch, branch),
+		Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch),
 		Env:    repo_module.PushingEnvironment(doer, repo),
 	})
 	if err == nil {
@@ -64,7 +66,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 		BaseRepoID: repo.BaseRepo.ID,
 		BaseRepo:   repo.BaseRepo,
 		HeadBranch: branch, // maybe HeadCommitID is not needed
-		BaseBranch: branch,
+		BaseBranch: repo.BaseRepo.DefaultBranch,
 	}
 	fakeIssue.PullRequest = fakePR
 	err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
@@ -74,6 +76,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 	return "merge", nil
 }
 
+// GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
 func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) {
 	if !repo.IsFork {
 		return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
@@ -92,7 +95,7 @@ func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository,
 		return nil, err
 	}
 
-	baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch)
+	baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch)
 	if err != nil {
 		return nil, err
 	}
@@ -102,14 +105,42 @@ func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository,
 		return info, nil
 	}
 
-	// TODO: if the fork repo has new commits, this call will fail:
+	// if the fork repo has new commits, this call will fail because they are not in the base repo
 	// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
-	// so at the moment, we are not able to handle this case, should be improved in the future
+	// so at the moment, we first check the update time, then check whether the fork branch has base's head
 	diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
 	if err != nil {
-		info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
+		info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
+		if info.BaseHasNewCommits {
+			return info, nil
+		}
+
+		// if the base's update time is before the fork, check whether the base's head is in the fork
+		baseGitRepo, baseGitRepoCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.BaseRepo)
+		if err != nil {
+			return nil, err
+		}
+		defer baseGitRepoCloser.Close()
+
+		headGitRepo, headGitRepoCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
+		if err != nil {
+			return nil, err
+		}
+		defer headGitRepoCloser.Close()
+
+		baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
+		info.BaseHasNewCommits = !hasPreviousCommit
 		return info, nil
 	}
+
 	info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead
 	return info, nil
 }
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index 51402598f95cb..bdcd99a7f7e22 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -1,8 +1,8 @@
-{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseIsNewer .UpstreamDivergingInfo.CommitsBehind)}}
+{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}}
 <div class="ui message flex-text-block">
 	<div class="tw-flex-1">
-		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}}
-		{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .BranchName}}
+		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
+		{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
 		{{if .UpstreamDivergingInfo.CommitsBehind}}
 			{{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}}
 		{{else}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 82a301da2fe99..fb37d45ce8de2 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -10867,6 +10867,52 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/merge-upstream": {
+      "post": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Merge a branch from upstream",
+        "operationId": "repoMergeUpstream",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repo",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repo",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/MergeUpstreamRequest"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/MergeUpstreamResponse"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/milestones": {
       "get": {
         "produces": [
@@ -22827,6 +22873,26 @@
       "x-go-name": "MergePullRequestForm",
       "x-go-package": "code.gitea.io/gitea/services/forms"
     },
+    "MergeUpstreamRequest": {
+      "type": "object",
+      "properties": {
+        "branch": {
+          "type": "string",
+          "x-go-name": "Branch"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
+    "MergeUpstreamResponse": {
+      "type": "object",
+      "properties": {
+        "merge_type": {
+          "type": "string",
+          "x-go-name": "MergeStyle"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "MigrateRepoOptions": {
       "description": "MigrateRepoOptions options for migrating repository's\nthis is used to interact with api v1",
       "type": "object",
@@ -26008,6 +26074,18 @@
         "type": "string"
       }
     },
+    "MergeUpstreamRequest": {
+      "description": "",
+      "schema": {
+        "$ref": "#/definitions/MergeUpstreamRequest"
+      }
+    },
+    "MergeUpstreamResponse": {
+      "description": "",
+      "schema": {
+        "$ref": "#/definitions/MergeUpstreamResponse"
+      }
+    },
     "Milestone": {
       "description": "Milestone",
       "schema": {
diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go
new file mode 100644
index 0000000000000..e3e423c51d5d2
--- /dev/null
+++ b/tests/integration/repo_merge_upstream_test.go
@@ -0,0 +1,122 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
+	git_model "code.gitea.io/gitea/models/git"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRepoMergeUpstream(t *testing.T) {
+	onGiteaRun(t, func(*testing.T, *url.URL) {
+		forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+		baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID})
+
+		checkFileContent := func(branch, exp string) {
+			req := NewRequest(t, "GET", fmt.Sprintf("/%s/test-repo-fork/raw/branch/%s/new-file.txt", forkUser.Name, branch))
+			resp := MakeRequest(t, req, http.StatusOK)
+			require.Equal(t, exp, resp.Body.String())
+		}
+
+		session := loginUser(t, forkUser.Name)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+		// create a fork
+		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{
+			Name: util.ToPointer("test-repo-fork"),
+		}).AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusAccepted)
+		forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: forkUser.ID, Name: "test-repo-fork"})
+
+		// create fork-branch
+		req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
+			"_csrf":           GetUserCSRFToken(t, session),
+			"new_branch_name": "fork-branch",
+		})
+		session.MakeRequest(t, req, http.StatusSeeOther)
+
+		queryMergeUpstreamButtonLink := func(htmlDoc *HTMLDoc) string {
+			return htmlDoc.Find(`button[data-url*="merge-upstream"]`).AttrOr("data-url", "")
+		}
+
+		t.Run("HeadBeforeBase", func(t *testing.T) {
+			// add a file in base repo
+			require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
+
+			// the repo shows a prompt to "sync fork"
+			var mergeUpstreamLink string
+			require.Eventually(t, func() bool {
+				resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+				htmlDoc := NewHTMLParser(t, resp.Body)
+				mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
+				if mergeUpstreamLink == "" {
+					return false
+				}
+				respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+				return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
+			}, 5*time.Second, 100*time.Millisecond)
+
+			// click the "sync fork" button
+			req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
+			session.MakeRequest(t, req, http.StatusOK)
+			checkFileContent("fork-branch", "test-content-1")
+		})
+
+		t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {
+			// update the files: base first, head later, and check the prompt
+			require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-2"))
+			require.NoError(t, createOrReplaceFileInBranch(forkUser, forkRepo, "new-file-other.txt", "fork-branch", "test-content-other"))
+
+			// make sure the base branch's update time is before the fork, to make it test the complete logic
+			baseBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: baseRepo.ID, Name: "master"})
+			forkBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: forkRepo.ID, Name: "fork-branch"})
+			_, err := db.GetEngine(db.DefaultContext).ID(forkBranch.ID).Update(&git_model.Branch{UpdatedUnix: baseBranch.UpdatedUnix + 1})
+			require.NoError(t, err)
+
+			// the repo shows a prompt to "sync fork"
+			require.Eventually(t, func() bool {
+				resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+				htmlDoc := NewHTMLParser(t, resp.Body)
+				respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+				return strings.Contains(respMsg, `The base branch <a href="/user2/repo1/src/branch/master">user2/repo1:master</a> has new changes`)
+			}, 5*time.Second, 100*time.Millisecond)
+
+			// and do the merge-upstream by API
+			req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
+				Branch: "fork-branch",
+			}).AddTokenAuth(token)
+			resp := MakeRequest(t, req, http.StatusOK)
+			checkFileContent("fork-branch", "test-content-2")
+
+			var mergeResp api.MergeUpstreamResponse
+			DecodeJSON(t, resp, &mergeResp)
+			assert.Equal(t, "merge", mergeResp.MergeStyle)
+
+			// after merge, there should be no "sync fork" button anymore
+			require.Eventually(t, func() bool {
+				resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+				htmlDoc := NewHTMLParser(t, resp.Body)
+				return queryMergeUpstreamButtonLink(htmlDoc) == ""
+			}, 5*time.Second, 100*time.Millisecond)
+		})
+	})
+}

From 1454e1b6eb38bf263334cada4267e54f2beee39b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 10 Jan 2025 16:00:22 +0800
Subject: [PATCH 063/206] Fix editor markdown not incrementing in a numbered
 list (#33187) (#33193)

Backport #33187 (no conflict)

Co-authored-by: Harry Vince <47283812+harryvince@users.noreply.github.com>
---
 .../js/features/comp/EditorMarkdown.test.ts   | 170 +++++++++++++++++-
 web_src/js/features/comp/EditorMarkdown.ts    | 136 +++++++++++---
 2 files changed, 273 insertions(+), 33 deletions(-)

diff --git a/web_src/js/features/comp/EditorMarkdown.test.ts b/web_src/js/features/comp/EditorMarkdown.test.ts
index 7b4b44e83cb56..9f34d77348228 100644
--- a/web_src/js/features/comp/EditorMarkdown.test.ts
+++ b/web_src/js/features/comp/EditorMarkdown.test.ts
@@ -1,4 +1,166 @@
-import {initTextareaMarkdown} from './EditorMarkdown.ts';
+import {initTextareaMarkdown, markdownHandleIndention, textareaSplitLines} from './EditorMarkdown.ts';
+
+test('textareaSplitLines', () => {
+  let ret = textareaSplitLines('a\nbc\nd', 0);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 1);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 1});
+
+  ret = textareaSplitLines('a\nbc\nd', 2);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 3);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 1});
+
+  ret = textareaSplitLines('a\nbc\nd', 4);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 2});
+
+  ret = textareaSplitLines('a\nbc\nd', 5);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 6);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 1});
+});
+
+test('markdownHandleIndention', () => {
+  const testInput = (input: string, expected?: string) => {
+    const inputPos = input.indexOf('|');
+    input = input.replace('|', '');
+    const ret = markdownHandleIndention({value: input, selStart: inputPos, selEnd: inputPos});
+    if (expected === null) {
+      expect(ret).toEqual({handled: false});
+    } else {
+      const expectedPos = expected.indexOf('|');
+      expected = expected.replace('|', '');
+      expect(ret).toEqual({
+        handled: true,
+        valueSelection: {value: expected, selStart: expectedPos, selEnd: expectedPos},
+      });
+    }
+  };
+
+  testInput(`
+  a|b
+`, `
+  a
+  |b
+`);
+
+  testInput(`
+1. a
+2. |
+`, `
+1. a
+|
+`);
+
+  testInput(`
+|1. a
+`, null); // let browser handle it
+
+  testInput(`
+1. a
+1. b|c
+`, `
+1. a
+2. b
+3. |c
+`);
+
+  testInput(`
+2. a
+2. b|
+
+1. x
+1. y
+`, `
+1. a
+2. b
+3. |
+
+1. x
+1. y
+`);
+
+  testInput(`
+2. a
+2. b
+
+1. x|
+1. y
+`, `
+2. a
+2. b
+
+1. x
+2. |
+3. y
+`);
+
+  testInput(`
+1. a
+2. b|
+3. c
+`, `
+1. a
+2. b
+3. |
+4. c
+`);
+
+  testInput(`
+1. a
+  1. b
+  2. b
+  3. b
+  4. b
+1. c|
+`, `
+1. a
+  1. b
+  2. b
+  3. b
+  4. b
+2. c
+3. |
+`);
+
+  testInput(`
+1. a
+2. a
+3. a
+4. a
+5. a
+6. a
+7. a
+8. a
+9. b|c
+`, `
+1. a
+2. a
+3. a
+4. a
+5. a
+6. a
+7. a
+8. a
+9. b
+10. |c
+`);
+
+  // this is a special case, it's difficult to re-format the parent level at the moment, so leave it to the future
+  testInput(`
+1. a
+  2. b|
+3. c
+`, `
+1. a
+  1. b
+  2. |
+3. c
+`);
+});
 
 test('EditorMarkdown', () => {
   const textarea = document.createElement('textarea');
@@ -32,10 +194,10 @@ test('EditorMarkdown', () => {
   testInput({value: '1. \n2. ', pos: 3}, {value: '\n2. ', pos: 0});
 
   testInput('- x', '- x\n- ');
-  testInput('1. foo', '1. foo\n1. ');
-  testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n1. \n2. b\n3. c', pos: 8});
+  testInput('1. foo', '1. foo\n2. ');
+  testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n2. \n3. b\n4. c', pos: 8});
   testInput('- [ ]', '- [ ]\n- ');
   testInput('- [ ] foo', '- [ ] foo\n- [ ] ');
   testInput('* [x] foo', '* [x] foo\n* [ ] ');
-  testInput('1. [x] foo', '1. [x] foo\n1. [ ] ');
+  testInput('1. [x] foo', '1. [x] foo\n2. [ ] ');
 });
diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts
index 5e2ef121f5297..d3ed492396fef 100644
--- a/web_src/js/features/comp/EditorMarkdown.ts
+++ b/web_src/js/features/comp/EditorMarkdown.ts
@@ -14,7 +14,13 @@ export function textareaInsertText(textarea, value) {
   triggerEditorContentChanged(textarea);
 }
 
-function handleIndentSelection(textarea, e) {
+type TextareaValueSelection = {
+  value: string;
+  selStart: number;
+  selEnd: number;
+}
+
+function handleIndentSelection(textarea: HTMLTextAreaElement, e) {
   const selStart = textarea.selectionStart;
   const selEnd = textarea.selectionEnd;
   if (selEnd === selStart) return; // do not process when no selection
@@ -56,53 +62,125 @@ function handleIndentSelection(textarea, e) {
   triggerEditorContentChanged(textarea);
 }
 
-function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
-  const selStart = textarea.selectionStart;
-  const selEnd = textarea.selectionEnd;
-  if (selEnd !== selStart) return; // do not process when there is a selection
+type MarkdownHandleIndentionResult = {
+  handled: boolean;
+  valueSelection?: TextareaValueSelection;
+}
+
+type TextLinesBuffer = {
+  lines: string[];
+  lengthBeforePosLine: number;
+  posLineIndex: number;
+  inlinePos: number
+}
+
+export function textareaSplitLines(value: string, pos: number): TextLinesBuffer {
+  const lines = value.split('\n');
+  let lengthBeforePosLine = 0, inlinePos = 0, posLineIndex = 0;
+  for (; posLineIndex < lines.length; posLineIndex++) {
+    const lineLength = lines[posLineIndex].length + 1;
+    if (lengthBeforePosLine + lineLength > pos) {
+      inlinePos = pos - lengthBeforePosLine;
+      break;
+    }
+    lengthBeforePosLine += lineLength;
+  }
+  return {lines, lengthBeforePosLine, posLineIndex, inlinePos};
+}
+
+function markdownReformatListNumbers(linesBuf: TextLinesBuffer, indention: string) {
+  const reDeeperIndention = new RegExp(`^${indention}\\s+`);
+  const reSameLevel = new RegExp(`^${indention}([0-9]+)\\.`);
+  let firstLineIdx: number;
+  for (firstLineIdx = linesBuf.posLineIndex - 1; firstLineIdx >= 0; firstLineIdx--) {
+    const line = linesBuf.lines[firstLineIdx];
+    if (!reDeeperIndention.test(line) && !reSameLevel.test(line)) break;
+  }
+  firstLineIdx++;
+  let num = 1;
+  for (let i = firstLineIdx; i < linesBuf.lines.length; i++) {
+    const oldLine = linesBuf.lines[i];
+    const sameLevel = reSameLevel.test(oldLine);
+    if (!sameLevel && !reDeeperIndention.test(oldLine)) break;
+    if (sameLevel) {
+      const newLine = `${indention}${num}.${oldLine.replace(reSameLevel, '')}`;
+      linesBuf.lines[i] = newLine;
+      num++;
+      if (linesBuf.posLineIndex === i) {
+        // need to correct the cursor inline position if the line length changes
+        linesBuf.inlinePos += newLine.length - oldLine.length;
+        linesBuf.inlinePos = Math.max(0, linesBuf.inlinePos);
+        linesBuf.inlinePos = Math.min(newLine.length, linesBuf.inlinePos);
+      }
+    }
+  }
+  recalculateLengthBeforeLine(linesBuf);
+}
+
+function recalculateLengthBeforeLine(linesBuf: TextLinesBuffer) {
+  linesBuf.lengthBeforePosLine = 0;
+  for (let i = 0; i < linesBuf.posLineIndex; i++) {
+    linesBuf.lengthBeforePosLine += linesBuf.lines[i].length + 1;
+  }
+}
 
-  const value = textarea.value;
+export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHandleIndentionResult {
+  const unhandled: MarkdownHandleIndentionResult = {handled: false};
+  if (tvs.selEnd !== tvs.selStart) return unhandled; // do not process when there is a selection
 
-  // find the current line
-  // * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0)
-  // * if lastIndexOf reruns -1, lineStart is 0 and it is still correct.
-  const lineStart = value.lastIndexOf('\n', selStart - 1) + 1;
-  let lineEnd = value.indexOf('\n', selStart);
-  lineEnd = lineEnd < 0 ? value.length : lineEnd;
-  let line = value.slice(lineStart, lineEnd);
-  if (!line) return; // if the line is empty, do nothing, let the browser handle it
+  const linesBuf = textareaSplitLines(tvs.value, tvs.selStart);
+  const line = linesBuf.lines[linesBuf.posLineIndex] ?? '';
+  if (!line) return unhandled; // if the line is empty, do nothing, let the browser handle it
 
   // parse the indention
-  const indention = /^\s*/.exec(line)[0];
-  line = line.slice(indention.length);
+  let lineContent = line;
+  const indention = /^\s*/.exec(lineContent)[0];
+  lineContent = lineContent.slice(indention.length);
+  if (linesBuf.inlinePos <= indention.length) return unhandled; // if cursor is at the indention, do nothing, let the browser handle it
 
   // parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
   // there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
-  const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(line);
+  const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(lineContent);
   let prefix = '';
   if (prefixMatch) {
     prefix = prefixMatch[0];
-    if (lineStart + prefix.length > selStart) prefix = ''; // do not add new line if cursor is at prefix
+    if (prefix.length > linesBuf.inlinePos) prefix = ''; // do not add new line if cursor is at prefix
   }
 
-  line = line.slice(prefix.length);
-  if (!indention && !prefix) return; // if no indention and no prefix, do nothing, let the browser handle it
+  lineContent = lineContent.slice(prefix.length);
+  if (!indention && !prefix) return unhandled; // if no indention and no prefix, do nothing, let the browser handle it
 
-  e.preventDefault();
-  if (!line) {
+  if (!lineContent) {
     // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
-    textarea.value = value.slice(0, lineStart) + value.slice(lineEnd);
-    textarea.setSelectionRange(selStart - prefix.length, selStart - prefix.length);
+    linesBuf.lines[linesBuf.posLineIndex] = '';
+    linesBuf.inlinePos = 0;
   } else {
-    // start a new line with the same indention and prefix
+    // start a new line with the same indention
     let newPrefix = prefix;
-    // a simple approach, otherwise it needs to parse the lines after the current line
     if (/^\d+\./.test(prefix)) newPrefix = `1. ${newPrefix.slice(newPrefix.indexOf('.') + 2)}`;
     newPrefix = newPrefix.replace('[x]', '[ ]');
-    const newLine = `\n${indention}${newPrefix}`;
-    textarea.value = value.slice(0, selStart) + newLine + value.slice(selEnd);
-    textarea.setSelectionRange(selStart + newLine.length, selStart + newLine.length);
+
+    const inlinePos = linesBuf.inlinePos;
+    linesBuf.lines[linesBuf.posLineIndex] = line.substring(0, inlinePos);
+    const newLineLeft = `${indention}${newPrefix}`;
+    const newLine = `${newLineLeft}${line.substring(inlinePos)}`;
+    linesBuf.lines.splice(linesBuf.posLineIndex + 1, 0, newLine);
+    linesBuf.posLineIndex++;
+    linesBuf.inlinePos = newLineLeft.length;
+    recalculateLengthBeforeLine(linesBuf);
   }
+
+  markdownReformatListNumbers(linesBuf, indention);
+  const newPos = linesBuf.lengthBeforePosLine + linesBuf.inlinePos;
+  return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
+}
+
+function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
+  const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
+  if (!ret.handled) return;
+  e.preventDefault();
+  textarea.value = ret.valueSelection.value;
+  textarea.setSelectionRange(ret.valueSelection.selStart, ret.valueSelection.selEnd);
   triggerEditorContentChanged(textarea);
 }
 

From fcbbc24cc43811f2fc2f99b2f88060bc360ec38f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 10 Jan 2025 00:06:45 -0800
Subject: [PATCH 064/206] Change log for 1.23.1 (#33191)

---
 CHANGELOG.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4a846078fac2..61644628e7f4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,20 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.1](https://github.com/go-gitea/gitea/releases/tag/v1.23.1) - 2025-01-09
+
+* ENHANCEMENTS
+  * Move repo size to sidebar (#33155) (#33182)
+* BUGFIXES
+  * Use updated path to s6-svscan after alpine upgrade (#33185) (#33188)
+  * Fix fuzz test (#33156) (#33158)
+  * Fix raw file API ref handling (#33172) (#33189)
+  * Fix ACME panic (#33178) (#33186)
+  * Fix branch dropdown not display ref name (#33159) (#33183)
+  * Fix assignee list overlapping in Issue sidebar (#33176) (#33181)
+  * Fix sync fork for consistency (#33147) #33192
+  * Fix editor markdown not incrementing in a numbered list (#33187) #33193
+
 ## [1.23.0](https://github.com/go-gitea/gitea/releases/tag/v1.23.0) - 2025-01-08
 
 * BREAKING

From 31f2a325dcf18927d0958a5a7cd143093433d925 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 12 Jan 2025 09:19:37 +0800
Subject: [PATCH 065/206] fix(cache): cache test triggered by non memory cache
 (#33220) (#33221)

Backport #33220 by TheFox0x7

Change SlowCacheThreshold to 30 milliseconds so it doesn't trigger on
non memory cache

Closes: https://github.com/go-gitea/gitea/issues/33190
Closes: https://github.com/go-gitea/gitea/issues/32657

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
---
 modules/cache/cache.go      | 9 +++++++--
 modules/cache/cache_test.go | 3 ++-
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/modules/cache/cache.go b/modules/cache/cache.go
index b5400b0bd6a14..f7828e3cae2df 100644
--- a/modules/cache/cache.go
+++ b/modules/cache/cache.go
@@ -37,10 +37,15 @@ func Init() error {
 }
 
 const (
-	testCacheKey       = "DefaultCache.TestKey"
-	SlowCacheThreshold = 100 * time.Microsecond
+	testCacheKey = "DefaultCache.TestKey"
+	// SlowCacheThreshold marks cache tests as slow
+	// set to 30ms per discussion: https://github.com/go-gitea/gitea/issues/33190
+	// TODO: Replace with metrics histogram
+	SlowCacheThreshold = 30 * time.Millisecond
 )
 
+// Test performs delete, put and get operations on a predefined key
+// returns
 func Test() (time.Duration, error) {
 	if defaultCache == nil {
 		return 0, fmt.Errorf("default cache not initialized")
diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go
index d0352947a831a..5408020306b38 100644
--- a/modules/cache/cache_test.go
+++ b/modules/cache/cache_test.go
@@ -43,7 +43,8 @@ func TestTest(t *testing.T) {
 	elapsed, err := Test()
 	assert.NoError(t, err)
 	// mem cache should take from 300ns up to 1ms on modern hardware ...
-	assert.Less(t, elapsed, time.Millisecond)
+	assert.Positive(t, elapsed)
+	assert.Less(t, elapsed, SlowCacheThreshold)
 }
 
 func TestGetCache(t *testing.T) {

From 2c47b0686933a2339474b6676a8411fb9e0fc0db Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 12 Jan 2025 19:11:02 +0800
Subject: [PATCH 066/206] Fix mirror bug (#33224) (#33225)

Backport #33224 by lunny

Fix #33200

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/git/ref_test.go        |  2 ++
 services/mirror/mirror_pull.go | 15 ++++++++++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/modules/git/ref_test.go b/modules/git/ref_test.go
index 58f679b7d6ee8..1fd33b5163ead 100644
--- a/modules/git/ref_test.go
+++ b/modules/git/ref_test.go
@@ -20,6 +20,8 @@ func TestRefName(t *testing.T) {
 
 	// Test pull names
 	assert.Equal(t, "1", RefName("refs/pull/1/head").PullName())
+	assert.True(t, RefName("refs/pull/1/head").IsPull())
+	assert.True(t, RefName("refs/pull/1/merge").IsPull())
 	assert.Equal(t, "my/pull", RefName("refs/pull/my/pull/head").PullName())
 
 	// Test for branch names
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 34879ddd16b9e..4a8bcd03a03e5 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -97,6 +97,7 @@ type mirrorSyncResult struct {
 /*
 // * [new tag]         v0.1.8     -> v0.1.8
 // * [new branch]      master     -> origin/master
+// * [new ref]         refs/pull/2/head  -> refs/pull/2/head"
 // - [deleted]         (none)     -> origin/test // delete a branch
 // - [deleted]         (none)     -> 1 // delete a tag
 //   957a993..a87ba5f  test       -> origin/test
@@ -127,6 +128,11 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
 				refName:     git.RefNameFromBranch(refName),
 				oldCommitID: gitShortEmptySha,
 			})
+		case strings.HasPrefix(lines[i], " * [new ref]"): // new reference
+			results = append(results, &mirrorSyncResult{
+				refName:     git.RefName(refName),
+				oldCommitID: gitShortEmptySha,
+			})
 		case strings.HasPrefix(lines[i], " - "): // Delete reference
 			isTag := !strings.HasPrefix(refName, remoteName+"/")
 			var refFullName git.RefName
@@ -169,8 +175,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
 				log.Error("Expect two SHAs but not what found: %q", lines[i])
 				continue
 			}
+			var refFullName git.RefName
+			if strings.HasPrefix(refName, "refs/") {
+				refFullName = git.RefName(refName)
+			} else {
+				refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
+			}
+
 			results = append(results, &mirrorSyncResult{
-				refName:     git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")),
+				refName:     refFullName,
 				oldCommitID: shas[0],
 				newCommitID: shas[1],
 			})

From 10b604749897bab87724035f67a1155741985994 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 13 Jan 2025 07:49:56 +0800
Subject: [PATCH 067/206] Fix upload file form (#33230) (#33233)

Backport #33230 by @wxiaoguang

Fix #33228

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/repo-editor.ts | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts
index 32d0b84f4c9bb..239035b3fec1a 100644
--- a/web_src/js/features/repo-editor.ts
+++ b/web_src/js/features/repo-editor.ts
@@ -38,9 +38,6 @@ export function initRepoEditor() {
   const dropzoneUpload = document.querySelector<HTMLElement>('.page-content.repository.editor.upload .dropzone');
   if (dropzoneUpload) initDropzone(dropzoneUpload);
 
-  const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
-  if (!editArea) return;
-
   for (const el of queryElems<HTMLInputElement>(document, '.js-quick-pull-choice-option')) {
     el.addEventListener('input', () => {
       if (el.value === 'commit-to-new-branch') {
@@ -55,6 +52,7 @@ export function initRepoEditor() {
   }
 
   const filenameInput = document.querySelector<HTMLInputElement>('#file-name');
+  if (!filenameInput) return;
   function joinTreePath() {
     const parts = [];
     for (const el of document.querySelectorAll('.breadcrumb span.section')) {
@@ -144,6 +142,10 @@ export function initRepoEditor() {
     }
   });
 
+  // on the upload page, there is no editor(textarea)
+  const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
+  if (!editArea) return;
+
   const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
   initEditPreviewTab(elForm);
 

From 80cc87b3d8b0342f7ef6a3b7e65d1ed12520ff66 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 14 Jan 2025 14:01:30 +0800
Subject: [PATCH 068/206] Fix tag route and empty repo (#33253)

---
 routers/web/repo/view_home.go        |  2 +-
 routers/web/web.go                   |  3 +--
 tests/integration/empty_repo_test.go | 21 ++++++++++++++++++++-
 3 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index 169c1a277f2fd..2d0d6ca28ae23 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -249,7 +249,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
 		} else if reallyEmpty {
 			showEmpty = true // the repo is really empty
 			updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
-		} else if ctx.Repo.Commit == nil {
+		} else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 {
 			showEmpty = true // it is not really empty, but there is no branch
 			// at the moment, other repo units like "actions" are not able to handle such case,
 			// so we just mark the repo as empty to prevent from displaying these units.
diff --git a/routers/web/web.go b/routers/web/web.go
index a243a79f8a17d..3c6e0862afb5e 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1335,8 +1335,7 @@ func registerRoutes(m *web.Router) {
 			m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
 		}, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
 			repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, context.RepoRefByTypeOptions{IgnoreNotExistErr: true}))
-		m.Post("/tags/delete", repo.DeleteTag, reqSignIn,
-			repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, context.RepoRef())
+		m.Post("/tags/delete", reqSignIn, repo.MustBeNotEmpty, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.DeleteTag)
 	}, optSignIn, context.RepoAssignment, reqRepoCodeReader)
 	// end "/{username}/{reponame}": repo tags
 
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index 0801b093dfb07..bb75aebd66a8f 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -14,6 +14,7 @@ import (
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -24,6 +25,7 @@ import (
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder {
@@ -60,7 +62,9 @@ func TestEmptyRepoAddFile(t *testing.T) {
 	session := loginUser(t, "user30")
 	req := NewRequest(t, "GET", "/user30/empty")
 	resp := session.MakeRequest(t, req, http.StatusOK)
-	assert.Contains(t, resp.Body.String(), "empty-repo-guide")
+	bodyString := resp.Body.String()
+	assert.Contains(t, bodyString, "empty-repo-guide")
+	assert.True(t, test.IsNormalPageCompleted(bodyString))
 
 	req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch)
 	resp = session.MakeRequest(t, req, http.StatusOK)
@@ -80,6 +84,21 @@ func TestEmptyRepoAddFile(t *testing.T) {
 	req = NewRequest(t, "GET", redirect)
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	assert.Contains(t, resp.Body.String(), "newly-added-test-file")
+
+	// the repo is not empty anymore
+	req = NewRequest(t, "GET", "/user30/empty")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "test-file.md")
+
+	// if the repo is in incorrect state, it should be able to self-heal (recover to correct state)
+	user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"})
+	user30EmptyRepo.IsEmpty = true
+	user30EmptyRepo.DefaultBranch = "no-such"
+	_, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Update(user30EmptyRepo)
+	require.NoError(t, err)
+	req = NewRequest(t, "GET", "/user30/empty")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "test-file.md")
 }
 
 func TestEmptyRepoUploadFile(t *testing.T) {

From 2984a7c121963c7c975d3b29666f99e5493c827e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 14 Jan 2025 14:26:27 +0800
Subject: [PATCH 069/206] Fix missing license when sync mirror (#33255)
 (#33258)

Backport #33255 by yp05327

Fix #33222

Co-authored-by: yp05327 <576951401@qq.com>
---
 models/repo/license.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/models/repo/license.go b/models/repo/license.go
index 366b4598cc4e0..9bcf0f7bc97f5 100644
--- a/models/repo/license.go
+++ b/models/repo/license.go
@@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string,
 		for _, o := range oldLicenses {
 			// Update already existing license
 			if o.License == license {
+				o.CommitID = commitID
 				if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
 					return err
 				}

From b4e2d5e8ee88e6f1cef93a6689caa5c156b2afc1 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 15 Jan 2025 06:51:17 +0800
Subject: [PATCH 070/206] Add a confirm dialog for "sync fork" (#33270)
 (#33273)

Backport #33270 by @wxiaoguang

Try to quickly fix #33264

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 options/locale/locale_en-US.ini                  | 1 +
 templates/repo/code/upstream_diverging_info.tmpl | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2c10a4dbe1f8e..c7cede4dc0f62 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1951,6 +1951,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
 pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
 pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
 pulls.upstream_diverging_merge = Sync fork
+pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
 
 pull.deleted_branch = (deleted):%s
 pull.agit_documentation = Review documentation about AGit
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index bdcd99a7f7e22..a1f37b8c05d51 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -10,7 +10,10 @@
 		{{end}}
 	</div>
 	{{if .CanWriteCode}}
-	<button class="ui compact primary button tw-m-0 link-action" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
+	<button class="ui compact primary button tw-m-0 link-action"
+					data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
+					data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
+					data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
 		{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
 	</button>
 	{{end}}

From 3b4af016339a9cad0f07c3fd1f696797e30f84c3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 15 Jan 2025 08:19:37 +0800
Subject: [PATCH 071/206] Fix sidebar milestone link (#33269) (#33272)

Backport #33269 by wxiaoguang

Fix  #33266

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/issue/sidebar/milestone_list.tmpl | 8 ++++----
 templates/repo/issue/sidebar/project_list.tmpl   | 4 ++--
 templates/repo/issue/sidebar/reviewer_list.tmpl  | 6 +++---
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl
index 0e926f7b03329..bf3582c54bd20 100644
--- a/templates/repo/issue/sidebar/milestone_list.tmpl
+++ b/templates/repo/issue/sidebar/milestone_list.tmpl
@@ -24,7 +24,7 @@
 					{{if $data.OpenMilestones}}
 						<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
 						{{range $data.OpenMilestones}}
-							<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
+							<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/milestone/{{.ID}}">
 								{{svg "octicon-milestone" 18}} {{.Name}}
 							</a>
 						{{end}}
@@ -33,7 +33,7 @@
 					{{if $data.ClosedMilestones}}
 						<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_closed"}}</div>
 						{{range $data.ClosedMilestones}}
-							<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/issues?milestone={{.ID}}">
+							<a class="item muted" data-value="{{.ID}}" href="{{$pageMeta.RepoLink}}/milestone/{{.ID}}">
 								{{svg "octicon-milestone" 18}} {{.Name}}
 							</a>
 						{{end}}
@@ -43,10 +43,10 @@
 		</div>
 	</div>
 
-	<div class="ui list">
+	<div class="ui list muted-links flex-items-block">
 		<span class="item empty-list {{if $issueMilestone}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_milestone"}}</span>
 		{{if $issueMilestone}}
-			<a class="item muted" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}">
+			<a class="item" href="{{$pageMeta.RepoLink}}/milestone/{{$issueMilestone.ID}}">
 				{{svg "octicon-milestone" 18}} {{$issueMilestone.Name}}
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl
index 0cbbdce7608b9..39feb089ef7cb 100644
--- a/templates/repo/issue/sidebar/project_list.tmpl
+++ b/templates/repo/issue/sidebar/project_list.tmpl
@@ -39,10 +39,10 @@
 			</div>
 		</div>
 	</div>
-	<div class="ui list">
+	<div class="ui list muted-links flex-items-block">
 		<span class="item empty-list {{if $issueProject}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_projects"}}</span>
 		{{if $issueProject}}
-			<a class="item muted" href="{{$issueProject.Link ctx}}">
+			<a class="item" href="{{$issueProject.Link ctx}}">
 				{{svg $issueProject.IconName 18}} {{$issueProject.Title}}
 			</a>
 		{{end}}
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
index b3d9590d585ec..b08af03a01eed 100644
--- a/templates/repo/issue/sidebar/reviewer_list.tmpl
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -51,9 +51,9 @@
 			<div class="item">
 				<div class="flex-text-inline tw-flex-1">
 					{{if .User}}
-						<a class="muted flex-text-inline" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a>
+						<a class="muted flex-text-inline tw-gap-2" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}} {{.User.GetDisplayName}}</a>
 					{{else if .Team}}
-						{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}
+						<span class="flex-text-inline tw-gap-2">{{svg "octicon-people" 20}} {{$repoOwnerName}}/{{.Team.Name}}</span>
 					{{end}}
 				</div>
 				<div class="flex-text-inline">
@@ -92,7 +92,7 @@
 				<div class="flex-text-inline tw-flex-1">
 					{{$originalURLHostname := $pageMeta.Repository.GetOriginalURLHostname}}
 					{{$originalURL := $pageMeta.Repository.OriginalURL}}
-					<a class="muted flex-text-inline" href="{{$originalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $originalURLHostname}}">
+					<a class="muted flex-text-inline tw-gap-2" href="{{$originalURL}}" data-tooltip-content="{{ctx.Locale.Tr "repo.migrated_from_fake" $originalURLHostname}}">
 						{{svg (MigrationIcon $originalURLHostname) 20}} {{.OriginalAuthor}}
 					</a>
 				</div>

From c09e43acf5389a66f10fe08b1294c853fa05c2e7 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 16 Jan 2025 00:05:01 +0800
Subject: [PATCH 072/206] Fix closed dependency title (#33285) (#33287)

Backport #33285
---
 templates/repo/issue/sidebar/issue_dependencies.tmpl | 6 +++---
 web_src/css/repo.css                                 | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/templates/repo/issue/sidebar/issue_dependencies.tmpl b/templates/repo/issue/sidebar/issue_dependencies.tmpl
index 837bcd56705d2..17e9738c6f77a 100644
--- a/templates/repo/issue/sidebar/issue_dependencies.tmpl
+++ b/templates/repo/issue/sidebar/issue_dependencies.tmpl
@@ -22,7 +22,7 @@
 				{{range .BlockingDependencies}}
 					<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 						<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
-							<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
+							<a class="muted issue-dependency-title gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
 								#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
 							</a>
 							<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
@@ -54,7 +54,7 @@
 				{{range .BlockedByDependencies}}
 					<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} tw-flex tw-items-center tw-justify-between">
 						<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
-							<a class="muted gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
+							<a class="muted issue-dependency-title gt-ellipsis" href="{{.Issue.Link}}" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
 								#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
 							</a>
 							<div class="text small gt-ellipsis" data-tooltip-content="{{.Repository.OwnerName}}/{{.Repository.Name}}">
@@ -76,7 +76,7 @@
 							<div class="item-left tw-flex tw-justify-center tw-flex-col tw-flex-1 gt-ellipsis">
 								<div class="gt-ellipsis">
 									<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
-									<span class="gt-ellipsis" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
+									<span class="gt-ellipsis issue-dependency-title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}">
 										#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}}
 									</span>
 								</div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 595acd288f1ae..7a39a725a50dd 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -796,7 +796,7 @@ td .commit-summary {
   box-shadow: none;
 }
 
-.repository.view.issue .ui.depending .item.is-closed .title {
+.repository.view.issue .ui.depending .item.is-closed .issue-dependency-title {
   text-decoration: line-through;
 }
 

From 52d298890b009f719713a4d029ad11667ae20407 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 17 Jan 2025 04:48:31 +0800
Subject: [PATCH 073/206] Fix "stop time tracking button" on navbar (#33084)
 (#33300)

Backport #33084 (no conflict)
Fix #33299, and remove incorrect translations
---
 cmd/migrate.go                      | 2 +-
 modules/markup/sanitizer_default.go | 2 +-
 options/locale/locale_fr-FR.ini     | 1 -
 options/locale/locale_zh-CN.ini     | 2 --
 templates/base/head_navbar.tmpl     | 4 ++--
 5 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/cmd/migrate.go b/cmd/migrate.go
index 4e4dd45af3a15..459805a76d732 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -18,7 +18,7 @@ import (
 var CmdMigrate = &cli.Command{
 	Name:        "migrate",
 	Usage:       "Migrate the database",
-	Description: "This is a command for migrating the database, so that you can run gitea admin create-user before starting the server.",
+	Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`,
 	Action:      runMigrate,
 }
 
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
index 5eeafe940a4da..14161eb533794 100644
--- a/modules/markup/sanitizer_default.go
+++ b/modules/markup/sanitizer_default.go
@@ -48,7 +48,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
 	policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
 
 	// Allow 'color' and 'background-color' properties for the style attribute on text elements.
-	policy.AllowStyles("color", "background-color").OnElements("span", "p")
+	policy.AllowStyles("color", "background-color").OnElements("div", "span", "p", "tr", "th", "td")
 
 	policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
 
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index 0870983c98621..c025ed0aba995 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -1687,7 +1687,6 @@ issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a hr
 issues.stop_tracking_history=`a fini de travailler sur <b>%s</b> %s.`
 issues.cancel_tracking_history=`a abandonné son minuteur %s.`
 issues.del_time=Supprimer ce minuteur du journal
-issues.add_time_history=`a pointé du temps de travail %s.`
 issues.del_time_history=`a supprimé son temps de travail %s.`
 issues.add_time_manually=Temps pointé manuellement
 issues.add_time_hours=Heures
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 8c6392a4dc995..003aa103e522a 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1687,10 +1687,8 @@ issues.time_estimate_invalid=预计时间格式无效
 issues.start_tracking_history=`开始工作 %s`
 issues.tracker_auto_close=当此工单关闭时，自动停止计时器
 issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪！`
-issues.stop_tracking_history=`停止工作 %s`
 issues.cancel_tracking_history=`取消时间跟踪 %s`
 issues.del_time=删除此时间跟踪日志
-issues.add_time_history=`添加计时 %s`
 issues.del_time_history=`已删除时间 %s`
 issues.add_time_manually=手动添加时间
 issues.add_time_hours=小时
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index bbb2a7ef0172f..86d73a235dbed 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -191,7 +191,7 @@
 					<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span>
 				</a>
 				<div class="tw-flex tw-gap-1">
-					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
+					<form class="stopwatch-commit form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle">
 						{{.CsrfTokenHtml}}
 						<button
 							type="submit"
@@ -199,7 +199,7 @@
 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
 						>{{svg "octicon-square-fill"}}</button>
 					</form>
-					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
+					<form class="stopwatch-cancel form-fetch-action" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel">
 						{{.CsrfTokenHtml}}
 						<button
 							type="submit"

From bf664c2e85827f3b745033477f8b68d5848ad879 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 18 Jan 2025 03:09:17 +0800
Subject: [PATCH 074/206] Trivial fixes (#33304) (#33312)

Backport #33304

The only conflict is caused by `templates/shared/issueicon.tmpl`
---
 modules/repository/branch.go             |  4 --
 options/locale/locale_en-US.ini          |  3 +-
 routers/web/repo/actions/view.go         |  2 +-
 routers/web/repo/issue_dependency.go     |  2 +-
 routers/web/repo/issue_watch.go          |  2 +-
 templates/package/content/container.tmpl |  2 +-
 templates/package/content/maven.tmpl     |  2 +-
 templates/repo/branch/list.tmpl          |  2 +-
 templates/repo/issue/view_title.tmpl     |  2 +-
 templates/shared/issueicon.tmpl          | 49 ++++++++++++------------
 web_src/js/features/issue.ts             | 23 +++++++----
 web_src/js/svg.ts                        |  2 +
 12 files changed, 52 insertions(+), 43 deletions(-)

diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index 4630e70aa8f39..2bf9930f19fd3 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -6,7 +6,6 @@ package repository
 import (
 	"context"
 	"fmt"
-	"strings"
 
 	"code.gitea.io/gitea/models/db"
 	git_model "code.gitea.io/gitea/models/git"
@@ -52,9 +51,6 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 	{
 		branches, _, err := gitRepo.GetBranchNames(0, 0)
 		if err != nil {
-			if strings.Contains(err.Error(), "ref file is empty") {
-				return 0, nil
-			}
 			return 0, err
 		}
 		log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index c7cede4dc0f62..8c18395e17bea 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3561,7 +3561,8 @@ conda.install = To install the package using Conda, run the following command:
 container.details.type = Image Type
 container.details.platform = Platform
 container.pull = Pull the image from the command line:
-container.digest = Digest:
+container.images = Images
+container.digest = Digest
 container.multi_arch = OS / Arch
 container.layers = Image Layers
 container.labels = Labels
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index ba17fa427d1a8..fe490ac717b6e 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -855,7 +855,7 @@ func Run(ctx *context_module.Context) {
 	inputs := make(map[string]any)
 	if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
 		for name, config := range workflowDispatch.Inputs {
-			value := ctx.Req.PostForm.Get(name)
+			value := ctx.Req.PostFormValue(name)
 			if config.Type == "boolean" {
 				// https://www.w3.org/TR/html401/interact/forms.html
 				// https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index f1d133edb0064..0f6787386d99a 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) {
 	}
 
 	// Dependency Type
-	depTypeStr := ctx.Req.PostForm.Get("dependencyType")
+	depTypeStr := ctx.Req.PostFormValue("dependencyType")
 
 	var depType issues_model.DependencyType
 
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 8b033f3b17a80..6799bf8eb292e 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) {
 		return
 	}
 
-	watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch"))
+	watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch"))
 	if err != nil {
 		ctx.ServerError("watch is not bool", err)
 		return
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index 04732d276ae52..a88ebec3bca15 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -24,7 +24,7 @@
 		</div>
 	</div>
 	{{if .PackageDescriptor.Metadata.Manifests}}
-		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.multi_arch"}}</h4>
+		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.images"}}</h4>
 		<div class="ui attached segment">
 			<table class="ui very basic compact table">
 				<thead>
diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl
index f56595a8308ec..e98fc53692479 100644
--- a/templates/package/content/maven.tmpl
+++ b/templates/package/content/maven.tmpl
@@ -11,7 +11,7 @@
 				<div class="markup"><pre class="code-block"><code>&lt;repositories&gt;
 	&lt;repository&gt;
 		&lt;id&gt;gitea&lt;/id&gt;
-			&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
+		&lt;url&gt;<origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url>&lt;/url&gt;
 	&lt;/repository&gt;
 &lt;/repositories&gt;
 
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 5484024ff8197..f10b2ef7117f3 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -142,7 +142,7 @@
 									{{if .LatestPullRequest.HasMerged}}
 										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a>
 									{{else if .LatestPullRequest.Issue.IsClosed}}
-										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
+										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request-closed" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a>
 									{{else}}
 										<a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a>
 									{{end}}
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index bf2ede58e487d..7bb00ff9a9488 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -42,7 +42,7 @@
 		{{if .HasMerged}}
 			<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div>
 		{{else if .Issue.IsClosed}}
-			<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
+			<div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div>
 		{{else if .Issue.IsPull}}
 			{{if .IsPullWorkInProgress}}
 				<div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div>
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl
index a62714e988ca3..bb6247c708854 100644
--- a/templates/shared/issueicon.tmpl
+++ b/templates/shared/issueicon.tmpl
@@ -1,25 +1,26 @@
-{{if .IsPull}}
-	{{if not .PullRequest}}
+{{/* the logic should be kept the same as getIssueIcon/getIssueColor in JS code */}}
+{{- if .IsPull -}}
+	{{- if not .PullRequest -}}
 		No PullRequest
-	{{else}}
-		{{if .IsClosed}}
-			{{if .PullRequest.HasMerged}}
-				{{svg "octicon-git-merge" 16 "text purple"}}
-			{{else}}
-				{{svg "octicon-git-pull-request" 16 "text red"}}
-			{{end}}
-		{{else}}
-			{{if .PullRequest.IsWorkInProgress ctx}}
-				{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
-			{{else}}
-				{{svg "octicon-git-pull-request" 16 "text green"}}
-			{{end}}
-		{{end}}
-	{{end}}
-{{else}}
-	{{if .IsClosed}}
-		{{svg "octicon-issue-closed" 16 "text red"}}
-	{{else}}
-		{{svg "octicon-issue-opened" 16 "text green"}}
-	{{end}}
-{{end}}
+	{{- else -}}
+		{{- if .IsClosed -}}
+			{{- if .PullRequest.HasMerged -}}
+				{{- svg "octicon-git-merge" 16 "text purple" -}}
+			{{- else -}}
+				{{- svg "octicon-git-pull-request-closed" 16 "text red" -}}
+			{{- end -}}
+		{{- else -}}
+			{{- if .PullRequest.IsWorkInProgress ctx -}}
+				{{- svg "octicon-git-pull-request-draft" 16 "text grey" -}}
+			{{- else -}}
+				{{- svg "octicon-git-pull-request" 16 "text green" -}}
+			{{- end -}}
+		{{- end -}}
+	{{- end -}}
+{{- else -}}
+	{{- if .IsClosed -}}
+		{{- svg "octicon-issue-closed" 16 "text red" -}}
+	{{- else -}}
+		{{- svg "octicon-issue-opened" 16 "text green" -}}
+	{{- end -}}
+{{- end -}}
diff --git a/web_src/js/features/issue.ts b/web_src/js/features/issue.ts
index a56015a2a25bc..911cf713d9059 100644
--- a/web_src/js/features/issue.ts
+++ b/web_src/js/features/issue.ts
@@ -1,17 +1,21 @@
 import type {Issue} from '../types.ts';
 
+// the getIssueIcon/getIssueColor logic should be kept the same as "templates/shared/issueicon.tmpl"
+
 export function getIssueIcon(issue: Issue) {
   if (issue.pull_request) {
     if (issue.state === 'open') {
-      if (issue.pull_request.draft === true) {
+      if (issue.pull_request.draft) {
         return 'octicon-git-pull-request-draft'; // WIP PR
       }
       return 'octicon-git-pull-request'; // Open PR
-    } else if (issue.pull_request.merged === true) {
+    } else if (issue.pull_request.merged) {
       return 'octicon-git-merge'; // Merged PR
     }
-    return 'octicon-git-pull-request'; // Closed PR
-  } else if (issue.state === 'open') {
+    return 'octicon-git-pull-request-closed'; // Closed PR
+  }
+
+  if (issue.state === 'open') {
     return 'octicon-issue-opened'; // Open Issue
   }
   return 'octicon-issue-closed'; // Closed Issue
@@ -19,12 +23,17 @@ export function getIssueIcon(issue: Issue) {
 
 export function getIssueColor(issue: Issue) {
   if (issue.pull_request) {
-    if (issue.pull_request.draft === true) {
-      return 'grey'; // WIP PR
-    } else if (issue.pull_request.merged === true) {
+    if (issue.state === 'open') {
+      if (issue.pull_request.draft) {
+        return 'grey'; // WIP PR
+      }
+      return 'green'; // Open PR
+    } else if (issue.pull_request.merged) {
       return 'purple'; // Merged PR
     }
+    return 'red'; // Closed PR
   }
+
   if (issue.state === 'open') {
     return 'green'; // Open Issue
   }
diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts
index 90b12fa87d753..8b4a2df3361ae 100644
--- a/web_src/js/svg.ts
+++ b/web_src/js/svg.ts
@@ -34,6 +34,7 @@ import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg
 import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg';
 import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg';
 import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg';
+import octiconGitPullRequestClosed from '../../public/assets/img/svg/octicon-git-pull-request-closed.svg';
 import octiconGitPullRequestDraft from '../../public/assets/img/svg/octicon-git-pull-request-draft.svg';
 import octiconGrabber from '../../public/assets/img/svg/octicon-grabber.svg';
 import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg';
@@ -110,6 +111,7 @@ const svgs = {
   'octicon-git-commit': octiconGitCommit,
   'octicon-git-merge': octiconGitMerge,
   'octicon-git-pull-request': octiconGitPullRequest,
+  'octicon-git-pull-request-closed': octiconGitPullRequestClosed,
   'octicon-git-pull-request-draft': octiconGitPullRequestDraft,
   'octicon-grabber': octiconGrabber,
   'octicon-heading': octiconHeading,

From 8d9ea68f19da6f80599ff062abc2b2757bee5dbd Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 19 Jan 2025 10:48:28 +0800
Subject: [PATCH 075/206] Fix push message behavior (#33215) (#33317)

Backport #33215

Manually resolved "reqctx" conflict

---------

Co-authored-by: Chai-Shi <changchaishi@gmail.com>
---
 models/git/branch.go                          |   4 +
 routers/web/repo/view_home.go                 |  20 ++-
 services/repository/branch.go                 |  58 +++++++++
 services/repository/merge_upstream.go         |  70 +----------
 .../repo/code/upstream_diverging_info.tmpl    |   6 +-
 tests/integration/repo_branch_test.go         | 116 ++++++++----------
 6 files changed, 139 insertions(+), 135 deletions(-)

diff --git a/models/git/branch.go b/models/git/branch.go
index e683ce47e657e..a264f555d9dd4 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -440,6 +440,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
 }
 
 type RecentlyPushedNewBranch struct {
+	BranchRepo        *repo_model.Repository
+	BranchName        string
 	BranchDisplayName string
 	BranchLink        string
 	BranchCompareURL  string
@@ -540,7 +542,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
 				branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
 			}
 			newBranches = append(newBranches, &RecentlyPushedNewBranch{
+				BranchRepo:        branch.Repo,
 				BranchDisplayName: branchDisplayName,
+				BranchName:        branch.Name,
 				BranchLink:        fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
 				BranchCompareURL:  branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
 				CommitTime:        branch.CommitTime,
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index 2d0d6ca28ae23..d7a393c5cd3b4 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -215,10 +215,28 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
 		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror &&
 			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) &&
 			baseRepoPerm.CanRead(unit_model.TypePullRequests) {
-			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
+			var finalBranches []*git_model.RecentlyPushedNewBranch
+			branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
 			if err != nil {
 				log.Error("FindRecentlyPushedNewBranches failed: %v", err)
 			}
+
+			for _, branch := range branches {
+				divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx,
+					branch.BranchRepo, branch.BranchName, // "base" repo for diverging info
+					opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info
+				)
+				if err != nil {
+					log.Error("GetBranchDivergingInfo failed: %v", err)
+					continue
+				}
+				branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits
+				baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind
+				if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 {
+					finalBranches = append(finalBranches, branch)
+				}
+			}
+			ctx.Data["RecentlyPushedNewBranches"] = finalBranches
 		}
 	}
 }
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 3a95aab264981..4f3accbe62341 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -636,3 +636,61 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 
 	return nil
 }
+
+// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch.
+// This struct is also used in templates, so it needs to search for all references before changing it.
+type BranchDivergingInfo struct {
+	BaseHasNewCommits bool
+	HeadCommitsBehind int
+	HeadCommitsAhead  int
+}
+
+// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch.
+func GetBranchDivergingInfo(ctx context.Context, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) {
+	headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	info := &BranchDivergingInfo{}
+	if headGitBranch.CommitID == baseGitBranch.CommitID {
+		return info, nil
+	}
+
+	// if the fork repo has new commits, this call will fail because they are not in the base repo
+	// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
+	// so at the moment, we first check the update time, then check whether the fork branch has base's head
+	diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID)
+	if err != nil {
+		info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix
+		if headRepo.IsFork && info.BaseHasNewCommits {
+			return info, nil
+		}
+		// if the base's update time is before the fork, check whether the base's head is in the fork
+		headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, headRepo)
+		if err != nil {
+			return nil, err
+		}
+		defer closer.Close()
+
+		headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID)
+		if err != nil {
+			return nil, err
+		}
+		hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
+		info.BaseHasNewCommits = !hasPreviousCommit
+		return info, nil
+	}
+
+	info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead
+	return info, nil
+}
diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go
index bfa2eeca970f8..91bd01b3715c9 100644
--- a/services/repository/merge_upstream.go
+++ b/services/repository/merge_upstream.go
@@ -7,23 +7,15 @@ import (
 	"context"
 	"fmt"
 
-	git_model "code.gitea.io/gitea/models/git"
 	issue_model "code.gitea.io/gitea/models/issues"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/gitrepo"
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/pull"
 )
 
-type UpstreamDivergingInfo struct {
-	BaseHasNewCommits bool
-	CommitsBehind     int
-	CommitsAhead      int
-}
-
 // MergeUpstream merges the base repository's default branch into the fork repository's current branch.
 func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
 	if err = repo.MustNotBeArchived(); err != nil {
@@ -77,70 +69,18 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 }
 
 // GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
-func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) {
-	if !repo.IsFork {
+func GetUpstreamDivergingInfo(ctx context.Context, forkRepo *repo_model.Repository, forkBranch string) (*BranchDivergingInfo, error) {
+	if !forkRepo.IsFork {
 		return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
 	}
 
-	if repo.IsArchived {
+	if forkRepo.IsArchived {
 		return nil, util.NewInvalidArgumentErrorf("repo is archived")
 	}
 
-	if err := repo.GetBaseRepo(ctx); err != nil {
-		return nil, err
-	}
-
-	forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
-	if err != nil {
-		return nil, err
-	}
-
-	baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch)
-	if err != nil {
+	if err := forkRepo.GetBaseRepo(ctx); err != nil {
 		return nil, err
 	}
 
-	info := &UpstreamDivergingInfo{}
-	if forkBranch.CommitID == baseBranch.CommitID {
-		return info, nil
-	}
-
-	// if the fork repo has new commits, this call will fail because they are not in the base repo
-	// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
-	// so at the moment, we first check the update time, then check whether the fork branch has base's head
-	diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID)
-	if err != nil {
-		info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix
-		if info.BaseHasNewCommits {
-			return info, nil
-		}
-
-		// if the base's update time is before the fork, check whether the base's head is in the fork
-		baseGitRepo, baseGitRepoCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.BaseRepo)
-		if err != nil {
-			return nil, err
-		}
-		defer baseGitRepoCloser.Close()
-
-		headGitRepo, headGitRepoCloser, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
-		if err != nil {
-			return nil, err
-		}
-		defer headGitRepoCloser.Close()
-
-		baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID)
-		if err != nil {
-			return nil, err
-		}
-		headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID)
-		if err != nil {
-			return nil, err
-		}
-		hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID)
-		info.BaseHasNewCommits = !hasPreviousCommit
-		return info, nil
-	}
-
-	info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead
-	return info, nil
+	return GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
 }
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index a1f37b8c05d51..8d6e55959f588 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -1,10 +1,10 @@
-{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}}
+{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.HeadCommitsBehind)}}
 <div class="ui message flex-text-block">
 	<div class="tw-flex-1">
 		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
 		{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
-		{{if .UpstreamDivergingInfo.CommitsBehind}}
-			{{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}}
+		{{if .UpstreamDivergingInfo.HeadCommitsBehind}}
+			{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadCommitsBehind $upstreamHtml}}
 		{{else}}
 			{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
 		{{end}}
diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go
index 2b4c417334bb7..f9cf13112a652 100644
--- a/tests/integration/repo_branch_test.go
+++ b/tests/integration/repo_branch_test.go
@@ -11,13 +11,8 @@ import (
 	"strings"
 	"testing"
 
-	auth_model "code.gitea.io/gitea/models/auth"
-	org_model "code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/models/unittest"
-	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/tests"
@@ -142,19 +137,51 @@ func TestCreateBranchInvalidCSRF(t *testing.T) {
 	assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
 }
 
-func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) {
-	baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch)
-
+func prepareRecentlyPushedBranchTest(t *testing.T, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name
+	headRepoPath := headRepo.OwnerName + "/" + headRepo.Name
+	// Case 1: Normal branch changeset to display pushed message
 	// create branch with no new commit
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther)
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit", http.StatusSeeOther)
 
 	// create branch with commit
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther)
-	testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit")
+	testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "new-commit", fmt.Sprintf("new-file-%s.txt", headRepo.Name), "new-commit")
+
+	// create a branch then delete it
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
+	testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "deleted-branch")
+
+	// only `new-commit` branch has commits ahead the base branch
+	checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{"new-commit"})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{fmt.Sprintf("%v:new-commit", headRepo.FullName())})
+	}
+
+	// Case 2: Create PR so that `new-commit` branch will not show
+	testCreatePullToDefaultBranch(t, headSession, baseRepo, headRepo, "new-commit", "merge new-commit to default branch")
+	// No push message show because of active PR
+	checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{})
+	}
+}
+
+func prepareRecentlyPushedBranchSpecialTest(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name
+	headRepoPath := headRepo.OwnerName + "/" + headRepo.Name
+	// create branch with no new commit
+	testCreateBranch(t, session, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit-special", http.StatusSeeOther)
+
+	// update base (default) branch before head branch is updated
+	testAPINewFile(t, session, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, fmt.Sprintf("new-file-special-%s.txt", headRepo.Name), "new-commit")
 
-	// create deleted branch
-	testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther)
-	testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch")
+	// Though we have new `no-commit` branch, but the headBranch is not newer or commits ahead baseBranch. No message show.
+	checkRecentlyPushedNewBranches(t, session, headRepoPath, []string{})
+	if baseRepo.RepoPath() != headRepo.RepoPath() {
+		checkRecentlyPushedNewBranches(t, session, baseRepoPath, []string{})
+	}
 }
 
 func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string {
@@ -169,6 +196,9 @@ func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo,
 }
 
 func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) {
+	refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch)
+	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "new-commit", http.StatusSeeOther)
+
 	// create opening PR
 	testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther)
 	testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr")
@@ -210,65 +240,19 @@ func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath
 
 func TestRecentlyPushedNewBranches(t *testing.T) {
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
-		user1Session := loginUser(t, "user1")
-		user2Session := loginUser(t, "user2")
 		user12Session := loginUser(t, "user12")
-		user13Session := loginUser(t, "user13")
 
-		// prepare branch and PRs in original repo
+		// Same reposioty check
 		repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
-		prepareBranch(t, user12Session, repo10)
 		prepareRepoPR(t, user12Session, user12Session, repo10, repo10)
-
-		// outdated new branch should not be displayed
-		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"})
+		prepareRecentlyPushedBranchTest(t, user12Session, repo10, repo10)
+		prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, repo10)
 
 		// create a fork repo in public org
-		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit")
+		testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", repo10.DefaultBranch)
 		orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"})
 		prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo)
-
-		// user12 is the owner of the repo10 and the organization org25
-		// in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch
-		checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"})
-
-		userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
-		testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository)
-		t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite))
-		prepareBranch(t, user13Session, userForkRepo)
-		prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo)
-
-		// create branch with same name in different repo by user13
-		testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
-		testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther)
-		testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr")
-
-		// user13 pushed 2 branches with the same name in repo10 and repo11
-		// and repo11's branch has a pr, but repo10's branch doesn't
-		// in this case, we should get repo10's branch but not repo11's branch
-		checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"})
-
-		// create a fork repo in private org
-		testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit")
-		orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"})
-		prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo)
-
-		// user1 is the owner of private_org35 and no write permission to repo10
-		// so user1 can only see the branch in org35_fork_repo10
-		checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"})
-
-		// user2 push a branch in private_org35
-		testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther)
-		// convert write permission to read permission for code unit
-		token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization)
-		req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{
-			Name:     "team24",
-			UnitsMap: map[string]string{"repo.code": "read"},
-		}).AddTokenAuth(token)
-		MakeRequest(t, req, http.StatusOK)
-		teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode})
-		assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode)
-		// user2 can see the branch as it is created by user2
-		checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"})
+		prepareRecentlyPushedBranchTest(t, user12Session, repo10, orgPublicForkRepo)
+		prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, orgPublicForkRepo)
 	})
 }

From e72d001708068e63c84fda4793eac73cdeafa25e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 19 Jan 2025 21:02:33 +0800
Subject: [PATCH 076/206] Fix Account linking page (#33325) (#33327)

Backport #33325 by CrimsonEdgeHope

Fix password form missing whilst linking account even with
`ENABLE_PASSWORD_SIGNIN_FORM = true`.

Remove redundant empty box in account linking sign up page when
`LinkAccountMode` is true.

Co-authored-by: CrimsonEdgeHope <92579614+CrimsonEdgeHope@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/auth/linkaccount.go       | 11 +++++++++++
 templates/user/auth/link_account.tmpl |  7 ++++++-
 templates/user/auth/signin.tmpl       |  1 +
 templates/user/auth/signup_inner.tmpl |  4 ++--
 tests/integration/signin_test.go      | 20 ++++++++++++++++++++
 5 files changed, 40 insertions(+), 3 deletions(-)

diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 519431d92b7a1..ba61a1ad1a2f3 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -29,6 +29,7 @@ var tplLinkAccount base.TplName = "user/auth/link_account"
 
 // LinkAccount shows the page where the user can decide to login or create a new account
 func LinkAccount(ctx *context.Context) {
+	// FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again.
 	ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
 	ctx.Data["Title"] = ctx.Tr("link_account")
 	ctx.Data["LinkAccountMode"] = true
@@ -43,6 +44,7 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
 	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
@@ -50,6 +52,11 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
 
 	gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
+
+	// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
+	// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
+	// gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
+
 	if !ok {
 		// no account in session, so just redirect to the login page, then the user could restart the process
 		ctx.Redirect(setting.AppSubURL + "/user/login")
@@ -135,6 +142,8 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
@@ -223,6 +232,8 @@ func LinkAccountPostRegister(ctx *context.Context) {
 	ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
 	ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index a99e172d05054..d244ce38c2e1a 100644
--- a/templates/user/auth/link_account.tmpl
+++ b/templates/user/auth/link_account.tmpl
@@ -16,13 +16,18 @@
 		</div>
 	</overflow-menu>
 	<div class="ui middle very relaxed page grid">
-		<div class="column">
+		<div class="column tw-my-5">
+			{{/* these styles are quite tricky but it needs to be the same as the signin page */}}
 			<div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
+				<div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 				{{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
 				{{template "user/auth/signup_inner" .}}
+				</div>
 			</div>
 			<div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
+				<div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 				{{template "user/auth/signin_inner" .}}
+				</div>
 			</div>
 		</div>
 	</div>
diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl
index 54cc82d49df73..75e1bb27f9377 100644
--- a/templates/user/auth/signin.tmpl
+++ b/templates/user/auth/signin.tmpl
@@ -1,6 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user signin{{if .LinkAccountMode}} icon{{end}}">
 	<div class="ui middle very relaxed page grid">
+		{{/* these styles are quite tricky and should also apply to the signup and link_account pages */}}
 		<div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
 			{{template "user/auth/signin_inner" .}}
 		</div>
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl
index b3b2a4205e1ce..d66568199dd8d 100644
--- a/templates/user/auth/signup_inner.tmpl
+++ b/templates/user/auth/signup_inner.tmpl
@@ -59,12 +59,12 @@
 </div>
 
 <div class="ui container fluid">
+	{{if not .LinkAccountMode}}
 	<div class="ui attached segment header top tw-flex tw-flex-col tw-items-center">
-		{{if not .LinkAccountMode}}
 		<div class="field">
 			<span>{{ctx.Locale.Tr "auth.already_have_account"}}</span>
 			<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.sign_in_now"}}</a>
 		</div>
-		{{end}}
 	</div>
+	{{end}}
 </div>
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index abad9eb5e55f1..7282a11b35e7c 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -14,8 +14,11 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/markbates/goth"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -96,6 +99,11 @@ func TestSigninWithRememberMe(t *testing.T) {
 func TestEnablePasswordSignInForm(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
+	mockLinkAccount := func(ctx *context.Context) {
+		gothUser := goth.User{Email: "invalid-email", Name: "."}
+		_ = ctx.Session.Set("linkAccountGothUser", gothUser)
+	}
+
 	t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 		defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)()
@@ -106,6 +114,12 @@ func TestEnablePasswordSignInForm(t *testing.T) {
 
 		req = NewRequest(t, "POST", "/user/login")
 		MakeRequest(t, req, http.StatusForbidden)
+
+		req = NewRequest(t, "GET", "/user/link_account")
+		defer web.RouteMockReset()
+		web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
+		resp = MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", false)
 	})
 
 	t.Run("EnablePasswordSignInForm=true", func(t *testing.T) {
@@ -118,5 +132,11 @@ func TestEnablePasswordSignInForm(t *testing.T) {
 
 		req = NewRequest(t, "POST", "/user/login")
 		MakeRequest(t, req, http.StatusOK)
+
+		req = NewRequest(t, "GET", "/user/link_account")
+		defer web.RouteMockReset()
+		web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount)
+		resp = MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true)
 	})
 }

From 8f45a11919ca8abfa7fc0cc2ec8945d6746bba5a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 20 Jan 2025 15:50:38 +0800
Subject: [PATCH 077/206] Improve sync fork behavior (#33319) (#33332)

Backport #33319
Fix #33271

The only conflict is `reqctx` in
`services/repository/merge_upstream.go`, which could keep using
`context.Context` in 1.23
---
 models/git/branch.go                          |  3 ++
 options/locale/locale_en-US.ini               |  2 +-
 services/repository/branch.go                 | 17 ++++++-
 services/repository/merge_upstream.go         | 45 ++++++++++++++--
 .../repo/code/upstream_diverging_info.tmpl    | 14 ++---
 tests/integration/repo_merge_upstream_test.go | 51 +++++++++++++++----
 6 files changed, 108 insertions(+), 24 deletions(-)

diff --git a/models/git/branch.go b/models/git/branch.go
index a264f555d9dd4..d1caa35947b40 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 			BranchName: branchName,
 		}
 	}
+	// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
+	// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
+	// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
 	return &branch, nil
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 8c18395e17bea..26c617f7bbc54 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1951,7 +1951,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[
 pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s
 pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes
 pulls.upstream_diverging_merge = Sync fork
-pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s?
+pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"?
 
 pull.deleted_branch = (deleted):%s
 pull.agit_documentation = Review documentation about AGit
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 4f3accbe62341..ee06d89cf2371 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -638,9 +638,12 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 }
 
 // BranchDivergingInfo contains the information about the divergence of a head branch to the base branch.
-// This struct is also used in templates, so it needs to search for all references before changing it.
 type BranchDivergingInfo struct {
+	// whether the base branch contains new commits which are not in the head branch
 	BaseHasNewCommits bool
+
+	// behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate.
+	// there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate).
 	HeadCommitsBehind int
 	HeadCommitsAhead  int
 }
@@ -651,11 +654,20 @@ func GetBranchDivergingInfo(ctx context.Context, baseRepo *repo_model.Repository
 	if err != nil {
 		return nil, err
 	}
-
+	if headGitBranch.IsDeleted {
+		return nil, git_model.ErrBranchNotExist{
+			BranchName: headBranch,
+		}
+	}
 	baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch)
 	if err != nil {
 		return nil, err
 	}
+	if baseGitBranch.IsDeleted {
+		return nil, git_model.ErrBranchNotExist{
+			BranchName: baseBranch,
+		}
+	}
 
 	info := &BranchDivergingInfo{}
 	if headGitBranch.CommitID == baseGitBranch.CommitID {
@@ -692,5 +704,6 @@ func GetBranchDivergingInfo(ctx context.Context, baseRepo *repo_model.Repository
 	}
 
 	info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead
+	info.BaseHasNewCommits = info.HeadCommitsBehind > 0
 	return info, nil
 }
diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go
index 91bd01b3715c9..655d74611fa2f 100644
--- a/services/repository/merge_upstream.go
+++ b/services/repository/merge_upstream.go
@@ -5,6 +5,7 @@ package repository
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	issue_model "code.gitea.io/gitea/models/issues"
@@ -24,9 +25,17 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 	if err = repo.GetBaseRepo(ctx); err != nil {
 		return "", err
 	}
+	divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch)
+	if err != nil {
+		return "", err
+	}
+	if !divergingInfo.BaseBranchHasNewCommits {
+		return "up-to-date", nil
+	}
+
 	err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
 		Remote: repo.RepoPath(),
-		Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch),
+		Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch),
 		Env:    repo_module.PushingEnvironment(doer, repo),
 	})
 	if err == nil {
@@ -58,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 		BaseRepoID: repo.BaseRepo.ID,
 		BaseRepo:   repo.BaseRepo,
 		HeadBranch: branch, // maybe HeadCommitID is not needed
-		BaseBranch: repo.BaseRepo.DefaultBranch,
+		BaseBranch: divergingInfo.BaseBranchName,
 	}
 	fakeIssue.PullRequest = fakePR
 	err = pull.Update(ctx, fakePR, doer, "merge upstream", false)
@@ -68,8 +77,15 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.
 	return "merge", nil
 }
 
+// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it.
+type UpstreamDivergingInfo struct {
+	BaseBranchName          string
+	BaseBranchHasNewCommits bool
+	HeadBranchCommitsBehind int
+}
+
 // GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch.
-func GetUpstreamDivergingInfo(ctx context.Context, forkRepo *repo_model.Repository, forkBranch string) (*BranchDivergingInfo, error) {
+func GetUpstreamDivergingInfo(ctx context.Context, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) {
 	if !forkRepo.IsFork {
 		return nil, util.NewInvalidArgumentErrorf("repo is not a fork")
 	}
@@ -82,5 +98,26 @@ func GetUpstreamDivergingInfo(ctx context.Context, forkRepo *repo_model.Reposito
 		return nil, err
 	}
 
-	return GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
+	// Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo:
+	// * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a`
+	// * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a`
+	info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch)
+	if err == nil {
+		return &UpstreamDivergingInfo{
+			BaseBranchName:          forkBranch,
+			BaseBranchHasNewCommits: info.BaseHasNewCommits,
+			HeadBranchCommitsBehind: info.HeadCommitsBehind,
+		}, nil
+	}
+	if errors.Is(err, util.ErrNotExist) {
+		info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch)
+		if err == nil {
+			return &UpstreamDivergingInfo{
+				BaseBranchName:          forkRepo.BaseRepo.DefaultBranch,
+				BaseBranchHasNewCommits: info.BaseHasNewCommits,
+				HeadBranchCommitsBehind: info.HeadCommitsBehind,
+			}, nil
+		}
+	}
+	return nil, err
 }
diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl
index 8d6e55959f588..b3d35c05e5149 100644
--- a/templates/repo/code/upstream_diverging_info.tmpl
+++ b/templates/repo/code/upstream_diverging_info.tmpl
@@ -1,10 +1,12 @@
-{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.HeadCommitsBehind)}}
+{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}}
 <div class="ui message flex-text-block">
 	<div class="tw-flex-1">
-		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}}
-		{{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}}
-		{{if .UpstreamDivergingInfo.HeadCommitsBehind}}
-			{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadCommitsBehind $upstreamHtml}}
+		{{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}}
+		{{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}}
+		{{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}}
+		{{$upstreamHtml := HTMLFormat `<a href="%s">%s</a>` $upstreamLink $upstreamRepoBranchDisplay}}
+		{{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}}
+			{{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}}
 		{{else}}
 			{{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}}
 		{{end}}
@@ -12,7 +14,7 @@
 	{{if .CanWriteCode}}
 	<button class="ui compact primary button tw-m-0 link-action"
 					data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}"
-					data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}"
+					data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" $upstreamRepoBranchDisplay $thisRepoBranchDisplay}}"
 					data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}">
 		{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}
 	</button>
diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go
index e3e423c51d5d2..e928b04e9b358 100644
--- a/tests/integration/repo_merge_upstream_test.go
+++ b/tests/integration/repo_merge_upstream_test.go
@@ -60,25 +60,54 @@ func TestRepoMergeUpstream(t *testing.T) {
 
 		t.Run("HeadBeforeBase", func(t *testing.T) {
 			// add a file in base repo
+			sessionBaseUser := loginUser(t, baseUser.Name)
 			require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1"))
 
-			// the repo shows a prompt to "sync fork"
 			var mergeUpstreamLink string
-			require.Eventually(t, func() bool {
-				resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
-				htmlDoc := NewHTMLParser(t, resp.Body)
-				mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
-				if mergeUpstreamLink == "" {
-					return false
-				}
-				respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
-				return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
-			}, 5*time.Second, 100*time.Millisecond)
+			t.Run("DetectDefaultBranch", func(t *testing.T) {
+				// the repo shows a prompt to "sync fork" (defaults to the default branch)
+				require.Eventually(t, func() bool {
+					resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+					htmlDoc := NewHTMLParser(t, resp.Body)
+					mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
+					if mergeUpstreamLink == "" {
+						return false
+					}
+					respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+					return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`)
+				}, 5*time.Second, 100*time.Millisecond)
+			})
+
+			t.Run("DetectSameBranch", func(t *testing.T) {
+				// if the fork-branch name also exists in the base repo, then use that branch instead
+				req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{
+					"_csrf":           GetUserCSRFToken(t, sessionBaseUser),
+					"new_branch_name": "fork-branch",
+				})
+				sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther)
+
+				require.Eventually(t, func() bool {
+					resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK)
+					htmlDoc := NewHTMLParser(t, resp.Body)
+					mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc)
+					if mergeUpstreamLink == "" {
+						return false
+					}
+					respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html()
+					return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`)
+				}, 5*time.Second, 100*time.Millisecond)
+			})
 
 			// click the "sync fork" button
 			req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)})
 			session.MakeRequest(t, req, http.StatusOK)
 			checkFileContent("fork-branch", "test-content-1")
+
+			// delete the "fork-branch" from the base repo
+			req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{
+				"_csrf": GetUserCSRFToken(t, sessionBaseUser),
+			})
+			sessionBaseUser.MakeRequest(t, req, http.StatusOK)
 		})
 
 		t.Run("BaseChangeAfterHeadChange", func(t *testing.T) {

From eef635523a8890ec6fb80eb2a67968d439f440ba Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 21 Jan 2025 06:49:58 +0800
Subject: [PATCH 078/206] Make tracked time representation display as hours
 (#33315) (#33334)

Try to backport #33315, the only trivial conflict is in the helper
functions map in the helper.go

Fix #33333

Co-authored-by: Sysoev, Vladimir <i@vsysoev.ru>
---
 models/issues/stopwatch.go          |  4 +-
 modules/templates/helper.go         |  2 +-
 modules/util/sec_to_time.go         | 59 ++++-------------------------
 modules/util/sec_to_time_test.go    | 21 ++++------
 routers/web/repo/issue_timetrack.go |  2 +-
 services/convert/issue.go           |  3 +-
 services/convert/issue_comment.go   |  2 +-
 web_src/js/features/stopwatch.ts    | 27 ++++---------
 8 files changed, 30 insertions(+), 90 deletions(-)

diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go
index 629af95b5774f..baffe04ace811 100644
--- a/models/issues/stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -48,7 +48,7 @@ func (s Stopwatch) Seconds() int64 {
 
 // Duration returns a human-readable duration string based on local server time
 func (s Stopwatch) Duration() string {
-	return util.SecToTime(s.Seconds())
+	return util.SecToHours(s.Seconds())
 }
 
 func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
@@ -201,7 +201,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
 		Doer:    user,
 		Issue:   issue,
 		Repo:    issue.Repo,
-		Content: util.SecToTime(timediff),
+		Content: util.SecToHours(timediff),
 		Type:    CommentTypeStopTracking,
 		TimeID:  tt.ID,
 	}); err != nil {
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index ff9673ccef781..0b78defac92ae 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -70,7 +70,7 @@ func NewFuncMap() template.FuncMap {
 		// time / number / format
 		"FileSize": base.FileSize,
 		"CountFmt": base.FormatNumberSI,
-		"Sec2Time": util.SecToTime,
+		"Sec2Time": util.SecToHours,
 
 		"TimeEstimateString": timeEstimateString,
 
diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go
index ad0fb1a68b4a7..73667d723ef76 100644
--- a/modules/util/sec_to_time.go
+++ b/modules/util/sec_to_time.go
@@ -8,59 +8,17 @@ import (
 	"strings"
 )
 
-// SecToTime converts an amount of seconds to a human-readable string. E.g.
-// 66s			-> 1 minute 6 seconds
-// 52410s		-> 14 hours 33 minutes
-// 563418		-> 6 days 12 hours
-// 1563418		-> 2 weeks 4 days
-// 3937125s     -> 1 month 2 weeks
-// 45677465s	-> 1 year 6 months
-func SecToTime(durationVal any) string {
+// SecToHours converts an amount of seconds to a human-readable hours string.
+// This is stable for planning and managing timesheets.
+// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
+func SecToHours(durationVal any) string {
 	duration, _ := ToInt64(durationVal)
-
-	formattedTime := ""
-
-	// The following four variables are calculated by taking
-	// into account the previously calculated variables, this avoids
-	// pitfalls when using remainders. As that could lead to incorrect
-	// results when the calculated number equals the quotient number.
-	remainingDays := duration / (60 * 60 * 24)
-	years := remainingDays / 365
-	remainingDays -= years * 365
-	months := remainingDays * 12 / 365
-	remainingDays -= months * 365 / 12
-	weeks := remainingDays / 7
-	remainingDays -= weeks * 7
-	days := remainingDays
-
-	// The following three variables are calculated without depending
-	// on the previous calculated variables.
-	hours := (duration / 3600) % 24
+	hours := duration / 3600
 	minutes := (duration / 60) % 60
-	seconds := duration % 60
 
-	// Extract only the relevant information of the time
-	// If the time is greater than a year, it makes no sense to display seconds.
-	switch {
-	case years > 0:
-		formattedTime = formatTime(years, "year", formattedTime)
-		formattedTime = formatTime(months, "month", formattedTime)
-	case months > 0:
-		formattedTime = formatTime(months, "month", formattedTime)
-		formattedTime = formatTime(weeks, "week", formattedTime)
-	case weeks > 0:
-		formattedTime = formatTime(weeks, "week", formattedTime)
-		formattedTime = formatTime(days, "day", formattedTime)
-	case days > 0:
-		formattedTime = formatTime(days, "day", formattedTime)
-		formattedTime = formatTime(hours, "hour", formattedTime)
-	case hours > 0:
-		formattedTime = formatTime(hours, "hour", formattedTime)
-		formattedTime = formatTime(minutes, "minute", formattedTime)
-	default:
-		formattedTime = formatTime(minutes, "minute", formattedTime)
-		formattedTime = formatTime(seconds, "second", formattedTime)
-	}
+	formattedTime := ""
+	formattedTime = formatTime(hours, "hour", formattedTime)
+	formattedTime = formatTime(minutes, "minute", formattedTime)
 
 	// The formatTime() function always appends a space at the end. This will be trimmed
 	return strings.TrimRight(formattedTime, " ")
@@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string {
 	} else if value > 1 {
 		formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name)
 	}
-
 	return formattedTime
 }
diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go
index 4d1213a52c05e..71a8801d4f4aa 100644
--- a/modules/util/sec_to_time_test.go
+++ b/modules/util/sec_to_time_test.go
@@ -9,22 +9,17 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestSecToTime(t *testing.T) {
+func TestSecToHours(t *testing.T) {
 	second := int64(1)
 	minute := 60 * second
 	hour := 60 * minute
 	day := 24 * hour
-	year := 365 * day
 
-	assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second))
-	assert.Equal(t, "1 hour", SecToTime(hour))
-	assert.Equal(t, "1 hour", SecToTime(hour+second))
-	assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second))
-	assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second))
-	assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second))
-	assert.Equal(t, "4 weeks", SecToTime(4*7*day))
-	assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day))
-	assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second))
-	assert.Equal(t, "11 months", SecToTime(year-25*day))
-	assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second))
+	assert.Equal(t, "1 minute", SecToHours(minute+6*second))
+	assert.Equal(t, "1 hour", SecToHours(hour))
+	assert.Equal(t, "1 hour", SecToHours(hour+second))
+	assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second))
+	assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second))
+	assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
+	assert.Equal(t, "672 hours", SecToHours(4*7*day))
 }
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 88c539488e56e..11ffa8bacf5a0 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) {
 		return
 	}
 
-	c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
+	c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time)))
 	c.JSONRedirect("")
 }
 
diff --git a/services/convert/issue.go b/services/convert/issue.go
index e3124efd6402e..37935accca269 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 )
 
 func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
@@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop
 		result = append(result, api.StopWatch{
 			Created:       sw.CreatedUnix.AsTime(),
 			Seconds:       sw.Seconds(),
-			Duration:      sw.Duration(),
+			Duration:      util.SecToHours(sw.Seconds()),
 			IssueIndex:    issue.Index,
 			IssueTitle:    issue.Title,
 			RepoOwnerName: repo.OwnerName,
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index b8527ae233cc0..9ad584a62f759 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
 			c.Content[0] == '|' {
 			// TimeTracking Comments from v1.21 on store the seconds instead of an formatted string
 			// so we check for the "|" delimiter and convert new to legacy format on demand
-			c.Content = util.SecToTime(c.Content[1:])
+			c.Content = util.SecToHours(c.Content[1:])
 		}
 
 		if c.Type == issues_model.CommentTypeChangeTimeEstimate {
diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts
index af52be4e24e8a..46168b2cd759f 100644
--- a/web_src/js/features/stopwatch.ts
+++ b/web_src/js/features/stopwatch.ts
@@ -1,6 +1,6 @@
 import {createTippy} from '../modules/tippy.ts';
 import {GET} from '../modules/fetch.ts';
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, queryElems, showElem} from '../utils/dom.ts';
 import {logoutFromWorker} from '../modules/worker.ts';
 
 const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config;
@@ -144,23 +144,10 @@ function updateStopwatchData(data) {
   return Boolean(data.length);
 }
 
-// TODO: This flickers on page load, we could avoid this by making a custom
-// element to render time periods. Feeding a datetime in backend does not work
-// when time zone between server and client differs.
-function updateStopwatchTime(seconds) {
-  if (!Number.isFinite(seconds)) return;
-  const datetime = (new Date(Date.now() - seconds * 1000)).toISOString();
-  for (const parent of document.querySelectorAll('.header-stopwatch-dot')) {
-    const existing = parent.querySelector(':scope > relative-time');
-    if (existing) {
-      existing.setAttribute('datetime', datetime);
-    } else {
-      const el = document.createElement('relative-time');
-      el.setAttribute('format', 'micro');
-      el.setAttribute('datetime', datetime);
-      el.setAttribute('lang', 'en-US');
-      el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip
-      parent.append(el);
-    }
-  }
+// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods.
+function updateStopwatchTime(seconds: number) {
+  const hours = seconds / 3600 || 0;
+  const minutes = seconds / 60 || 0;
+  const timeText = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`;
+  queryElems(document, '.header-stopwatch-dot', (el) => el.textContent = timeText);
 }

From 72837530bf9d4aa6f79de09f26fcd3d533bb93c3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 21 Jan 2025 19:18:22 +0800
Subject: [PATCH 079/206] Fix issue count (#33338) (#33341)

Backport #33338 by wxiaoguang

Fix #33336

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/user/home.go | 30 ++++++++++++++++++++----------
 1 file changed, 20 insertions(+), 10 deletions(-)

diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index befa33b0c0d66..5d6b99909b76f 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -578,17 +578,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 	// -------------------------------
 	// Fill stats to post to ctx.Data.
 	// -------------------------------
-	issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
+	issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
 		func(o *issue_indexer.SearchOptions) {
 			o.IsFuzzyKeyword = isFuzzy
-			// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
-			// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
-			// because the doer may create issues or be mentioned in any public repo.
-			// So we need search issues in all public repos.
-			o.AllPublic = ctx.Doer.ID == ctxUser.ID
-			o.MentionID = nil
-			o.ReviewRequestedID = nil
-			o.ReviewedID = nil
 		},
 	))
 	if err != nil {
@@ -777,10 +769,19 @@ func UsernameSubRoute(ctx *context.Context) {
 	}
 }
 
-func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
+func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
 	ret = &issues_model.IssueStats{}
 	doerID := ctx.Doer.ID
 
+	opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+		// If the doer is the same as the context user, which means the doer is viewing his own dashboard,
+		// it's not enough to show the repos that the doer owns or has been explicitly granted access to,
+		// because the doer may create issues or be mentioned in any public repo.
+		// So we need search issues in all public repos.
+		o.AllPublic = doerID == ctxUser.ID
+	})
+
+	// Open/Closed are for the tabs of the issue list
 	{
 		openClosedOpts := opts.Copy()
 		switch filterMode {
@@ -811,6 +812,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
 		}
 	}
 
+	// Below stats are for the left sidebar
+	opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+		o.AssigneeID = nil
+		o.PosterID = nil
+		o.MentionID = nil
+		o.ReviewRequestedID = nil
+		o.ReviewedID = nil
+	})
+
 	ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
 	if err != nil {
 		return nil, err

From 4ffa49aa044a7e9172d7362e2f984e8ae8c40229 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 21 Jan 2025 21:09:37 +0800
Subject: [PATCH 080/206] Make issue suggestion work for all editors (#33340)
 (#33342)

Backport #33340

And do not handle special keys when the text-expander popup exists
---
 web_src/js/features/comp/EditorMarkdown.ts |  5 +++++
 web_src/js/features/comp/TextExpander.ts   | 11 ++++++++---
 web_src/js/types.ts                        |  5 +++++
 web_src/js/utils.test.ts                   | 15 +++++++++------
 web_src/js/utils.ts                        | 13 +++++++------
 5 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts
index d3ed492396fef..08306531f15f8 100644
--- a/web_src/js/features/comp/EditorMarkdown.ts
+++ b/web_src/js/features/comp/EditorMarkdown.ts
@@ -184,8 +184,13 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
   triggerEditorContentChanged(textarea);
 }
 
+function isTextExpanderShown(textarea: HTMLElement): boolean {
+  return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions'));
+}
+
 export function initTextareaMarkdown(textarea) {
   textarea.addEventListener('keydown', (e) => {
+    if (isTextExpanderShown(textarea)) return;
     if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
       // use Tab/Shift-Tab to indent/unindent the selected lines
       handleIndentSelection(textarea, e);
diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts
index e0c4abed75b80..bad8d2e59d076 100644
--- a/web_src/js/features/comp/TextExpander.ts
+++ b/web_src/js/features/comp/TextExpander.ts
@@ -1,14 +1,19 @@
 import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
 import {emojiString} from '../emoji.ts';
 import {svg} from '../../svg.ts';
-import {parseIssueHref, parseIssueNewHref} from '../../utils.ts';
+import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts';
 import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
 import {getIssueColor, getIssueIcon} from '../issue.ts';
 import {debounce} from 'perfect-debounce';
 
 const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
-  let issuePathInfo = parseIssueHref(window.location.href);
-  if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(window.location.href);
+  const issuePathInfo = parseIssueHref(window.location.href);
+  if (!issuePathInfo.ownerName) {
+    const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
+    issuePathInfo.ownerName = repoOwnerPathInfo.ownerName;
+    issuePathInfo.repoName = repoOwnerPathInfo.repoName;
+    // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
+  }
   if (!issuePathInfo.ownerName) return resolve({matched: false});
 
   const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
diff --git a/web_src/js/types.ts b/web_src/js/types.ts
index e7c9ac0df42f6..e972994928f6e 100644
--- a/web_src/js/types.ts
+++ b/web_src/js/types.ts
@@ -30,6 +30,11 @@ export type RequestOpts = {
   data?: RequestData,
 } & RequestInit;
 
+export type RepoOwnerPathInfo = {
+  ownerName: string,
+  repoName: string,
+}
+
 export type IssuePathInfo = {
   ownerName: string,
   repoName: string,
diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts
index bbe328f658df0..ccdbc2dbd798f 100644
--- a/web_src/js/utils.test.ts
+++ b/web_src/js/utils.test.ts
@@ -1,7 +1,7 @@
 import {
   basename, extname, isObject, stripTags, parseIssueHref,
   parseUrl, translateMonth, translateDay, blobToDataURI,
-  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref,
+  toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseRepoOwnerPathInfo,
 } from './utils.ts';
 
 test('basename', () => {
@@ -45,11 +45,14 @@ test('parseIssueHref', () => {
   expect(parseIssueHref('')).toEqual({ownerName: undefined, repoName: undefined, type: undefined, index: undefined});
 });
 
-test('parseIssueNewHref', () => {
-  expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'});
-  expect(parseIssueNewHref('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls'});
+test('parseRepoOwnerPathInfo', () => {
+  expect(parseRepoOwnerPathInfo('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/owner/repo/releases')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/other')).toEqual({});
+  window.config.appSubUrl = '/sub';
+  expect(parseRepoOwnerPathInfo('/sub/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  expect(parseRepoOwnerPathInfo('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo'});
+  window.config.appSubUrl = '';
 });
 
 test('parseUrl', () => {
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index 2a2bdc60f9577..86bdd3790e0d5 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -1,5 +1,5 @@
 import {decode, encode} from 'uint8-to-base64';
-import type {IssuePageInfo, IssuePathInfo} from './types.ts';
+import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts';
 
 // transform /path/to/file.ext to file.ext
 export function basename(path: string): string {
@@ -32,16 +32,17 @@ export function stripTags(text: string): string {
 }
 
 export function parseIssueHref(href: string): IssuePathInfo {
+  // FIXME: it should use pathname and trim the appSubUrl ahead
   const path = (href || '').replace(/[#?].*$/, '');
   const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
   return {ownerName, repoName, pathType, indexString};
 }
 
-export function parseIssueNewHref(href: string): IssuePathInfo {
-  const path = (href || '').replace(/[#?].*$/, '');
-  const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || [];
-  const pathType = pathTypeField ? (pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls') : undefined;
-  return {ownerName, repoName, pathType};
+export function parseRepoOwnerPathInfo(pathname: string): RepoOwnerPathInfo {
+  const appSubUrl = window.config.appSubUrl;
+  if (appSubUrl && pathname.startsWith(appSubUrl)) pathname = pathname.substring(appSubUrl.length);
+  const [_, ownerName, repoName] = /([^/]+)\/([^/]+)/.exec(pathname) || [];
+  return {ownerName, repoName};
 }
 
 export function parseIssuePageInfo(): IssuePageInfo {

From e08eed90401a06e6640a9cdda39bdcd27f12dde3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 22 Jan 2025 15:18:34 +0800
Subject: [PATCH 081/206] Fix code button alignment (#33345) (#33351)

Backport #33345 by silverwind

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/clone_panel.tmpl | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl
index d3496bdb73a04..19370837a6fd6 100644
--- a/templates/repo/clone_panel.tmpl
+++ b/templates/repo/clone_panel.tmpl
@@ -1,5 +1,6 @@
 <button class="ui primary button js-btn-clone-panel">
-	<span>{{svg "octicon-code" 16}} Code</span>
+	{{svg "octicon-code" 16}}
+	<span>Code</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 </button>
 <div class="clone-panel-popup tippy-target">

From 987d0144684e8df52ab112ae06de1daec0b183f1 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 22 Jan 2025 17:37:47 +0100
Subject: [PATCH 082/206] Update go tool dependencies (#32916) (#33355)

Clean cherry-pick of https://github.com/go-gitea/gitea/pull/32916.
Update all go tool dependencies to latest version.
---
 Makefile                                  | 8 ++++----
 modules/markup/common/linkify.go          | 2 +-
 modules/markup/html.go                    | 2 +-
 modules/markup/internal/renderinternal.go | 2 +-
 modules/markup/markdown/goldmark.go       | 2 +-
 modules/util/time_str.go                  | 2 +-
 modules/util/util_test.go                 | 4 ++--
 routers/web/repo/pull.go                  | 3 ---
 8 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/Makefile b/Makefile
index 978d546eeea5d..5524cfb08cdf7 100644
--- a/Makefile
+++ b/Makefile
@@ -26,17 +26,17 @@ COMMA := ,
 XGO_VERSION := go-1.23.x
 
 AIR_PACKAGE ?= github.com/air-verse/air@v1
-EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
+EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.0.3
 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
-GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
-MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
+GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
+MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
-GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.15.3
+GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.0
 
 DOCKER_IMAGE ?= gitea/gitea
 DOCKER_TAG ?= latest
diff --git a/modules/markup/common/linkify.go b/modules/markup/common/linkify.go
index be6ab22b55f49..52888958faf66 100644
--- a/modules/markup/common/linkify.go
+++ b/modules/markup/common/linkify.go
@@ -24,7 +24,7 @@ type GlobalVarsType struct {
 	LinkRegex   *regexp.Regexp // fast matching a URL link, no any extra validation.
 }
 
-var GlobalVars = sync.OnceValue[*GlobalVarsType](func() *GlobalVarsType {
+var GlobalVars = sync.OnceValue(func() *GlobalVarsType {
 	v := &GlobalVarsType{}
 	v.wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`)
 	v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://")
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 25628db39e4c5..bb12febf277ec 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -42,7 +42,7 @@ type globalVarsType struct {
 	nulCleaner *strings.Replacer
 }
 
-var globalVars = sync.OnceValue[*globalVarsType](func() *globalVarsType {
+var globalVars = sync.OnceValue(func() *globalVarsType {
 	v := &globalVarsType{}
 	// NOTE: All below regex matching do not perform any extra validation.
 	// Thus a link is produced even if the linked entity does not exist.
diff --git a/modules/markup/internal/renderinternal.go b/modules/markup/internal/renderinternal.go
index 4775fecfc700c..4d58f160a9288 100644
--- a/modules/markup/internal/renderinternal.go
+++ b/modules/markup/internal/renderinternal.go
@@ -17,7 +17,7 @@ import (
 	"golang.org/x/net/html"
 )
 
-var reAttrClass = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
+var reAttrClass = sync.OnceValue(func() *regexp.Regexp {
 	// TODO: it isn't a problem at the moment because our HTML contents are always well constructed
 	return regexp.MustCompile(`(<[^>]+)\s+class="([^"]+)"([^>]*>)`)
 })
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 620a39ebfdec9..69c2a96ff1fd7 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -112,7 +112,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 }
 
 // it is copied from old code, which is quite doubtful whether it is correct
-var reValidIconName = sync.OnceValue[*regexp.Regexp](func() *regexp.Regexp {
+var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
 	return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
 })
 
diff --git a/modules/util/time_str.go b/modules/util/time_str.go
index b56f4740af427..0fccfe82cc1cb 100644
--- a/modules/util/time_str.go
+++ b/modules/util/time_str.go
@@ -25,7 +25,7 @@ type timeStrGlobalVarsType struct {
 // In the future, it could be some configurable options to help users
 // to convert the working time to different units.
 
-var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType {
+var timeStrGlobalVars = sync.OnceValue(func() *timeStrGlobalVarsType {
 	v := &timeStrGlobalVarsType{}
 	v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`)
 	v.units = []struct {
diff --git a/modules/util/util_test.go b/modules/util/util_test.go
index 5abce08b41fc5..52b05acc5bc8d 100644
--- a/modules/util/util_test.go
+++ b/modules/util/util_test.go
@@ -242,10 +242,10 @@ func TestReserveLineBreakForTextarea(t *testing.T) {
 }
 
 func TestOptionalArg(t *testing.T) {
-	foo := func(other any, optArg ...int) int {
+	foo := func(_ any, optArg ...int) int {
 		return OptionalArg(optArg)
 	}
-	bar := func(other any, optArg ...int) int {
+	bar := func(_ any, optArg ...int) int {
 		return OptionalArg(optArg, 42)
 	}
 	assert.Equal(t, 0, foo(nil))
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 9694ae845b1b9..8fd23f835ad67 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -701,9 +701,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 
 	if ctx.Written() {
 		return
-	} else if prInfo == nil {
-		ctx.NotFound("ViewPullFiles", nil)
-		return
 	}
 
 	headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())

From a3c5358d35a618408814b0738eb27194468ebffe Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Thu, 23 Jan 2025 01:21:58 +0100
Subject: [PATCH 083/206] Update katex to latest version (#33361)

Partial backport of https://github.com/go-gitea/gitea/pull/33359.
Upgrade katex because there were a number of security problems fixed in
recent versions.
---
 package-lock.json | 8 ++++----
 package.json      | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 1f67b69a70f1a..008c022960a73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,7 +33,7 @@
         "htmx.org": "2.0.4",
         "idiomorph": "0.3.0",
         "jquery": "3.7.1",
-        "katex": "0.16.11",
+        "katex": "0.16.21",
         "license-checker-webpack-plugin": "0.2.1",
         "mermaid": "11.4.1",
         "mini-css-extract-plugin": "2.9.2",
@@ -11057,9 +11057,9 @@
       "license": "MIT"
     },
     "node_modules/katex": {
-      "version": "0.16.11",
-      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
-      "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==",
+      "version": "0.16.21",
+      "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
+      "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==",
       "funding": [
         "https://opencollective.com/katex",
         "https://github.com/sponsors/katex"
diff --git a/package.json b/package.json
index f318cbf5bba66..f39e5b5101c8d 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,7 @@
     "htmx.org": "2.0.4",
     "idiomorph": "0.3.0",
     "jquery": "3.7.1",
-    "katex": "0.16.11",
+    "katex": "0.16.21",
     "license-checker-webpack-plugin": "0.2.1",
     "mermaid": "11.4.1",
     "mini-css-extract-plugin": "2.9.2",

From 12347f07ae0d134479d17a79371e041933322452 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 25 Jan 2025 02:41:01 +0800
Subject: [PATCH 084/206] Repo homepage styling tweaks (#33289) (#33381)

Backport #33289 by @silverwind

Reduce it to a value that results in `.repo-home-sidebar-top` and
`.repo-home-sidebar-bottom` having 240px content width, the same as
GitHub.

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/home_sidebar_top.tmpl | 2 +-
 templates/repo/sub_menu.tmpl         | 2 +-
 web_src/css/repo/home.css            | 5 ++---
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl
index d7c2b8336f37a..8d3d7c6f86974 100644
--- a/templates/repo/home_sidebar_top.tmpl
+++ b/templates/repo/home_sidebar_top.tmpl
@@ -1,5 +1,5 @@
 <div class="repo-home-sidebar-top">
-	<form class="ignore-dirty tw-flex tw-flex-1 tw-mt-1" action="{{.RepoLink}}/search" method="get">
+	<form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
 		<div class="ui small action input tw-flex-1">
 			<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> {{template "shared/search/button"}}
 		</div>
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 1e83b53ef048f..b39db959aa73d 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -1,5 +1,5 @@
 {{if and (not .HideRepoInfo) (not .IsBlame)}}
-<div class="ui segments repository-summary tw-mt-1 tw-mb-0">
+<div class="ui segments repository-summary tw-my-0">
 	<div class="ui segment sub-menu repository-menu">
 		{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
 			<a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">
diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css
index 65005e2263537..96551979ea21a 100644
--- a/web_src/css/repo/home.css
+++ b/web_src/css/repo/home.css
@@ -1,7 +1,8 @@
 .repo-grid-filelist-sidebar {
   display: grid;
-  grid-template-columns: auto 300px;
+  grid-template-columns: auto 280px;
   grid-template-rows: auto auto 1fr;
+  gap: var(--page-spacing);
 }
 
 .repo-home-filelist {
@@ -13,13 +14,11 @@
 .repo-home-sidebar-top {
   grid-column: 2;
   grid-row: 1;
-  padding-left: 1em;
 }
 
 .repo-home-sidebar-bottom {
   grid-column: 2;
   grid-row: 2;
-  padding-left: 1em;
 }
 
 .repo-home-sidebar-bottom .flex-list > :first-child {

From 2196ba5e42be98670677fb5e9d1bc8bfa0b4ce37 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 27 Jan 2025 02:20:57 +0800
Subject: [PATCH 085/206] Clone button enhancements (#33362) (#33404)

Backport #33362 by @silverwind

- Add box-shadow to default tippy theme
- Make colors for tabs match the ones from `.ui.tabular.menu`
- Remove tippy arrow and slightly offset tooltip closer to the button
- Fix setting of `aria-haspopup` when default role is used with tippy

<img width="335" alt="image"
src="https://github.com/user-attachments/assets/8633ebac-a43f-457a-86bd-7a88a83519ee"
/>

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/modules/tippy.css      | 4 ++++
 web_src/css/repo.css               | 2 +-
 web_src/css/repo/clone.css         | 4 +++-
 web_src/js/features/repo-common.ts | 1 +
 web_src/js/modules/tippy.ts        | 5 +++--
 5 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
index 55b9751cc635a..4438a31c9d53b 100644
--- a/web_src/css/modules/tippy.css
+++ b/web_src/css/modules/tippy.css
@@ -28,6 +28,10 @@
   z-index: 1;
 }
 
+.tippy-box[data-theme="default"] {
+  box-shadow: 0 6px 18px var(--color-shadow);
+}
+
 /* bare theme, no styling at all, except box-shadow */
 .tippy-box[data-theme="bare"] {
   border: none;
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 7a39a725a50dd..cb75f7745a2d1 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1669,7 +1669,7 @@ td .commit-summary {
 }
 
 .repo-button-row-left {
-  flex: 1;
+  flex-grow: 1;
 }
 
 .repo-button-row .button {
diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css
index 3f6a1323fea26..c6887fbf161c4 100644
--- a/web_src/css/repo/clone.css
+++ b/web_src/css/repo/clone.css
@@ -20,10 +20,12 @@
 .clone-panel-tab .item {
   padding: 5px 10px;
   background: none;
+  color: var(--color-text-light-2);
 }
 
 .clone-panel-tab .item.active {
-  border-bottom: 3px solid var(--color-secondary);
+  color: var(--color-text-dark);
+  border-bottom: 3px solid currentcolor;
 }
 
 .clone-panel-tab + .divider {
diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts
index 90860720e4fc2..86e14585c7620 100644
--- a/web_src/js/features/repo-common.ts
+++ b/web_src/js/features/repo-common.ts
@@ -99,6 +99,7 @@ function initClonePanelButton(btn: HTMLButtonElement) {
     placement: 'bottom-end',
     interactive: true,
     hideOnClick: true,
+    arrow: false,
   });
 }
 
diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts
index 4e7f1ac093374..6eca6d9a86081 100644
--- a/web_src/js/modules/tippy.ts
+++ b/web_src/js/modules/tippy.ts
@@ -42,16 +42,17 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
       visibleInstances.add(instance);
       return onShow?.(instance);
     },
-    arrow: arrow || (theme === 'bare' ? false : arrowSvg),
+    arrow: arrow ?? (theme === 'bare' ? false : arrowSvg),
     // HTML role attribute, ideally the default role would be "popover" but it does not exist
     role: role || 'menu',
     // CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
     theme: theme || role || 'default',
+    offset: [0, arrow ? 10 : 6],
     plugins: [followCursor],
     ...other,
   } satisfies Partial<Props>);
 
-  if (role === 'menu') {
+  if (instance.props.role === 'menu') {
     target.setAttribute('aria-haspopup', 'true');
   }
 

From 826fffb59ed05c70742228fdeea71da6d450e157 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 27 Jan 2025 02:57:45 +0800
Subject: [PATCH 086/206] Add missed auto merge feed message on dashboard
 (#33309) (#33405)

Backport #33309 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/activities/action.go         | 4 ++--
 templates/user/dashboard/feeds.tmpl | 5 ++++-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index 65d95fbe6676e..4c71810c61777 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -72,9 +72,9 @@ func (at ActionType) String() string {
 	case ActionRenameRepo:
 		return "rename_repo"
 	case ActionStarRepo:
-		return "star_repo"
+		return "star_repo" // will not displayed in feeds.tmpl
 	case ActionWatchRepo:
-		return "watch_repo"
+		return "watch_repo" // will not displayed in feeds.tmpl
 	case ActionCommitRepo:
 		return "commit_repo"
 	case ActionCreateIssue:
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 1c1ba5566fcf5..739be586b856f 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -73,10 +73,13 @@
 					{{else if .GetOpType.InActions "publish_release"}}
 						{{$linkText := .Content | ctx.RenderUtils.RenderEmoji}}
 						{{ctx.Locale.Tr "action.publish_release" (.GetRepoLink ctx) (printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag) (.ShortRepoPath ctx) $linkText}}
-					{{else if .GetOpType.InActions "review_dismissed"}}
+					{{else if .GetOpType.InActions "pull_review_dismissed"}}
 						{{$index := index .GetIssueInfos 0}}
 						{{$reviewer := index .GetIssueInfos 1}}
 						{{ctx.Locale.Tr "action.review_dismissed" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx) $reviewer}}
+					{{else if .GetOpType.InActions "auto_merge_pull_request"}}
+						{{$index := index .GetIssueInfos 0}}
+						{{ctx.Locale.Tr "action.auto_merge_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}}
 					{{end}}
 					{{DateUtils.TimeSince .GetCreate}}
 				</div>

From 4578288ea3736083b194ffcc48199b288ce3e585 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 27 Jan 2025 10:50:00 +0800
Subject: [PATCH 087/206] Use ProtonMail/go-crypto to replace keybase/go-crypto
 (#33402) (#33410)

Backport #33402 by wxiaoguang

Fix #33400

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 assets/go-licenses.json                       |  5 ----
 go.mod                                        |  1 -
 go.sum                                        |  2 --
 models/asymkey/gpg_key.go                     | 12 ++++++---
 models/asymkey/gpg_key_add.go                 |  8 +++---
 models/asymkey/gpg_key_commit_verification.go |  2 +-
 models/asymkey/gpg_key_common.go              | 12 ++++-----
 models/asymkey/gpg_key_test.go                | 25 ++++++++++++++++++-
 routers/web/user/home.go                      |  4 +--
 services/packages/arch/repository.go          |  6 ++---
 services/packages/debian/repository.go        |  8 +++---
 tests/integration/user_test.go                |  9 +++----
 12 files changed, 55 insertions(+), 39 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 60e39b6694d02..6d1b2e16898d4 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -744,11 +744,6 @@
     "path": "github.com/kevinburke/ssh_config/LICENSE",
     "licenseText": "Copyright (c) 2017 Kevin Burke.\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n===================\n\nThe lexer and parser borrow heavily from github.com/pelletier/go-toml. The\nlicense for that project is copied below.\n\nThe MIT License (MIT)\n\nCopyright (c) 2013 - 2017 Thomas Pelletier, Eric Anderton\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
   },
-  {
-    "name": "github.com/keybase/go-crypto",
-    "path": "github.com/keybase/go-crypto/LICENSE",
-    "licenseText": "Copyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
-  },
   {
     "name": "github.com/klauspost/compress",
     "path": "github.com/klauspost/compress/LICENSE",
diff --git a/go.mod b/go.mod
index d8e7e37152804..422952af9e2db 100644
--- a/go.mod
+++ b/go.mod
@@ -79,7 +79,6 @@ require (
 	github.com/jhillyerd/enmime v1.3.0
 	github.com/json-iterator/go v1.1.12
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
-	github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
 	github.com/klauspost/compress v1.17.11
 	github.com/klauspost/cpuid/v2 v2.2.8
 	github.com/lib/pq v1.10.9
diff --git a/go.sum b/go.sum
index 968001e7f91ae..e7f6b7c6d9993 100644
--- a/go.sum
+++ b/go.sum
@@ -510,8 +510,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4=
-github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go
index 5236b2d4500e4..e921340730bd6 100644
--- a/models/asymkey/gpg_key.go
+++ b/models/asymkey/gpg_key.go
@@ -13,8 +13,8 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
 
-	"github.com/keybase/go-crypto/openpgp"
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 	"xorm.io/builder"
 )
 
@@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
 	// Parse Subkeys
 	subkeys := make([]*GPGKey, len(e.Subkeys))
 	for i, k := range e.Subkeys {
-		subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
+		subkeyExpiry := expiry
+		if k.Sig.KeyLifetimeSecs != nil {
+			subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second)
+		}
+		subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry)
 		if err != nil {
 			return nil, ErrGPGKeyParsing{ParseError: err}
 		}
@@ -156,7 +160,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
 
 	emails := make([]*user_model.EmailAddress, 0, len(e.Identities))
 	for _, ident := range e.Identities {
-		if ident.Revocation != nil {
+		if ident.Revoked(time.Now()) {
 			continue
 		}
 		email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go
index 11124b1366511..6c0f6e01a7cb5 100644
--- a/models/asymkey/gpg_key_add.go
+++ b/models/asymkey/gpg_key_add.go
@@ -10,7 +10,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 
-	"github.com/keybase/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp"
 )
 
 //   __________________  ________   ____  __.
@@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
 	verified := false
 	// Handle provided signature
 	if signature != "" {
-		signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature))
+		signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
 		if err != nil {
-			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature))
+			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
 		}
 		if err != nil {
-			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature))
+			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
 		}
 		if err != nil {
 			log.Error("Unable to validate token signature. Error: %v", err)
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 26fad3bb3f211..9219a509df0e0 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -16,7 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 )
 
 //   __________________  ________   ____  __.
diff --git a/models/asymkey/gpg_key_common.go b/models/asymkey/gpg_key_common.go
index 28cb8f4e766ee..92c34a2569ece 100644
--- a/models/asymkey/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -13,9 +13,9 @@ import (
 	"strings"
 	"time"
 
-	"github.com/keybase/go-crypto/openpgp"
-	"github.com/keybase/go-crypto/openpgp/armor"
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/armor"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 )
 
 //   __________________  ________   ____  __.
@@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
 	return pkey, nil
 }
 
-// getExpiryTime extract the expire time of primary key based on sig
+// getExpiryTime extract the expiry time of primary key based on sig
 func getExpiryTime(e *openpgp.Entity) time.Time {
 	expiry := time.Time{}
 	// Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
@@ -88,12 +88,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time {
 	for _, ident := range e.Identities {
 		if selfSig == nil {
 			selfSig = ident.SelfSignature
-		} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
+		} else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
 			selfSig = ident.SelfSignature
 			break
 		}
 	}
-	if selfSig.KeyLifetimeSecs != nil {
+	if selfSig != nil && selfSig.KeyLifetimeSecs != nil {
 		expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
 	}
 	return expiry
diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go
index d3fbb01d82b1d..de463dfbe2777 100644
--- a/models/asymkey/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -13,7 +13,8 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) {
 		IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
 	}))
 }
+
+func TestParseGPGKey(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	assert.NoError(t, db.Insert(db.DefaultContext, &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true}))
+
+	// create a key for test email
+	e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil)
+	assert.NoError(t, err)
+	k, err := parseGPGKey(db.DefaultContext, 1, e, true)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, k.KeyID)
+	assert.NotEmpty(t, k.Emails) // the key is valid, matches the email
+
+	// then revoke the key
+	for _, id := range e.Identities {
+		id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)})
+	}
+	k, err = parseGPGKey(db.DefaultContext, 1, e, true)
+	assert.NoError(t, err)
+	assert.NotEmpty(t, k.KeyID)
+	assert.Empty(t, k.Emails) // the key is revoked, matches no email
+}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 5d6b99909b76f..7552310cf2436 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -40,8 +40,8 @@ import (
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
 
-	"github.com/keybase/go-crypto/openpgp"
-	"github.com/keybase/go-crypto/openpgp/armor"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/armor"
 	"xorm.io/builder"
 )
 
diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go
index 6731d9a1ac728..7fb4222cf6177 100644
--- a/services/packages/arch/repository.go
+++ b/services/packages/arch/repository.go
@@ -26,9 +26,9 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 
-	"github.com/keybase/go-crypto/openpgp"
-	"github.com/keybase/go-crypto/openpgp/armor"
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/armor"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 )
 
 const (
diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go
index 13e98a820e50e..34b52b45cfdec 100644
--- a/services/packages/debian/repository.go
+++ b/services/packages/debian/repository.go
@@ -23,10 +23,10 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	packages_service "code.gitea.io/gitea/services/packages"
 
-	"github.com/keybase/go-crypto/openpgp"
-	"github.com/keybase/go-crypto/openpgp/armor"
-	"github.com/keybase/go-crypto/openpgp/clearsign"
-	"github.com/keybase/go-crypto/openpgp/packet"
+	"github.com/ProtonMail/go-crypto/openpgp"
+	"github.com/ProtonMail/go-crypto/openpgp/armor"
+	"github.com/ProtonMail/go-crypto/openpgp/clearsign"
+	"github.com/ProtonMail/go-crypto/openpgp/packet"
 	"github.com/ulikunitz/xz"
 )
 
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index 5b6f28d1ff795..bf248a4dde0f2 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -134,8 +134,7 @@ Note: This user hasn't uploaded any GPG keys.
 
 
 =twTO
------END PGP PUBLIC KEY BLOCK-----
-`)
+-----END PGP PUBLIC KEY BLOCK-----`)
 	// Import key
 	// User1 <user1@example.com>
 	session := loginUser(t, "user1")
@@ -169,8 +168,7 @@ C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
 7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
 GrE0MHOxUbc9tbtyk0F1SuzREUBH
 =DDXw
------END PGP PUBLIC KEY BLOCK-----
-`)
+-----END PGP PUBLIC KEY BLOCK-----`)
 	// Export new key
 	testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
 
@@ -201,8 +199,7 @@ C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
 7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
 GrE0MHOxUbc9tbtyk0F1SuzREUBH
 =WFf5
------END PGP PUBLIC KEY BLOCK-----
-`)
+-----END PGP PUBLIC KEY BLOCK-----`)
 }
 
 func testExportUserGPGKeys(t *testing.T, user, expected string) {

From c30f4f4be5eddc1996e496b01eb5585dac7cab49 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 27 Jan 2025 19:11:13 +0800
Subject: [PATCH 088/206] Fix issue suggestion bug (#33389) (#33391)

Backport #33389
---
 web_src/js/features/comp/TextExpander.ts | 62 ++++++++++++++++++++----
 1 file changed, 53 insertions(+), 9 deletions(-)

diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts
index bad8d2e59d076..1e6d46f977b70 100644
--- a/web_src/js/features/comp/TextExpander.ts
+++ b/web_src/js/features/comp/TextExpander.ts
@@ -5,8 +5,22 @@ import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts';
 import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
 import {getIssueColor, getIssueIcon} from '../issue.ts';
 import {debounce} from 'perfect-debounce';
+import type TextExpanderElement from '@github/text-expander-element';
 
-const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
+type TextExpanderProvideResult = {
+  matched: boolean,
+  fragment?: HTMLElement,
+}
+
+type TextExpanderChangeEvent = Event & {
+  detail?: {
+    key: string,
+    text: string,
+    provide: (result: TextExpanderProvideResult | Promise<TextExpanderProvideResult>) => void,
+  }
+}
+
+async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderProvideResult> {
   const issuePathInfo = parseIssueHref(window.location.href);
   if (!issuePathInfo.ownerName) {
     const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
@@ -14,10 +28,10 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
     issuePathInfo.repoName = repoOwnerPathInfo.repoName;
     // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
   }
-  if (!issuePathInfo.ownerName) return resolve({matched: false});
+  if (!issuePathInfo.ownerName) return {matched: false};
 
   const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
-  if (!matches.length) return resolve({matched: false});
+  if (!matches.length) return {matched: false};
 
   const ul = createElementFromAttrs('ul', {class: 'suggestions'});
   for (const issue of matches) {
@@ -29,11 +43,40 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
     );
     ul.append(li);
   }
-  resolve({matched: true, fragment: ul});
-}), 100);
+  return {matched: true, fragment: ul};
+}
 
-export function initTextExpander(expander) {
-  expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
+export function initTextExpander(expander: TextExpanderElement) {
+  if (!expander) return;
+
+  const textarea = expander.querySelector<HTMLTextAreaElement>('textarea');
+
+  // help to fix the text-expander "multiword+promise" bug: do not show the popup when there is no "#" before current line
+  const shouldShowIssueSuggestions = () => {
+    const posVal = textarea.value.substring(0, textarea.selectionStart);
+    const lineStart = posVal.lastIndexOf('\n');
+    const keyStart = posVal.lastIndexOf('#');
+    return keyStart > lineStart;
+  };
+
+  const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderProvideResult> => {
+    // https://github.com/github/text-expander-element/issues/71
+    // Upstream bug: when using "multiword+promise", TextExpander will get wrong "key" position.
+    // To reproduce, comment out the "shouldShowIssueSuggestions" check, use the "await sleep" below,
+    // then use content "close #20\nclose #20\nclose #20" (3 lines), keep changing the last line `#20` part from the end (including removing the `#`)
+    // There will be a JS error: Uncaught (in promise) IndexSizeError: Failed to execute 'setStart' on 'Range': The offset 28 is larger than the node's length (27).
+
+    // check the input before the request, to avoid emitting empty query to backend (still related to the upstream bug)
+    if (!shouldShowIssueSuggestions()) return {matched: false};
+    // await sleep(Math.random() * 1000); // help to reproduce the text-expander bug
+    const ret = await fetchIssueSuggestions(key, text);
+    // check the input again to avoid text-expander using incorrect position (upstream bug)
+    if (!shouldShowIssueSuggestions()) return {matched: false};
+    return ret;
+  }, 300); // to match onInputDebounce delay
+
+  expander.addEventListener('text-expander-change', (e: TextExpanderChangeEvent) => {
+    const {key, text, provide} = e.detail;
     if (key === ':') {
       const matches = matchEmoji(text);
       if (!matches.length) return provide({matched: false});
@@ -81,10 +124,11 @@ export function initTextExpander(expander) {
 
       provide({matched: true, fragment: ul});
     } else if (key === '#') {
-      provide(debouncedSuggestIssues(key, text));
+      provide(debouncedIssueSuggestions(key, text));
     }
   });
-  expander?.addEventListener('text-expander-value', ({detail}) => {
+
+  expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
     if (detail?.item) {
       // add a space after @mentions and #issue as it's likely the user wants one
       const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';

From 6f3837284d93f1279a38df9bbeb18e7c331828df Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 28 Jan 2025 01:16:21 +0800
Subject: [PATCH 089/206] Fix flex width (#33414) (#33418)

Backport #33414 by wxiaoguang

Fix #33409

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/repo/home-file-list.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css
index 19ba1f2bcbeb8..189b6406d402b 100644
--- a/web_src/css/repo/home-file-list.css
+++ b/web_src/css/repo/home-file-list.css
@@ -65,6 +65,7 @@
 }
 
 #repo-files-table .repo-file-last-commit {
+  min-width: 0; /* otherwise the flex axis is not limited and the text might overflow in Pale Moon */
   background: var(--color-box-header);
 }
 

From 2674d27fb8d8640ded573245d193e17fe2cf0a7e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 28 Jan 2025 01:42:47 +0800
Subject: [PATCH 090/206] Add pubdate for repository rss and add some tests
 (#33411) (#33416)

Backport #33411 by @lunny

Fix #33291

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/web/feed/branch.go                    |  1 +
 routers/web/feed/file.go                      |  1 +
 tests/integration/feed_repo_test.go           | 35 +++++++++++++++++++
 ...pi_feed_user_test.go => feed_user_test.go} | 25 ++++++++++++-
 4 files changed, 61 insertions(+), 1 deletion(-)
 create mode 100644 tests/integration/feed_repo_test.go
 rename tests/integration/{api_feed_user_test.go => feed_user_test.go} (53%)

diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index 80ce2ad198bcd..a8a001e0cd297 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -43,6 +43,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri
 			},
 			Description: commit.Message(),
 			Content:     commit.Message(),
+			Created:     commit.Committer.When,
 		})
 	}
 
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 1ab768ff27fcb..48f87c7c62d43 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -55,6 +55,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
 			},
 			Description: commit.Message(),
 			Content:     commit.Message(),
+			Created:     commit.Committer.When,
 		})
 	}
 
diff --git a/tests/integration/feed_repo_test.go b/tests/integration/feed_repo_test.go
new file mode 100644
index 0000000000000..132ed32ced803
--- /dev/null
+++ b/tests/integration/feed_repo_test.go
@@ -0,0 +1,35 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"encoding/xml"
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestFeedRepo(t *testing.T) {
+	t.Run("RSS", func(t *testing.T) {
+		defer tests.PrepareTestEnv(t)()
+
+		req := NewRequest(t, "GET", "/user2/repo1.rss")
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		data := resp.Body.String()
+		assert.Contains(t, data, `<rss version="2.0"`)
+
+		var rss RSS
+		err := xml.Unmarshal(resp.Body.Bytes(), &rss)
+		assert.NoError(t, err)
+		assert.Contains(t, rss.Channel.Link, "/user2/repo1")
+		assert.NotEmpty(t, rss.Channel.PubDate)
+		assert.Len(t, rss.Channel.Items, 1)
+		assert.EqualValues(t, "issue5", rss.Channel.Items[0].Description)
+		assert.NotEmpty(t, rss.Channel.Items[0].PubDate)
+	})
+}
diff --git a/tests/integration/api_feed_user_test.go b/tests/integration/feed_user_test.go
similarity index 53%
rename from tests/integration/api_feed_user_test.go
rename to tests/integration/feed_user_test.go
index c44f9a195111a..4315c67f4830b 100644
--- a/tests/integration/api_feed_user_test.go
+++ b/tests/integration/feed_user_test.go
@@ -4,6 +4,7 @@
 package integration
 
 import (
+	"encoding/xml"
 	"net/http"
 	"testing"
 
@@ -12,7 +13,23 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestFeed(t *testing.T) {
+// RSS is a struct to unmarshal RSS feeds test only
+type RSS struct {
+	Channel struct {
+		Title       string `xml:"title"`
+		Link        string `xml:"link"`
+		Description string `xml:"description"`
+		PubDate     string `xml:"pubDate"`
+		Items       []struct {
+			Title       string `xml:"title"`
+			Link        string `xml:"link"`
+			Description string `xml:"description"`
+			PubDate     string `xml:"pubDate"`
+		} `xml:"item"`
+	} `xml:"channel"`
+}
+
+func TestFeedUser(t *testing.T) {
 	t.Run("User", func(t *testing.T) {
 		t.Run("Atom", func(t *testing.T) {
 			defer tests.PrepareTestEnv(t)()
@@ -32,6 +49,12 @@ func TestFeed(t *testing.T) {
 
 			data := resp.Body.String()
 			assert.Contains(t, data, `<rss version="2.0"`)
+
+			var rss RSS
+			err := xml.Unmarshal(resp.Body.Bytes(), &rss)
+			assert.NoError(t, err)
+			assert.Contains(t, rss.Channel.Link, "/user2")
+			assert.NotEmpty(t, rss.Channel.PubDate)
 		})
 	})
 }

From b6fd8741eeb3796741cceb7928f388041044a4bb Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 28 Jan 2025 02:43:16 +0800
Subject: [PATCH 091/206] Fix system admin cannot fork or get private fork with
 API (#33401) (#33417)

Backport #33401 by @lunny

Fix #33368

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/api/v1/repo/fork.go         | 16 ++++++++------
 services/repository/fork.go         |  8 ++++---
 tests/integration/api_fork_test.go  | 33 +++++++++++++++++++++++++++--
 tests/integration/repo_fork_test.go |  3 ++-
 4 files changed, 47 insertions(+), 13 deletions(-)

diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go
index 14a1a8d1c4a3d..f96c432b92988 100644
--- a/routers/api/v1/repo/fork.go
+++ b/routers/api/v1/repo/fork.go
@@ -132,13 +132,15 @@ func CreateFork(ctx *context.APIContext) {
 			}
 			return
 		}
-		isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
-			return
-		} else if !isMember {
-			ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
-			return
+		if !ctx.Doer.IsAdmin {
+			isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
+			if err != nil {
+				ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
+				return
+			} else if !isMember {
+				ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
+				return
+			}
 		}
 		forker = org.AsUser()
 	}
diff --git a/services/repository/fork.go b/services/repository/fork.go
index bc4fdf85627b0..b91ad6889c555 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -258,9 +258,11 @@ type findForksOptions struct {
 }
 
 func (opts findForksOptions) ToConds() builder.Cond {
-	return builder.Eq{"fork_id": opts.RepoID}.And(
-		repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid),
-	)
+	cond := builder.Eq{"fork_id": opts.RepoID}
+	if opts.Doer != nil && opts.Doer.IsAdmin {
+		return cond
+	}
+	return cond.And(repo_model.AccessibleRepositoryCondition(opts.Doer, unit.TypeInvalid))
 }
 
 // FindForks returns all the forks of the repository
diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go
index 580bb459e7cce..69f37f45741bc 100644
--- a/tests/integration/api_fork_test.go
+++ b/tests/integration/api_fork_test.go
@@ -10,6 +10,7 @@ import (
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	org_model "code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
@@ -81,8 +82,8 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
 		var forks []*api.Repository
 		DecodeJSON(t, resp, &forks)
 
-		assert.Len(t, forks, 1)
-		assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
+		assert.Len(t, forks, 2)
+		assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
 
 		assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
 
@@ -96,3 +97,31 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
 		assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count"))
 	})
 }
+
+func TestGetPrivateReposForks(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user1Sess := loginUser(t, "user1")
+	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository
+	privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
+	user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository)
+
+	forkedRepoName := "forked-repo"
+	// create fork from a private repository
+	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
+		Organization: &privateOrg.Name,
+		Name:         &forkedRepoName,
+	}).AddTokenAuth(user1Token)
+	MakeRequest(t, req, http.StatusAccepted)
+
+	// test get a private fork without clear permissions
+	req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	forks := []*api.Repository{}
+	DecodeJSON(t, resp, &forks)
+	assert.Len(t, forks, 1)
+	assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "forked-repo", forks[0].Name)
+	assert.EqualValues(t, privateOrg.Name, forks[0].Owner.UserName)
+}
diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go
index 267fd0d56e57e..cbe5e4bb3fb01 100644
--- a/tests/integration/repo_fork_test.go
+++ b/tests/integration/repo_fork_test.go
@@ -118,7 +118,8 @@ func TestForkListLimitedAndPrivateRepos(t *testing.T) {
 		req := NewRequest(t, "GET", "/user2/repo1/forks")
 		resp := user1Sess.MakeRequest(t, req, http.StatusOK)
 		htmlDoc := NewHTMLParser(t, resp.Body)
-		assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length())
+		// since user1 is an admin, he can get both of the forked repositories
+		assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length())
 
 		assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1))
 		resp = user1Sess.MakeRequest(t, req, http.StatusOK)

From a8eaf43f970e560003a0b796158200e40da0bb75 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 30 Jan 2025 17:11:13 +0800
Subject: [PATCH 092/206] Fix user avatar (#33439)

---
 models/user/avatar.go               | 21 ++++++++-------
 models/user/avatar_test.go          | 40 +++++++++++++++++++++++++++++
 models/user/user_system.go          | 20 ++++++++++++++-
 models/user/user_system_test.go     | 32 +++++++++++++++++++++++
 modules/storage/storage.go          |  4 +--
 routers/web/user/avatar.go          | 28 +++++++-------------
 routers/web/user/home.go            |  2 +-
 routers/web/web.go                  |  2 +-
 services/actions/notifier_helper.go |  2 +-
 9 files changed, 117 insertions(+), 34 deletions(-)
 create mode 100644 models/user/user_system_test.go

diff --git a/models/user/avatar.go b/models/user/avatar.go
index 5453c78fc61db..2a41b9912928b 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -38,27 +38,30 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
 
 	u.Avatar = avatars.HashEmail(seed)
 
-	// Don't share the images so that we can delete them easily
-	if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
-		if err := png.Encode(w, img); err != nil {
-			log.Error("Encode: %v", err)
+	_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+	if err != nil {
+		// If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
+		// Don't share the images so that we can delete them easily
+		if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
+			if err := png.Encode(w, img); err != nil {
+				log.Error("Encode: %v", err)
+			}
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
 		}
-		return err
-	}); err != nil {
-		return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
 	}
 
 	if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
 		return err
 	}
 
-	log.Info("New random avatar created: %d", u.ID)
 	return nil
 }
 
 // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
 func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
-	if u.IsGhost() {
+	if u.IsGhost() || u.IsGiteaActions() {
 		return avatars.DefaultAvatarLink()
 	}
 
diff --git a/models/user/avatar_test.go b/models/user/avatar_test.go
index 1078875ee1611..a1cc01316f324 100644
--- a/models/user/avatar_test.go
+++ b/models/user/avatar_test.go
@@ -4,13 +4,19 @@
 package user
 
 import (
+	"context"
+	"io"
+	"strings"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestUserAvatarLink(t *testing.T) {
@@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
 	link = u.AvatarLink(db.DefaultContext)
 	assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
 }
+
+func TestUserAvatarGenerate(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+	var err error
+	tmpDir := t.TempDir()
+	storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
+	require.NoError(t, err)
+
+	u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
+
+	// there was no avatar, generate a new one
+	assert.Empty(t, u.Avatar)
+	err = GenerateRandomAvatar(db.DefaultContext, u)
+	require.NoError(t, err)
+	assert.NotEmpty(t, u.Avatar)
+
+	// make sure the generated one exists
+	oldAvatarPath := u.CustomAvatarRelativePath()
+	_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
+	require.NoError(t, err)
+	// and try to change its content
+	_, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
+	require.NoError(t, err)
+
+	// try to generate again
+	err = GenerateRandomAvatar(db.DefaultContext, u)
+	require.NoError(t, err)
+	assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
+	f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
+	require.NoError(t, err)
+	defer f.Close()
+	content, _ := io.ReadAll(f)
+	assert.Equal(t, "abcd", string(content))
+}
diff --git a/models/user/user_system.go b/models/user/user_system.go
index 612cdb2caef26..7ac48f5ea5583 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -24,6 +24,10 @@ func NewGhostUser() *User {
 	}
 }
 
+func IsGhostUserName(name string) bool {
+	return strings.EqualFold(name, GhostUserName)
+}
+
 // IsGhost check if user is fake user for a deleted account
 func (u *User) IsGhost() bool {
 	if u == nil {
@@ -48,6 +52,10 @@ const (
 	ActionsEmail    = "teabot@gitea.io"
 )
 
+func IsGiteaActionsUserName(name string) bool {
+	return strings.EqualFold(name, ActionsUserName)
+}
+
 // NewActionsUser creates and returns a fake user for running the actions.
 func NewActionsUser() *User {
 	return &User{
@@ -65,6 +73,16 @@ func NewActionsUser() *User {
 	}
 }
 
-func (u *User) IsActions() bool {
+func (u *User) IsGiteaActions() bool {
 	return u != nil && u.ID == ActionsUserID
 }
+
+func GetSystemUserByName(name string) *User {
+	if IsGhostUserName(name) {
+		return NewGhostUser()
+	}
+	if IsGiteaActionsUserName(name) {
+		return NewActionsUser()
+	}
+	return nil
+}
diff --git a/models/user/user_system_test.go b/models/user/user_system_test.go
new file mode 100644
index 0000000000000..97768b509be3d
--- /dev/null
+++ b/models/user/user_system_test.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSystemUser(t *testing.T) {
+	u, err := GetPossibleUserByID(db.DefaultContext, -1)
+	require.NoError(t, err)
+	assert.Equal(t, "Ghost", u.Name)
+	assert.Equal(t, "ghost", u.LowerName)
+	assert.True(t, u.IsGhost())
+	assert.True(t, IsGhostUserName("gHost"))
+
+	u, err = GetPossibleUserByID(db.DefaultContext, -2)
+	require.NoError(t, err)
+	assert.Equal(t, "gitea-actions", u.Name)
+	assert.Equal(t, "gitea-actions", u.LowerName)
+	assert.True(t, u.IsGiteaActions())
+	assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
+
+	_, err = GetPossibleUserByID(db.DefaultContext, -3)
+	require.Error(t, err)
+}
diff --git a/modules/storage/storage.go b/modules/storage/storage.go
index 750ecdfe0db70..b0529941e7da4 100644
--- a/modules/storage/storage.go
+++ b/modules/storage/storage.go
@@ -93,7 +93,7 @@ func Clean(storage ObjectStorage) error {
 }
 
 // SaveFrom saves data to the ObjectStorage with path p from the callback
-func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) error) error {
+func SaveFrom(objStorage ObjectStorage, path string, callback func(w io.Writer) error) error {
 	pr, pw := io.Pipe()
 	defer pr.Close()
 	go func() {
@@ -103,7 +103,7 @@ func SaveFrom(objStorage ObjectStorage, p string, callback func(w io.Writer) err
 		}
 	}()
 
-	_, err := objStorage.Save(p, pr, -1)
+	_, err := objStorage.Save(path, pr, -1)
 	return err
 }
 
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 7000e2577857b..81c00b3bd42d1 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -4,7 +4,6 @@
 package user
 
 import (
-	"strings"
 	"time"
 
 	"code.gitea.io/gitea/models/avatars"
@@ -21,32 +20,23 @@ func cacheableRedirect(ctx *context.Context, location string) {
 	ctx.Redirect(location)
 }
 
-// AvatarByUserName redirect browser to user avatar of requested size
-func AvatarByUserName(ctx *context.Context) {
-	userName := ctx.PathParam(":username")
-	size := int(ctx.PathParamInt64(":size"))
-
-	var user *user_model.User
-	if strings.ToLower(userName) != user_model.GhostUserLowerName {
+// AvatarByUsernameSize redirect browser to user avatar of requested size
+func AvatarByUsernameSize(ctx *context.Context) {
+	username := ctx.PathParam("username")
+	user := user_model.GetSystemUserByName(username)
+	if user == nil {
 		var err error
-		if user, err = user_model.GetUserByName(ctx, userName); err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				ctx.NotFound("GetUserByName", err)
-				return
-			}
-			ctx.ServerError("Invalid user: "+userName, err)
+		if user, err = user_model.GetUserByName(ctx, username); err != nil {
+			ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err)
 			return
 		}
-	} else {
-		user = user_model.NewGhostUser()
 	}
-
-	cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size))
+	cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size"))))
 }
 
 // AvatarByEmailHash redirects the browser to the email avatar link
 func AvatarByEmailHash(ctx *context.Context) {
-	hash := ctx.PathParam(":hash")
+	hash := ctx.PathParam("hash")
 	email, err := avatars.GetEmailForHash(ctx, hash)
 	if err != nil {
 		ctx.ServerError("invalid avatar hash: "+hash, err)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 7552310cf2436..df9a8a6bf6ad4 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -734,7 +734,7 @@ func UsernameSubRoute(ctx *context.Context) {
 	switch {
 	case strings.HasSuffix(username, ".png"):
 		if reloadParam(".png") {
-			AvatarByUserName(ctx)
+			AvatarByUsernameSize(ctx)
 		}
 	case strings.HasSuffix(username, ".keys"):
 		if reloadParam(".keys") {
diff --git a/routers/web/web.go b/routers/web/web.go
index 3c6e0862afb5e..b6c2fe8b4b95d 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -681,7 +681,7 @@ func registerRoutes(m *web.Router) {
 		m.Get("/activate", auth.Activate)
 		m.Post("/activate", auth.ActivatePost)
 		m.Any("/activate_email", auth.ActivateEmail)
-		m.Get("/avatar/{username}/{size}", user.AvatarByUserName)
+		m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
 		m.Get("/recover_account", auth.ResetPasswd)
 		m.Post("/recover_account", auth.ResetPasswdPost)
 		m.Get("/forgot_password", auth.ForgotPasswd)
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 323c6a76e422c..2d8885dc323ab 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -117,7 +117,7 @@ func (input *notifyInput) Notify(ctx context.Context) {
 
 func notify(ctx context.Context, input *notifyInput) error {
 	shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
-	if input.Doer.IsActions() {
+	if input.Doer.IsGiteaActions() {
 		// avoiding triggering cyclically, for example:
 		// a comment of an issue will trigger the runner to add a new comment as reply,
 		// and the new comment will trigger the runner again.

From 9780da583da6bdc5474ebbf81ef5146fcbf57a31 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 31 Jan 2025 10:11:51 +0800
Subject: [PATCH 093/206] Fix issue sidebar dropdown keyboard support (#33447)
 (#33450)

Backport #33447 by wxiaoguang

Just a quick fix, fix #33444

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 templates/repo/issue/sidebar/assignee_list.tmpl  | 3 ++-
 templates/repo/issue/sidebar/label_list.tmpl     | 3 ++-
 templates/repo/issue/sidebar/milestone_list.tmpl | 4 ++--
 templates/repo/issue/sidebar/project_list.tmpl   | 3 ++-
 web_src/js/features/repo-issue-sidebar.md        | 5 ++++-
 5 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl
index 4fe8043f3473d..f124c3f1ced8d 100644
--- a/templates/repo/issue/sidebar/assignee_list.tmpl
+++ b/templates/repo/issue/sidebar/assignee_list.tmpl
@@ -15,8 +15,9 @@
 				<i class="icon">{{svg "octicon-search" 16}}</i>
 				<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
 			</div>
-			<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
 			<div class="scrolling menu flex-items-block">
+				<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
+				<div class="divider"></div>
 				{{range $data.CandidateAssignees}}
 					<a class="item" href="#" data-value="{{.ID}}">
 						<span class="item-check-mark">{{svg "octicon-check"}}</span>
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl
index 9dd83ba188294..9b6195a8f4bb4 100644
--- a/templates/repo/issue/sidebar/label_list.tmpl
+++ b/templates/repo/issue/sidebar/label_list.tmpl
@@ -16,8 +16,9 @@
 					<i class="icon">{{svg "octicon-search" 16}}</i>
 					<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
 				</div>
-				<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
 				<div class="scrolling menu">
+					<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
+					<div class="divider"></div>
 					{{$previousExclusiveScope := "_no_scope"}}
 					{{range $data.RepoLabels}}
 						{{$exclusiveScope := .ExclusiveScope}}
diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl
index bf3582c54bd20..6495a42166b9c 100644
--- a/templates/repo/issue/sidebar/milestone_list.tmpl
+++ b/templates/repo/issue/sidebar/milestone_list.tmpl
@@ -18,9 +18,9 @@
 					<i class="icon">{{svg "octicon-search"}}</i>
 					<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestones"}}">
 				</div>
-				<div class="divider"></div>
-				<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
 				<div class="scrolling menu">
+					<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
+					<div class="divider"></div>
 					{{if $data.OpenMilestones}}
 						<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>
 						{{range $data.OpenMilestones}}
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl
index 39feb089ef7cb..4520d1f88d3b0 100644
--- a/templates/repo/issue/sidebar/project_list.tmpl
+++ b/templates/repo/issue/sidebar/project_list.tmpl
@@ -17,8 +17,9 @@
 				<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_projects"}}">
 			</div>
 			{{end}}
-			<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
 			<div class="scrolling menu">
+				<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
+				<div class="divider"></div>
 				{{if $data.OpenProjects}}
 					<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>
 					{{range $data.OpenProjects}}
diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md
index 6de013f1c2833..e1ce0927e1bdd 100644
--- a/web_src/js/features/repo-issue-sidebar.md
+++ b/web_src/js/features/repo-issue-sidebar.md
@@ -22,10 +22,13 @@ A sidebar combo (dropdown+list) is like this:
 When the selected items change, the `combo-value` input will be updated.
 If there is `data-update-url`, it also calls backend to attach/detach the changed items.
 
-Also, the changed items will be syncronized to the `ui list` items.
+Also, the changed items will be synchronized to the `ui list` items.
 
 The items with the same data-scope only allow one selected at a time.
 
 The dropdown selection could work in 2 modes:
 * single: only one item could be selected, it updates immediately when the item is selected.
 * multiple: multiple items could be selected, it defers the update until the dropdown is hidden.
+
+When using "scrolling menu", the items must be in the same level,
+otherwise keyboard (ArrowUp/ArrowDown/Enter) won't work.

From 159544a9508f8d587ea293c3463055c7d50c7343 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 31 Jan 2025 18:27:23 +0800
Subject: [PATCH 094/206] Revert empty lfs ref name (#33454) (#33457)

Backport #33454 by wxiaoguang

Fix #33453

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/lfs/http_client.go | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go
index 3acd23b8f73cf..0a27fb0c867cf 100644
--- a/modules/lfs/http_client.go
+++ b/modules/lfs/http_client.go
@@ -72,10 +72,14 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
 
 	url := fmt.Sprintf("%s/objects/batch", c.endpoint)
 
+	// Original:  In some lfs server implementations, they require the ref attribute. #32838
 	// `ref` is an "optional object describing the server ref that the objects belong to"
-	// but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones.
+	// but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones.
 	// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
-	request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects}
+	//
+	// UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453
+	request := &BatchRequest{operation, c.transferNames(), nil, objects}
+
 	payload := new(bytes.Buffer)
 	err := json.NewEncoder(payload).Encode(request)
 	if err != nil {

From 7df1204795a90c169a8d3a36df38f30fec229fc2 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 31 Jan 2025 19:30:16 +0800
Subject: [PATCH 095/206] Fix SSH LFS memory usage (#33455) (#33460)

Backport #33455 by wxiaoguang

Fix #33448

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/httplib/request.go             | 13 ++++--
 modules/lfstransfer/backend/backend.go | 40 ++++++++--------
 modules/lfstransfer/backend/lock.go    |  6 +--
 modules/lfstransfer/backend/util.go    | 63 ++++++--------------------
 modules/private/actions.go             |  2 +-
 modules/private/hook.go                | 10 ++--
 modules/private/internal.go            |  8 +++-
 modules/private/key.go                 |  4 +-
 modules/private/mail.go                |  2 +-
 modules/private/manager.go             | 22 ++++-----
 modules/private/restore_repo.go        |  2 +-
 modules/private/serv.go                |  4 +-
 services/lfs/server.go                 |  4 +-
 13 files changed, 77 insertions(+), 103 deletions(-)

diff --git a/modules/httplib/request.go b/modules/httplib/request.go
index 880d7ad3cbe36..267e276df344d 100644
--- a/modules/httplib/request.go
+++ b/modules/httplib/request.go
@@ -99,10 +99,10 @@ func (r *Request) Param(key, value string) *Request {
 	return r
 }
 
-// Body adds request raw body.
-// it supports string and []byte.
+// Body adds request raw body. It supports string, []byte and io.Reader as body.
 func (r *Request) Body(data any) *Request {
 	switch t := data.(type) {
+	case nil: // do nothing
 	case string:
 		bf := bytes.NewBufferString(t)
 		r.req.Body = io.NopCloser(bf)
@@ -111,6 +111,12 @@ func (r *Request) Body(data any) *Request {
 		bf := bytes.NewBuffer(t)
 		r.req.Body = io.NopCloser(bf)
 		r.req.ContentLength = int64(len(t))
+	case io.ReadCloser:
+		r.req.Body = t
+	case io.Reader:
+		r.req.Body = io.NopCloser(t)
+	default:
+		panic(fmt.Sprintf("unsupported request body type %T", t))
 	}
 	return r
 }
@@ -141,7 +147,7 @@ func (r *Request) getResponse() (*http.Response, error) {
 		}
 	} else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 {
 		r.Header("Content-Type", "application/x-www-form-urlencoded")
-		r.Body(paramBody)
+		r.Body(paramBody) // string
 	}
 
 	var err error
@@ -185,6 +191,7 @@ func (r *Request) getResponse() (*http.Response, error) {
 }
 
 // Response executes request client gets response manually.
+// Caller MUST close the response body if no error occurs
 func (r *Request) Response() (*http.Response, error) {
 	return r.getResponse()
 }
diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go
index 2b1fe49fdab28..540932b9302de 100644
--- a/modules/lfstransfer/backend/backend.go
+++ b/modules/lfstransfer/backend/backend.go
@@ -4,7 +4,6 @@
 package backend
 
 import (
-	"bytes"
 	"context"
 	"encoding/base64"
 	"fmt"
@@ -29,7 +28,7 @@ var Capabilities = []string{
 	"locking",
 }
 
-var _ transfer.Backend = &GiteaBackend{}
+var _ transfer.Backend = (*GiteaBackend)(nil)
 
 // GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
 type GiteaBackend struct {
@@ -78,17 +77,17 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
 		return nil, err
 	}
+	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {
 		g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
 		return nil, statusCodeToErr(resp.StatusCode)
 	}
-	defer resp.Body.Close()
 	respBytes, err := io.ReadAll(resp.Body)
 	if err != nil {
 		g.logger.Log("http read error", err)
@@ -158,8 +157,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
 	return pointers, nil
 }
 
-// Download implements transfer.Backend. The returned reader must be closed by the
-// caller.
+// Download implements transfer.Backend. The returned reader must be closed by the caller.
 func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
 	idMapStr, exists := args[argID]
 	if !exists {
@@ -187,25 +185,25 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeOctetStream,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
 	resp, err := req.Response()
 	if err != nil {
-		return nil, 0, err
+		return nil, 0, fmt.Errorf("failed to get response: %w", err)
 	}
+	// no need to close the body here by "defer resp.Body.Close()", see below
 	if resp.StatusCode != http.StatusOK {
 		return nil, 0, statusCodeToErr(resp.StatusCode)
 	}
-	defer resp.Body.Close()
-	respBytes, err := io.ReadAll(resp.Body)
+
+	respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64)
 	if err != nil {
-		return nil, 0, err
+		return nil, 0, fmt.Errorf("failed to parse content length: %w", err)
 	}
-	respSize := int64(len(respBytes))
-	respBuf := io.NopCloser(bytes.NewBuffer(respBytes))
-	return respBuf, respSize, nil
+	// transfer.Backend will check io.Closer interface and close this Body reader
+	return resp.Body, respSize, nil
 }
 
-// StartUpload implements transfer.Backend.
+// Upload implements transfer.Backend.
 func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
 	idMapStr, exists := args[argID]
 	if !exists {
@@ -234,15 +232,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
 		headerContentType:       mimeOctetStream,
 		headerContentLength:     strconv.FormatInt(size, 10),
 	}
-	reqBytes, err := io.ReadAll(r)
-	if err != nil {
-		return err
-	}
-	req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes)
+
+	req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil)
+	req.Body(r)
 	resp, err := req.Response()
 	if err != nil {
 		return err
 	}
+	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {
 		return statusCodeToErr(resp.StatusCode)
 	}
@@ -284,11 +281,12 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		return transfer.NewStatus(transfer.StatusInternalServerError), err
 	}
+	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusOK {
 		return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
 	}
diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go
index f094cce1db657..4b45658611463 100644
--- a/modules/lfstransfer/backend/lock.go
+++ b/modules/lfstransfer/backend/lock.go
@@ -50,7 +50,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
@@ -102,7 +102,7 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
@@ -185,7 +185,7 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
+	req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go
index cffefef375c79..f322d542575d8 100644
--- a/modules/lfstransfer/backend/util.go
+++ b/modules/lfstransfer/backend/util.go
@@ -5,15 +5,12 @@ package backend
 
 import (
 	"context"
-	"crypto/tls"
 	"fmt"
-	"net"
+	"io"
 	"net/http"
-	"time"
 
 	"code.gitea.io/gitea/modules/httplib"
-	"code.gitea.io/gitea/modules/proxyprotocol"
-	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/private"
 
 	"github.com/charmbracelet/git-lfs-transfer/transfer"
 )
@@ -89,53 +86,19 @@ func statusCodeToErr(code int) error {
 	}
 }
 
-func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request {
-	req := httplib.NewRequest(url, method).
-		SetContext(ctx).
-		SetTimeout(10*time.Second, 60*time.Second).
-		SetTLSClientConfig(&tls.Config{
-			InsecureSkipVerify: true,
-		})
-
-	if setting.Protocol == setting.HTTPUnix {
-		req.SetTransport(&http.Transport{
-			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
-				var d net.Dialer
-				conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
-				if err != nil {
-					return conn, err
-				}
-				if setting.LocalUseProxyProtocol {
-					if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
-						_ = conn.Close()
-						return nil, err
-					}
-				}
-				return conn, err
-			},
-		})
-	} else if setting.LocalUseProxyProtocol {
-		req.SetTransport(&http.Transport{
-			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
-				var d net.Dialer
-				conn, err := d.DialContext(ctx, network, address)
-				if err != nil {
-					return conn, err
-				}
-				if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
-					_ = conn.Close()
-					return nil, err
-				}
-				return conn, err
-			},
-		})
-	}
-
+func newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request {
+	req := private.NewInternalRequest(ctx, url, method)
 	for k, v := range headers {
 		req.Header(k, v)
 	}
-
-	req.Body(body)
-
+	switch body := body.(type) {
+	case nil: // do nothing
+	case []byte:
+		req.Body(body) // []byte
+	case io.Reader:
+		req.Body(body) // io.Reader or io.ReadCloser
+	default:
+		panic(fmt.Sprintf("unsupported request body type %T", body))
+	}
 	return req
 }
diff --git a/modules/private/actions.go b/modules/private/actions.go
index 311a28365004b..e68f2f85b0388 100644
--- a/modules/private/actions.go
+++ b/modules/private/actions.go
@@ -17,7 +17,7 @@ type GenerateTokenRequest struct {
 func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) {
 	reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token"
 
-	req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{
+	req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{
 		Scope: scope,
 	})
 
diff --git a/modules/private/hook.go b/modules/private/hook.go
index 745c200619faa..87d6549f9c503 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -85,7 +85,7 @@ type HookProcReceiveRefResult struct {
 // HookPreReceive check whether the provided commits are allowed
 func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
-	req := newInternalRequest(ctx, reqURL, "POST", opts)
+	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
 	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
 	_, extra := requestJSONResp(req, &ResponseText{})
 	return extra
@@ -94,7 +94,7 @@ func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOp
 // HookPostReceive updates services and users
 func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
-	req := newInternalRequest(ctx, reqURL, "POST", opts)
+	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
 	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
 	return requestJSONResp(req, &HookPostReceiveResult{})
 }
@@ -103,7 +103,7 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO
 func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
 
-	req := newInternalRequest(ctx, reqURL, "POST", opts)
+	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
 	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
 	return requestJSONResp(req, &HookProcReceiveResult{})
 }
@@ -115,7 +115,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
 		url.PathEscape(repoName),
 		url.PathEscape(branch),
 	)
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	_, extra := requestJSONResp(req, &ResponseText{})
 	return extra
 }
@@ -123,7 +123,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
 // SSHLog sends ssh error log response
 func SSHLog(ctx context.Context, isErr bool, msg string) error {
 	reqURL := setting.LocalURL + "api/internal/ssh/log"
-	req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
+	req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
 	_, extra := requestJSONResp(req, &ResponseText{})
 	return extra.Error
 }
diff --git a/modules/private/internal.go b/modules/private/internal.go
index c7e7773524d79..3bd4eb06b1954 100644
--- a/modules/private/internal.go
+++ b/modules/private/internal.go
@@ -34,7 +34,7 @@ func getClientIP() string {
 	return strings.Fields(sshConnEnv)[0]
 }
 
-func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request {
+func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request {
 	if setting.InternalToken == "" {
 		log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
 Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
@@ -82,13 +82,17 @@ Ensure you are running in the correct environment or set the correct configurati
 			},
 		})
 	}
+	return req
+}
 
+func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request {
+	req := NewInternalRequest(ctx, url, method)
 	if len(body) == 1 {
 		req.Header("Content-Type", "application/json")
 		jsonBytes, _ := json.Marshal(body[0])
 		req.Body(jsonBytes)
 	} else if len(body) > 1 {
-		log.Fatal("Too many arguments for newInternalRequest")
+		log.Fatal("Too many arguments for newInternalRequestAPI")
 	}
 
 	req.SetTimeout(10*time.Second, 60*time.Second)
diff --git a/modules/private/key.go b/modules/private/key.go
index dcd17148564cd..114683b34392c 100644
--- a/modules/private/key.go
+++ b/modules/private/key.go
@@ -14,7 +14,7 @@ import (
 func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
 	// Ask for running deliver hook and test pull request tasks.
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID)
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	_, extra := requestJSONResp(req, &ResponseText{})
 	return extra.Error
 }
@@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
 func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) {
 	// Ask for running deliver hook and test pull request tasks.
 	reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	req.Param("content", content)
 	return requestJSONResp(req, &ResponseText{})
 }
diff --git a/modules/private/mail.go b/modules/private/mail.go
index 08de5b7e28724..3904e37beab26 100644
--- a/modules/private/mail.go
+++ b/modules/private/mail.go
@@ -23,7 +23,7 @@ type Email struct {
 func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) {
 	reqURL := setting.LocalURL + "api/internal/mail/send"
 
-	req := newInternalRequest(ctx, reqURL, "POST", Email{
+	req := newInternalRequestAPI(ctx, reqURL, "POST", Email{
 		Subject: subject,
 		Message: message,
 		To:      to,
diff --git a/modules/private/manager.go b/modules/private/manager.go
index 6055e553bd0f0..e3d5ad57e068f 100644
--- a/modules/private/manager.go
+++ b/modules/private/manager.go
@@ -18,21 +18,21 @@ import (
 // Shutdown calls the internal shutdown function
 func Shutdown(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/shutdown"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Shutting down")
 }
 
 // Restart calls the internal restart function
 func Restart(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/restart"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Restarting")
 }
 
 // ReloadTemplates calls the internal reload-templates function
 func ReloadTemplates(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/reload-templates"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Reloaded")
 }
 
@@ -45,7 +45,7 @@ type FlushOptions struct {
 // FlushQueues calls the internal flush-queues function
 func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
-	req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
+	req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
 	if timeout > 0 {
 		req.SetReadWriteTimeout(timeout + 10*time.Second)
 	}
@@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R
 // PauseLogging pauses logging
 func PauseLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/pause-logging"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Logging Paused")
 }
 
 // ResumeLogging resumes logging
 func ResumeLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/resume-logging"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Logging Restarted")
 }
 
 // ReleaseReopenLogging releases and reopens logging files
 func ReleaseReopenLogging(ctx context.Context) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging"
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Logging Restarted")
 }
 
 // SetLogSQL sets database logging
 func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on)
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Log SQL setting set")
 }
 
@@ -91,7 +91,7 @@ type LoggerOptions struct {
 // AddLogger adds a logger
 func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/manager/add-logger"
-	req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{
+	req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{
 		Logger: logger,
 		Writer: writer,
 		Mode:   mode,
@@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri
 // RemoveLogger removes a logger
 func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
-	req := newInternalRequest(ctx, reqURL, "POST")
+	req := newInternalRequestAPI(ctx, reqURL, "POST")
 	return requestJSONClientMsg(req, "Removed")
 }
 
@@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
 func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel))
 
-	req := newInternalRequest(ctx, reqURL, "GET")
+	req := newInternalRequestAPI(ctx, reqURL, "GET")
 	callback := func(resp *http.Response, extra *ResponseExtra) {
 		_, extra.Error = io.Copy(out, resp.Body)
 	}
diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go
index 496209d3cbda2..9c3a0081427c9 100644
--- a/modules/private/restore_repo.go
+++ b/modules/private/restore_repo.go
@@ -24,7 +24,7 @@ type RestoreParams struct {
 func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra {
 	reqURL := setting.LocalURL + "api/internal/restore_repo"
 
-	req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{
+	req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{
 		RepoDir:    repoDir,
 		OwnerName:  ownerName,
 		RepoName:   repoName,
diff --git a/modules/private/serv.go b/modules/private/serv.go
index 480a44695496d..2ccc6c112915a 100644
--- a/modules/private/serv.go
+++ b/modules/private/serv.go
@@ -23,7 +23,7 @@ type KeyAndOwner struct {
 // ServNoCommand returns information about the provided key
 func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID)
-	req := newInternalRequest(ctx, reqURL, "GET")
+	req := newInternalRequestAPI(ctx, reqURL, "GET")
 	keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{})
 	if extra.HasError() {
 		return nil, nil, extra.Error
@@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m
 			reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb))
 		}
 	}
-	req := newInternalRequest(ctx, reqURL, "GET")
+	req := newInternalRequestAPI(ctx, reqURL, "GET")
 	return requestJSONResp(req, &ServCommandResults{})
 }
diff --git a/services/lfs/server.go b/services/lfs/server.go
index a77623fdc196c..c4866edaab233 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -134,7 +134,9 @@ func DownloadHandler(ctx *context.Context) {
 	}
 
 	contentLength := toByte + 1 - fromByte
-	ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
+	contentLengthStr := strconv.FormatInt(contentLength, 10)
+	ctx.Resp.Header().Set("Content-Length", contentLengthStr)
+	ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression
 	ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
 
 	filename := ctx.PathParam("filename")

From ebac324ff2153602cea1a86c089182ab496f7cd7 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 1 Feb 2025 16:43:10 +0800
Subject: [PATCH 096/206] Fix `GetCommitBranchStart` bug (#33298) (#33421)

Backport #33298 by Zettat123

Fix #33265
Fix #33370

This PR also fixes some bugs in `TestGitGeneral`.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/git/commit_test.go            |  2 +-
 modules/git/repo_commit.go            |  8 +--
 tests/integration/git_general_test.go | 71 +++++++++++++++++----------
 3 files changed, 50 insertions(+), 31 deletions(-)

diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go
index 6ac65564dc179..ee20e95c4517f 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -360,5 +360,5 @@ func Test_GetCommitBranchStart(t *testing.T) {
 	startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String())
 	assert.NoError(t, err)
 	assert.NotEmpty(t, startCommitID)
-	assert.EqualValues(t, "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", startCommitID)
+	assert.EqualValues(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
 }
diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 647894bb21346..02d8e163e4f25 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -519,6 +519,7 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
 	return nil
 }
 
+// GetCommitBranchStart returns the commit where the branch diverged
 func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
 	cmd := NewCommand(repo.Ctx, "log", prettyLogFormat)
 	cmd.AddDynamicArguments(endCommitID)
@@ -533,7 +534,8 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
 
 	parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'})
 
-	var startCommitID string
+	// check the commits one by one until we find a commit contained by another branch
+	// and we think this commit is the divergence point
 	for _, commitID := range parts {
 		branches, err := repo.getBranches(env, string(commitID), 2)
 		if err != nil {
@@ -541,11 +543,9 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s
 		}
 		for _, b := range branches {
 			if b != branch {
-				return startCommitID, nil
+				return string(commitID), nil
 			}
 		}
-
-		startCommitID = string(commitID)
 	}
 
 	return "", nil
diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go
index a47cb75196d03..5baf51c6eeebf 100644
--- a/tests/integration/git_general_test.go
+++ b/tests/integration/git_general_test.go
@@ -80,6 +80,7 @@ func testGitGeneral(t *testing.T, u *url.URL) {
 		mediaTest(t, &httpContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
 
 		t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "test/head"))
+		t.Run("CreateProtectedBranch", doCreateProtectedBranch(&httpContext, dstPath))
 		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
 		t.Run("AutoMerge", doAutoPRMerge(&httpContext, dstPath))
 		t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
@@ -121,6 +122,7 @@ func testGitGeneral(t *testing.T, u *url.URL) {
 			mediaTest(t, &sshContext, pushedFilesStandard[0], pushedFilesStandard[1], pushedFilesLFS[0], pushedFilesLFS[1])
 
 			t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "test/head2"))
+			t.Run("CreateProtectedBranch", doCreateProtectedBranch(&sshContext, dstPath))
 			t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
 			t.Run("MergeFork", func(t *testing.T) {
 				defer tests.PrintCurrentTest(t)()
@@ -325,6 +327,34 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin
 	return filepath.Base(tmpFile.Name()), err
 }
 
+func doCreateProtectedBranch(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
+	return func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
+
+		t.Run("ProtectBranchWithFilePatterns", doProtectBranch(ctx, "release-*", baseCtx.Username, "", "", "config*"))
+
+		// push a new branch without any new commits
+		t.Run("CreateProtectedBranch-NoChanges", doGitCreateBranch(dstPath, "release-v1.0"))
+		t.Run("PushProtectedBranch-NoChanges", doGitPushTestRepository(dstPath, "origin", "release-v1.0"))
+		t.Run("CheckoutMaster-NoChanges", doGitCheckoutBranch(dstPath, "master"))
+
+		// push a new branch with a new unprotected file
+		t.Run("CreateProtectedBranch-UnprotectedFile", doGitCreateBranch(dstPath, "release-v2.0"))
+		_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "abc.txt")
+		assert.NoError(t, err)
+		t.Run("PushProtectedBranch-UnprotectedFile", doGitPushTestRepository(dstPath, "origin", "release-v2.0"))
+		t.Run("CheckoutMaster-UnprotectedFile", doGitCheckoutBranch(dstPath, "master"))
+
+		// push a new branch with a new protected file
+		t.Run("CreateProtectedBranch-ProtectedFile", doGitCreateBranch(dstPath, "release-v3.0"))
+		_, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "config")
+		assert.NoError(t, err)
+		t.Run("PushProtectedBranch-ProtectedFile", doGitPushTestRepositoryFail(dstPath, "origin", "release-v3.0"))
+		t.Run("CheckoutMaster-ProtectedFile", doGitCheckoutBranch(dstPath, "master"))
+	}
+}
+
 func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
 	return func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
@@ -334,27 +364,23 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
 		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
 
 		// Protect branch without any whitelisting
-		t.Run("ProtectBranchNoWhitelist", func(t *testing.T) {
-			doProtectBranch(ctx, "protected", "", "", "")
-		})
+		t.Run("ProtectBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "", ""))
 
 		// Try to push without permissions, which should fail
 		t.Run("TryPushWithoutPermissions", func(t *testing.T) {
 			_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
 			assert.NoError(t, err)
-			doGitPushTestRepositoryFail(dstPath, "origin", "protected")
+			doGitPushTestRepositoryFail(dstPath, "origin", "protected")(t)
 		})
 
 		// Set up permissions for normal push but not force push
-		t.Run("SetupNormalPushPermissions", func(t *testing.T) {
-			doProtectBranch(ctx, "protected", baseCtx.Username, "", "")
-		})
+		t.Run("SetupNormalPushPermissions", doProtectBranch(ctx, "protected", baseCtx.Username, "", "", ""))
 
 		// Normal push should work
 		t.Run("NormalPushWithPermissions", func(t *testing.T) {
 			_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
 			assert.NoError(t, err)
-			doGitPushTestRepository(dstPath, "origin", "protected")
+			doGitPushTestRepository(dstPath, "origin", "protected")(t)
 		})
 
 		// Try to force push without force push permissions, which should fail
@@ -364,30 +390,22 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
 				_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
 				assert.NoError(t, err)
 			})
-			doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
+			doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")(t)
 		})
 
 		// Set up permissions for force push but not normal push
-		t.Run("SetupForcePushPermissions", func(t *testing.T) {
-			doProtectBranch(ctx, "protected", "", baseCtx.Username, "")
-		})
+		t.Run("SetupForcePushPermissions", doProtectBranch(ctx, "protected", "", baseCtx.Username, "", ""))
 
 		// Try to force push without normal push permissions, which should fail
-		t.Run("ForcePushWithoutNormalPermissions", func(t *testing.T) {
-			doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected")
-		})
+		t.Run("ForcePushWithoutNormalPermissions", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "protected"))
 
 		// Set up permissions for normal and force push (both are required to force push)
-		t.Run("SetupNormalAndForcePushPermissions", func(t *testing.T) {
-			doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "")
-		})
+		t.Run("SetupNormalAndForcePushPermissions", doProtectBranch(ctx, "protected", baseCtx.Username, baseCtx.Username, "", ""))
 
 		// Force push should now work
-		t.Run("ForcePushWithPermissions", func(t *testing.T) {
-			doGitPushTestRepository(dstPath, "-f", "origin", "protected")
-		})
+		t.Run("ForcePushWithPermissions", doGitPushTestRepository(dstPath, "-f", "origin", "protected"))
 
-		t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", ""))
+		t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", "", "", ""))
 		t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
 		var pr api.PullRequest
 		var err error
@@ -409,14 +427,14 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
 		t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
 		t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
 
-		t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*"))
+		t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "", "unprotected-file-*", ""))
 		t.Run("GenerateCommit", func(t *testing.T) {
 			_, err := generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "unprotected-file-")
 			assert.NoError(t, err)
 		})
 		t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
 
-		t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", ""))
+		t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, "", "", ""))
 
 		t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
 		t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
@@ -431,7 +449,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
 	}
 }
 
-func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) {
+func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns, protectedFilePatterns string) func(t *testing.T) {
 	// We are going to just use the owner to set the protection.
 	return func(t *testing.T) {
 		csrf := GetUserCSRFToken(t, ctx.Session)
@@ -440,6 +458,7 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhit
 			"_csrf":                     csrf,
 			"rule_name":                 branch,
 			"unprotected_file_patterns": unprotectedFilePatterns,
+			"protected_file_patterns":   protectedFilePatterns,
 		}
 
 		if userToWhitelistPush != "" {
@@ -465,7 +484,7 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhit
 		// Check if master branch has been locked successfully
 		flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash)
 		assert.NotNil(t, flashCookie)
-		assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
+		assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(url.QueryEscape(branch))+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
 	}
 }
 

From 23971a77a00d4970b7c209545c9f65db6351d509 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 1 Feb 2025 22:44:50 -0800
Subject: [PATCH 097/206] Add tests for webhook and fix some webhook bugs
 (#33396) (#33442)

This PR created a mock webhook server in the tests and added integration
tests for generic webhooks.
It also fixes bugs in package webhooks and pull request comment
webhooks.

This also corrected an error on the package webhook. The previous
implementation uses a `User` struct as an organization, now it has been
corrected but it will not be consistent with the previous
implementation, some fields which not belong to the organization have
been removed.

Backport #33396
Backport part of #33337
---
 models/webhook/webhook.go              |   6 +
 models/webhook/webhook_test.go         |   2 +-
 modules/structs/hook.go                |  60 +--
 modules/webhook/structs.go             |   1 +
 modules/webhook/type.go                |  17 +-
 routers/api/v1/utils/hook.go           |   2 +
 services/webhook/dingtalk.go           |   4 +
 services/webhook/discord.go            |   4 +
 services/webhook/feishu.go             |   4 +
 services/webhook/general_test.go       |   4 +-
 services/webhook/matrix.go             |   4 +
 services/webhook/msteams.go            |   4 +
 services/webhook/notifier.go           |  13 +-
 services/webhook/packagist.go          |   4 +
 services/webhook/slack.go              |   4 +
 services/webhook/telegram.go           |   4 +
 services/webhook/webhook.go            |  16 +-
 services/webhook/wechatwork.go         |   4 +
 tests/integration/api_repo_test.go     |  15 +-
 tests/integration/api_wiki_test.go     |  24 +-
 tests/integration/issue_test.go        |   2 +-
 tests/integration/repo_webhook_test.go | 565 +++++++++++++++++++++++++
 tests/mssql.ini.tmpl                   |   6 +
 tests/mysql.ini.tmpl                   |   6 +
 tests/pgsql.ini.tmpl                   |   6 +
 tests/sqlite.ini.tmpl                  |   6 +
 26 files changed, 682 insertions(+), 105 deletions(-)

diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 894357e36a12d..a17582c0c9aa1 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -299,6 +299,11 @@ func (w *Webhook) HasPackageEvent() bool {
 		(w.ChooseEvents && w.HookEvents.Package)
 }
 
+func (w *Webhook) HasStatusEvent() bool {
+	return w.SendEverything ||
+		(w.ChooseEvents && w.HookEvents.Status)
+}
+
 // HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
 func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
 	return w.SendEverything ||
@@ -337,6 +342,7 @@ func (w *Webhook) EventCheckers() []struct {
 		{w.HasReleaseEvent, webhook_module.HookEventRelease},
 		{w.HasPackageEvent, webhook_module.HookEventPackage},
 		{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
+		{w.HasStatusEvent, webhook_module.HookEventStatus},
 	}
 }
 
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index c6c3f40d46eff..5e135369e6c53 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -74,7 +74,7 @@ func TestWebhook_EventsArray(t *testing.T) {
 		"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
 		"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
 		"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
-		"package", "pull_request_review_request",
+		"package", "pull_request_review_request", "status",
 	},
 		(&Webhook{
 			HookEvent: &webhook_module.HookEvent{SendEverything: true},
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index ce5742e5c7ce9..cef2dbd712912 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -116,14 +116,7 @@ var (
 	_ Payloader = &PackagePayload{}
 )
 
-// _________                        __
-// \_   ___ \_______   ____ _____ _/  |_  ____
-// /    \  \/\_  __ \_/ __ \\__  \\   __\/ __ \
-// \     \____|  | \/\  ___/ / __ \|  | \  ___/
-//  \______  /|__|    \___  >____  /__|  \___  >
-//         \/             \/     \/          \/
-
-// CreatePayload FIXME
+// CreatePayload represents a payload information of create event.
 type CreatePayload struct {
 	Sha     string      `json:"sha"`
 	Ref     string      `json:"ref"`
@@ -157,13 +150,6 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
 	return hook, nil
 }
 
-// ________         .__          __
-// \______ \   ____ |  |   _____/  |_  ____
-//  |    |  \_/ __ \|  | _/ __ \   __\/ __ \
-//  |    `   \  ___/|  |_\  ___/|  | \  ___/
-// /_______  /\___  >____/\___  >__|  \___  >
-//         \/     \/          \/          \/
-
 // PusherType define the type to push
 type PusherType string
 
@@ -186,13 +172,6 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
 
-// ___________           __
-// \_   _____/__________|  | __
-//  |    __)/  _ \_  __ \  |/ /
-//  |     \(  <_> )  | \/    <
-//  \___  / \____/|__|  |__|_ \
-//      \/                   \/
-
 // ForkPayload represents fork payload
 type ForkPayload struct {
 	Forkee *Repository `json:"forkee"`
@@ -232,13 +211,6 @@ func (p *IssueCommentPayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
 
-// __________       .__
-// \______   \ ____ |  |   ____ _____    ______ ____
-//  |       _// __ \|  | _/ __ \\__  \  /  ___// __ \
-//  |    |   \  ___/|  |_\  ___/ / __ \_\___ \\  ___/
-//  |____|_  /\___  >____/\___  >____  /____  >\___  >
-//         \/     \/          \/     \/     \/     \/
-
 // HookReleaseAction defines hook release action type
 type HookReleaseAction string
 
@@ -302,13 +274,6 @@ func (p *PushPayload) Branch() string {
 	return strings.ReplaceAll(p.Ref, "refs/heads/", "")
 }
 
-// .___
-// |   | ______ ________ __   ____
-// |   |/  ___//  ___/  |  \_/ __ \
-// |   |\___ \ \___ \|  |  /\  ___/
-// |___/____  >____  >____/  \___  >
-//          \/     \/            \/
-
 // HookIssueAction FIXME
 type HookIssueAction string
 
@@ -371,13 +336,6 @@ type ChangesPayload struct {
 	Ref   *ChangesFromPayload `json:"ref,omitempty"`
 }
 
-// __________      .__  .__    __________                                     __
-// \______   \__ __|  | |  |   \______   \ ____  ________ __   ____   _______/  |_
-//  |     ___/  |  \  | |  |    |       _// __ \/ ____/  |  \_/ __ \ /  ___/\   __\
-//  |    |   |  |  /  |_|  |__  |    |   \  ___< <_|  |  |  /\  ___/ \___ \  |  |
-//  |____|   |____/|____/____/  |____|_  /\___  >__   |____/  \___  >____  > |__|
-//                                     \/     \/   |__|           \/     \/
-
 // PullRequestPayload represents a payload information of pull request event.
 type PullRequestPayload struct {
 	Action            HookIssueAction `json:"action"`
@@ -402,13 +360,6 @@ type ReviewPayload struct {
 	Content string `json:"content"`
 }
 
-//  __      __.__ __   .__
-// /  \    /  \__|  | _|__|
-// \   \/\/   /  |  |/ /  |
-//  \        /|  |    <|  |
-//   \__/\  / |__|__|_ \__|
-//        \/          \/
-
 // HookWikiAction an action that happens to a wiki page
 type HookWikiAction string
 
@@ -435,13 +386,6 @@ func (p *WikiPayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", " ")
 }
 
-//__________                           .__  __
-//\______   \ ____ ______   ____  _____|__|/  |_  ___________ ___.__.
-// |       _// __ \\____ \ /  _ \/  ___/  \   __\/  _ \_  __ <   |  |
-// |    |   \  ___/|  |_> >  <_> )___ \|  ||  | (  <_> )  | \/\___  |
-// |____|_  /\___  >   __/ \____/____  >__||__|  \____/|__|   / ____|
-//        \/     \/|__|              \/                       \/
-
 // HookRepoAction an action that happens to a repo
 type HookRepoAction string
 
@@ -480,7 +424,7 @@ type PackagePayload struct {
 	Action       HookPackageAction `json:"action"`
 	Repository   *Repository       `json:"repository"`
 	Package      *Package          `json:"package"`
-	Organization *User             `json:"organization"`
+	Organization *Organization     `json:"organization"`
 	Sender       *User             `json:"sender"`
 }
 
diff --git a/modules/webhook/structs.go b/modules/webhook/structs.go
index 927a91a74c545..a9f8e33ae28f7 100644
--- a/modules/webhook/structs.go
+++ b/modules/webhook/structs.go
@@ -26,6 +26,7 @@ type HookEvents struct {
 	Repository               bool `json:"repository"`
 	Release                  bool `json:"release"`
 	Package                  bool `json:"package"`
+	Status                   bool `json:"status"`
 }
 
 // HookEvent represents events that will delivery hook.
diff --git a/modules/webhook/type.go b/modules/webhook/type.go
index fbec8892722b7..281f7e653b2f6 100644
--- a/modules/webhook/type.go
+++ b/modules/webhook/type.go
@@ -38,14 +38,6 @@ const (
 // Event returns the HookEventType as an event string
 func (h HookEventType) Event() string {
 	switch h {
-	case HookEventCreate:
-		return "create"
-	case HookEventDelete:
-		return "delete"
-	case HookEventFork:
-		return "fork"
-	case HookEventPush:
-		return "push"
 	case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone:
 		return "issues"
 	case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone,
@@ -59,14 +51,9 @@ func (h HookEventType) Event() string {
 		return "pull_request_rejected"
 	case HookEventPullRequestReviewComment:
 		return "pull_request_comment"
-	case HookEventWiki:
-		return "wiki"
-	case HookEventRepository:
-		return "repository"
-	case HookEventRelease:
-		return "release"
+	default:
+		return string(h)
 	}
-	return ""
 }
 
 // HookType is the type of a webhook
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 4328878e19613..8e91c2e0efcc7 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -205,6 +205,8 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
 				Wiki:                     util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
 				Repository:               util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
 				Release:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
+				Package:                  util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
+				Status:                   util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
 			},
 			BranchFilter: form.BranchFilter,
 		},
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index e382f5a9df732..992b8c566fad7 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -190,3 +190,7 @@ func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
 	var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest)
+}
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index c562d981680a8..30d930062eb4c 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -277,6 +277,10 @@ func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
 	return newJSONRequest(pc, w, t, true)
 }
 
+func init() {
+	RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest)
+}
+
 func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
 	switch event {
 	case webhook_module.HookEventPullRequestReviewApproved:
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 7ca7d1cf5f369..4e6aebc39dc82 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -170,3 +170,7 @@ func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mo
 	var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest)
+}
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index ef1ec7f324c82..ec735d785a524 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -319,8 +319,8 @@ func packageTestPayload() *api.PackagePayload {
 			AvatarURL: "http://localhost:3000/user1/avatar",
 		},
 		Repository: nil,
-		Organization: &api.User{
-			UserName:  "org1",
+		Organization: &api.Organization{
+			Name:      "org1",
 			AvatarURL: "http://localhost:3000/org1/avatar",
 		},
 		Package: &api.Package{
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 5e9f808d8b6cb..96dfa139aca46 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -24,6 +24,10 @@ import (
 	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
+func init() {
+	RegisterWebhookRequester(webhook_module.MATRIX, newMatrixRequest)
+}
+
 func newMatrixRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &MatrixMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 7ef96ffa2734c..1ae7c4f93155a 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -349,3 +349,7 @@ func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_m
 	var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest)
+}
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index a3d5cb34b19d4..52a69d53b6960 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -8,6 +8,7 @@ import (
 
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/models/organization"
 	packages_model "code.gitea.io/gitea/models/packages"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -924,10 +925,16 @@ func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_mo
 		return
 	}
 
+	var org *api.Organization
+	if pd.Owner.IsOrganization() {
+		org = convert.ToOrganization(ctx, organization.OrgFromUser(pd.Owner))
+	}
+
 	if err := PrepareWebhooks(ctx, source, webhook_module.HookEventPackage, &api.PackagePayload{
-		Action:  action,
-		Package: apiPackage,
-		Sender:  convert.ToUser(ctx, sender, nil),
+		Action:       action,
+		Package:      apiPackage,
+		Organization: org,
+		Sender:       convert.ToUser(ctx, sender, nil),
 	}); err != nil {
 		log.Error("PrepareWebhooks: %v", err)
 	}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 4d809ab3a6a5f..e66895832b922 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -120,3 +120,7 @@ func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook
 	}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.PACKAGIST, newPackagistRequest)
+}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index c905e7a89f48f..e0e938d53db25 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -295,6 +295,10 @@ func newSlackRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_mod
 	return newJSONRequest(pc, w, t, true)
 }
 
+func init() {
+	RegisterWebhookRequester(webhook_module.SLACK, newSlackRequest)
+}
+
 var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
 
 // IsValidSlackChannel validates a channel name conforms to what slack expects:
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index e54d6f2947d21..6fbf995801cbd 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -187,3 +187,7 @@ func newTelegramRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_
 	var pc payloadConvertor[TelegramPayload] = telegramConvertor{}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.TELEGRAM, newTelegramRequest)
+}
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index e0e8fa2fc19b5..b5e0864f15dee 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -27,16 +27,12 @@ import (
 	"github.com/gobwas/glob"
 )
 
-var webhookRequesters = map[webhook_module.HookType]func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error){
-	webhook_module.SLACK:      newSlackRequest,
-	webhook_module.DISCORD:    newDiscordRequest,
-	webhook_module.DINGTALK:   newDingtalkRequest,
-	webhook_module.TELEGRAM:   newTelegramRequest,
-	webhook_module.MSTEAMS:    newMSTeamsRequest,
-	webhook_module.FEISHU:     newFeishuRequest,
-	webhook_module.MATRIX:     newMatrixRequest,
-	webhook_module.WECHATWORK: newWechatworkRequest,
-	webhook_module.PACKAGIST:  newPackagistRequest,
+type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
+
+var webhookRequesters = map[webhook_module.HookType]Requester{}
+
+func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) {
+	webhookRequesters[hookType] = requester
 }
 
 // IsValidHookTaskType returns true if a webhook registered
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 1d8c1d7dacd83..44e0ff7de51e5 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -179,3 +179,7 @@ func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhoo
 	var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
 	return newJSONRequest(pc, w, t, true)
 }
+
+func init() {
+	RegisterWebhookRequester(webhook_module.WECHATWORK, newWechatworkRequest)
+}
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 122afbfa08fb1..aae038d51e406 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -471,6 +471,15 @@ func TestAPIMirrorSyncNonMirrorRepo(t *testing.T) {
 	assert.Equal(t, "Repository is not a mirror", errRespJSON["message"])
 }
 
+func testAPIOrgCreateRepo(t *testing.T, session *TestSession, orgName, repoName string, status int) {
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
+
+	req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", orgName), &api.CreateRepoOption{
+		Name: repoName,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, status)
+}
+
 func TestAPIOrgRepoCreate(t *testing.T) {
 	testCases := []struct {
 		ctxUserID         int64
@@ -488,11 +497,7 @@ func TestAPIOrgRepoCreate(t *testing.T) {
 	for _, testCase := range testCases {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
 		session := loginUser(t, user.Name)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
-		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", testCase.orgName), &api.CreateRepoOption{
-			Name: testCase.repoName,
-		}).AddTokenAuth(token)
-		MakeRequest(t, req, testCase.expectedStatus)
+		testAPIOrgCreateRepo(t, session, testCase.orgName, testCase.repoName, testCase.expectedStatus)
 	}
 }
 
diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go
index 05d90fc4e371d..8e5f67e2822bc 100644
--- a/tests/integration/api_wiki_test.go
+++ b/tests/integration/api_wiki_test.go
@@ -172,6 +172,19 @@ func TestAPIListWikiPages(t *testing.T) {
 	assert.Equal(t, dummymeta, meta)
 }
 
+func testAPICreateWikiPage(t *testing.T, session *TestSession, userName, repoName, title string, status int) {
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", userName, repoName)
+
+	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
+		Title:         title,
+		ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
+		Message:       "",
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, status)
+}
+
 func TestAPINewWikiPage(t *testing.T) {
 	for _, title := range []string{
 		"New page",
@@ -180,16 +193,7 @@ func TestAPINewWikiPage(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 		username := "user2"
 		session := loginUser(t, username)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
-
-		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", username, "repo1")
-
-		req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{
-			Title:         title,
-			ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")),
-			Message:       "",
-		}).AddTokenAuth(token)
-		MakeRequest(t, req, http.StatusCreated)
+		testAPICreateWikiPage(t, session, username, "repo1", title, http.StatusCreated)
 	}
 }
 
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
index 4617c5f89a149..bd0cedd300baf 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -174,7 +174,7 @@ func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content,
 
 	htmlDoc = NewHTMLParser(t, resp.Body)
 
-	val := htmlDoc.doc.Find(".comment-list .comment .render-content p").Eq(commentCount).Text()
+	val := strings.TrimSpace(htmlDoc.doc.Find(".comment-list .comment .render-content").Eq(commentCount).Text())
 	assert.Equal(t, content, val)
 
 	idAttr, has := htmlDoc.doc.Find(".comment-list .comment").Eq(commentCount).Attr("id")
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index ef44a9e2d0fb4..5a50d7d4914f1 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -4,10 +4,23 @@
 package integration
 
 import (
+	"context"
+	"fmt"
+	"io"
 	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"strings"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/gitrepo"
+	"code.gitea.io/gitea/modules/json"
+	api "code.gitea.io/gitea/modules/structs"
+	webhook_module "code.gitea.io/gitea/modules/webhook"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/PuerkitoBio/goquery"
@@ -39,3 +52,555 @@ func TestNewWebHookLink(t *testing.T) {
 		})
 	}
 }
+
+func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string) {
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
+	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+userName+"/"+repoName+"/hooks", api.CreateHookOption{
+		Type: "gitea",
+		Config: api.CreateHookOptionConfig{
+			"content_type": "json",
+			"url":          url,
+		},
+		Events: []string{event},
+		Active: true,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusCreated)
+}
+
+func testCreateWebhookForRepo(t *testing.T, session *TestSession, webhookType, userName, repoName, url, eventKind string) {
+	csrf := GetUserCSRFToken(t, session)
+	req := NewRequestWithValues(t, "POST", "/"+userName+"/"+repoName+"/settings/hooks/"+webhookType+"/new", map[string]string{
+		"_csrf":        csrf,
+		"payload_url":  url,
+		"events":       eventKind,
+		"active":       "true",
+		"content_type": fmt.Sprintf("%d", webhook.ContentTypeJSON),
+		"http_method":  "POST",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
+func testAPICreateWebhookForOrg(t *testing.T, session *TestSession, userName, url, event string) {
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
+	req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/"+userName+"/hooks", api.CreateHookOption{
+		Type: "gitea",
+		Config: api.CreateHookOptionConfig{
+			"content_type": "json",
+			"url":          url,
+		},
+		Events: []string{event},
+		Active: true,
+	}).AddTokenAuth(token)
+	MakeRequest(t, req, http.StatusCreated)
+}
+
+type mockWebhookProvider struct {
+	server *httptest.Server
+}
+
+func newMockWebhookProvider(callback func(r *http.Request), status int) *mockWebhookProvider {
+	m := &mockWebhookProvider{}
+	m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		callback(r)
+		w.WriteHeader(status)
+	}))
+	return m
+}
+
+func (m *mockWebhookProvider) URL() string {
+	if m.server == nil {
+		return ""
+	}
+	return m.server.URL
+}
+
+// Close closes the mock webhook http server
+func (m *mockWebhookProvider) Close() {
+	if m.server != nil {
+		m.server.Close()
+		m.server = nil
+	}
+}
+
+func Test_WebhookCreate(t *testing.T) {
+	var payloads []api.CreatePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.CreatePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = string(webhook_module.HookEventCreate)
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "create")
+
+		// 2. trigger the webhook
+		testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
+
+		// 3. validate the webhook is triggered
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, string(webhook_module.HookEventCreate), triggeredEvent)
+		assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
+		assert.EqualValues(t, "master2", payloads[0].Ref)
+		assert.EqualValues(t, "branch", payloads[0].RefType)
+	})
+}
+
+func Test_WebhookDelete(t *testing.T) {
+	var payloads []api.DeletePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.DeletePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "delete"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "delete")
+
+		// 2. trigger the webhook
+		testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
+		testAPIDeleteBranch(t, "master2", http.StatusNoContent)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "delete", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
+		assert.EqualValues(t, "master2", payloads[0].Ref)
+		assert.EqualValues(t, "branch", payloads[0].RefType)
+	})
+}
+
+func Test_WebhookFork(t *testing.T) {
+	var payloads []api.ForkPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.ForkPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "fork"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user1")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "fork")
+
+		// 2. trigger the webhook
+		testRepoFork(t, session, "user2", "repo1", "user1", "repo1-fork", "master")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "fork", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "repo1-fork", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user1/repo1-fork", payloads[0].Repo.FullName)
+		assert.EqualValues(t, "repo1", payloads[0].Forkee.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Forkee.FullName)
+	})
+}
+
+func Test_WebhookIssueComment(t *testing.T) {
+	var payloads []api.IssueCommentPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.IssueCommentPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "issue_comment"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment")
+
+		// 2. trigger the webhook
+		issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2")
+		testIssueAddComment(t, session, issueURL, "issue title2 comment1", "")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "issue_comment", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "created", payloads[0].Action)
+		assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+		assert.EqualValues(t, "Title2", payloads[0].Issue.Title)
+		assert.EqualValues(t, "Description2", payloads[0].Issue.Body)
+		assert.EqualValues(t, "issue title2 comment1", payloads[0].Comment.Body)
+	})
+}
+
+func Test_WebhookRelease(t *testing.T) {
+	var payloads []api.ReleasePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.ReleasePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "release"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "release")
+
+		// 2. trigger the webhook
+		createNewRelease(t, session, "/user2/repo1", "v0.0.99", "v0.0.99", false, false)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "release", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "repo1", payloads[0].Repository.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName)
+		assert.EqualValues(t, "v0.0.99", payloads[0].Release.TagName)
+		assert.False(t, payloads[0].Release.IsDraft)
+		assert.False(t, payloads[0].Release.IsPrerelease)
+	})
+}
+
+func Test_WebhookPush(t *testing.T) {
+	var payloads []api.PushPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.PushPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "push"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push")
+
+		// 2. trigger the webhook
+		testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "push", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
+		assert.Len(t, payloads[0].Commits, 1)
+		assert.EqualValues(t, []string{"test_webhook_push.md"}, payloads[0].Commits[0].Added)
+	})
+}
+
+func Test_WebhookIssue(t *testing.T) {
+	var payloads []api.IssuePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.IssuePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "issues"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues")
+
+		// 2. trigger the webhook
+		testNewIssue(t, session, "user2", "repo1", "Title1", "Description1")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "issues", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "opened", payloads[0].Action)
+		assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+		assert.EqualValues(t, "Title1", payloads[0].Issue.Title)
+		assert.EqualValues(t, "Description1", payloads[0].Issue.Body)
+	})
+}
+
+func Test_WebhookPullRequest(t *testing.T) {
+	var payloads []api.PullRequestPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.PullRequestPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "pull_request"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request")
+
+		testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
+		// 2. trigger the webhook
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+		testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "pull_request", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "repo1", payloads[0].PullRequest.Base.Repository.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Base.Repository.FullName)
+		assert.EqualValues(t, "repo1", payloads[0].PullRequest.Head.Repository.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Head.Repository.FullName)
+		assert.EqualValues(t, 0, payloads[0].PullRequest.Additions)
+	})
+}
+
+func Test_WebhookPullRequestComment(t *testing.T) {
+	var payloads []api.IssueCommentPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.IssueCommentPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "pull_request_comment"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_comment")
+
+		// 2. trigger the webhook
+		testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated)
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+		prID := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request")
+
+		testIssueAddComment(t, session, "/user2/repo1/pulls/"+prID, "pull title2 comment1", "")
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "pull_request_comment", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "created", payloads[0].Action)
+		assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+		assert.EqualValues(t, "first pull request", payloads[0].Issue.Title)
+		assert.EqualValues(t, "", payloads[0].Issue.Body)
+		assert.EqualValues(t, "pull title2 comment1", payloads[0].Comment.Body)
+	})
+}
+
+func Test_WebhookWiki(t *testing.T) {
+	var payloads []api.WikiPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.WikiPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "wiki"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "wiki")
+
+		// 2. trigger the webhook
+		testAPICreateWikiPage(t, session, "user2", "repo1", "Test Wiki Page", http.StatusCreated)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "wiki", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "created", payloads[0].Action)
+		assert.EqualValues(t, "repo1", payloads[0].Repository.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName)
+		assert.EqualValues(t, "Test-Wiki-Page", payloads[0].Page)
+	})
+}
+
+func Test_WebhookRepository(t *testing.T) {
+	var payloads []api.RepositoryPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.RepositoryPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "repository"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user1")
+
+		testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "repository")
+
+		// 2. trigger the webhook
+		testAPIOrgCreateRepo(t, session, "org3", "repo_new", http.StatusCreated)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "repository", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "created", payloads[0].Action)
+		assert.EqualValues(t, "org3", payloads[0].Organization.UserName)
+		assert.EqualValues(t, "repo_new", payloads[0].Repository.Name)
+		assert.EqualValues(t, "org3/repo_new", payloads[0].Repository.FullName)
+	})
+}
+
+func Test_WebhookPackage(t *testing.T) {
+	var payloads []api.PackagePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.PackagePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "package"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user1")
+
+		testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "package")
+
+		// 2. trigger the webhook
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
+		url := fmt.Sprintf("/api/packages/%s/generic/%s/%s", "org3", "gitea", "v1.24.0")
+		req := NewRequestWithBody(t, "PUT", url+"/gitea", strings.NewReader("This is a dummy file")).
+			AddTokenAuth(token)
+		MakeRequest(t, req, http.StatusCreated)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "package", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, "created", payloads[0].Action)
+		assert.EqualValues(t, "gitea", payloads[0].Package.Name)
+		assert.EqualValues(t, "generic", payloads[0].Package.Type)
+		assert.EqualValues(t, "org3", payloads[0].Organization.UserName)
+		assert.EqualValues(t, "v1.24.0", payloads[0].Package.Version)
+	})
+}
+
+func Test_WebhookStatus(t *testing.T) {
+	var payloads []api.CommitStatusPayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		assert.Contains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should contain status")
+		assert.Contains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should contain status")
+		assert.Contains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should contain status")
+		assert.Contains(t, r.Header["X-Github-Event"], "status", "X-GitHub-Event should contain status")
+		assert.Contains(t, r.Header["X-Gitea-Event"], "status", "X-Gitea-Event should contain status")
+		assert.Contains(t, r.Header["X-Gogs-Event"], "status", "X-Gogs-Event should contain status")
+		content, _ := io.ReadAll(r.Body)
+		var payload api.CommitStatusPayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "status"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "status")
+
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+
+		gitRepo1, err := gitrepo.OpenRepository(context.Background(), repo1)
+		assert.NoError(t, err)
+		commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch)
+		assert.NoError(t, err)
+
+		// 2. trigger the webhook
+		testCtx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeAll)
+
+		// update a status for a commit via API
+		doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
+			State:       api.CommitStatusSuccess,
+			TargetURL:   "http://test.ci/",
+			Description: "",
+			Context:     "testci",
+		})(t)
+
+		// 3. validate the webhook is triggered
+		assert.EqualValues(t, "status", triggeredEvent)
+		assert.Len(t, payloads, 1)
+		assert.EqualValues(t, commitID, payloads[0].Commit.ID)
+		assert.EqualValues(t, "repo1", payloads[0].Repo.Name)
+		assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName)
+		assert.EqualValues(t, "testci", payloads[0].Context)
+		assert.EqualValues(t, commitID, payloads[0].SHA)
+	})
+}
+
+func Test_WebhookStatus_NoWrongTrigger(t *testing.T) {
+	var trigger string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		assert.NotContains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should not contain status")
+		assert.NotContains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should not contain status")
+		assert.NotContains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should not contain status")
+		trigger = "push"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// 1. create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+
+		// create a push_only webhook from web UI
+		testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only")
+
+		// 2. trigger the webhook with a push action
+		testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push")
+
+		// 3. validate the webhook is triggered with right event
+		assert.EqualValues(t, "push", trigger)
+	})
+}
diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl
index 77c969e813611..8b1593a396f9e 100644
--- a/tests/mssql.ini.tmpl
+++ b/tests/mssql.ini.tmpl
@@ -26,6 +26,9 @@ TYPE = immediate
 [queue.push_update]
 TYPE = immediate
 
+[queue.webhook_sender]
+TYPE = immediate
+
 [repository]
 ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories
 
@@ -112,3 +115,6 @@ ENABLED = true
 
 [actions]
 ENABLED = true
+
+[webhook]
+ALLOWED_HOST_LIST = 127.0.0.1
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index 0fddde46de69e..8cafba9591382 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -28,6 +28,9 @@ TYPE = immediate
 [queue.push_update]
 TYPE = immediate
 
+[queue.webhook_sender]
+TYPE = immediate
+
 [repository]
 ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories
 
@@ -119,3 +122,6 @@ REPLY_TO_ADDRESS = incoming+%{token}@localhost
 
 [actions]
 ENABLED = true
+
+[webhook]
+ALLOWED_HOST_LIST = 127.0.0.1
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index 695662c2e9d2e..fb881d411c7ad 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -27,6 +27,9 @@ TYPE = immediate
 [queue.push_update]
 TYPE = immediate
 
+[queue.webhook_sender]
+TYPE = immediate
+
 [repository]
 ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories
 
@@ -128,3 +131,6 @@ ENABLED = true
 
 [actions]
 ENABLED = true
+
+[webhook]
+ALLOWED_HOST_LIST = 127.0.0.1
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
index 1cbcd8b2e591a..c0b3e1c85979c 100644
--- a/tests/sqlite.ini.tmpl
+++ b/tests/sqlite.ini.tmpl
@@ -22,6 +22,9 @@ TYPE = immediate
 [queue.push_update]
 TYPE = immediate
 
+[queue.webhook_sender]
+TYPE = immediate
+
 [repository]
 ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories
 
@@ -117,3 +120,6 @@ RENDER_CONTENT_MODE=sanitized
 
 [actions]
 ENABLED = true
+
+[webhook]
+ALLOWED_HOST_LIST = 127.0.0.1

From 2746c6f1aa900a8ac99fcf6eb273af006873e1ec Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 3 Feb 2025 04:08:59 +0800
Subject: [PATCH 098/206] Correct bot label `vertical-align` (#33477) (#33480)

---
 templates/shared/user/authorlink.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index d57a635b4bf29..abe1ab1ce229c 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1 @@
-<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
+<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}

From 200cb6140d9929130e4aacf49464bd544765befe Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 4 Feb 2025 19:35:47 -0800
Subject: [PATCH 099/206] Fix commit status events (#33320) (#33493)

Fix #32873
Fix #33201
~Fix #33244~
~Fix #33302~

depends on ~#33396~
backport #33320
---
 options/locale/locale_en-US.ini               |  2 ++
 routers/web/repo/setting/webhook.go           |  1 +
 services/forms/repo_form.go                   |  1 +
 services/webhook/dingtalk.go                  |  6 ++++++
 services/webhook/discord.go                   |  6 ++++++
 services/webhook/feishu.go                    |  6 ++++++
 services/webhook/general.go                   | 12 ++++++++++++
 services/webhook/matrix.go                    |  7 +++++++
 services/webhook/msteams.go                   | 14 ++++++++++++++
 services/webhook/packagist.go                 |  4 ++++
 services/webhook/payloader.go                 |  3 +++
 services/webhook/slack.go                     |  6 ++++++
 services/webhook/telegram.go                  |  6 ++++++
 services/webhook/wechatwork.go                |  6 ++++++
 templates/repo/settings/webhook/settings.tmpl | 11 +++++++++++
 15 files changed, 91 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 26c617f7bbc54..fd05ae7614948 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2325,6 +2325,8 @@ settings.event_fork = Fork
 settings.event_fork_desc = Repository forked.
 settings.event_wiki = Wiki
 settings.event_wiki_desc = Wiki page created, renamed, edited or deleted.
+settings.event_statuses = Statuses
+settings.event_statuses_desc = Commit Status updated from the API.
 settings.event_release = Release
 settings.event_release_desc = Release published, updated or deleted in a repository.
 settings.event_push = Push
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 8d548c4e3d1ae..c519b4117fc36 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -184,6 +184,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
 			Wiki:                     form.Wiki,
 			Repository:               form.Repository,
 			Package:                  form.Package,
+			Status:                   form.Status,
 		},
 		BranchFilter: form.BranchFilter,
 	}
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 7647c74e46579..f558133aeb599 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -263,6 +263,7 @@ type WebhookForm struct {
 	Wiki                     bool
 	Repository               bool
 	Package                  bool
+	Status                   bool
 	Active                   bool
 	BranchFilter             string `binding:"GlobPattern"`
 	AuthorizationHeader      string
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 992b8c566fad7..3ea8f50764d62 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -170,6 +170,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
 	return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
 }
 
+func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
+}
+
 func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
 	return DingtalkPayload{
 		MsgType: "actionCard",
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 30d930062eb4c..43e5e533bff65 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -265,6 +265,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
 	return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
 }
 
+func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
+	text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+	return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
+}
+
 func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &DiscordMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 4e6aebc39dc82..639118d2a5eee 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -166,6 +166,12 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
 	return newFeishuTextPayload(text), nil
 }
 
+func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return newFeishuTextPayload(text), nil
+}
+
 func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/services/webhook/general.go b/services/webhook/general.go
index dde43bb3495c6..91bf68600fcc5 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -307,6 +307,18 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
 	return text, color
 }
 
+func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+	refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+
+	text = fmt.Sprintf("Commit Status changed: %s", refLink)
+	color = greenColor
+	if withSender {
+		text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+	}
+
+	return text, color
+}
+
 // ToHook convert models.Webhook to api.Hook
 // This function is not part of the convert package to prevent an import cycle
 func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 96dfa139aca46..ec21712837b93 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -244,6 +244,13 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
 	return m.newPayload(text)
 }
 
+func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
+	refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+	text := fmt.Sprintf("Commit Status changed: %s", refLink)
+
+	return m.newPayload(text)
+}
+
 var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
 
 func getMessageBody(htmlText string) string {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 1ae7c4f93155a..485f695be2042 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -303,6 +303,20 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
 	), nil
 }
 
+func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
+	title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+	return createMSTeamsPayload(
+		p.Repo,
+		p.Sender,
+		title,
+		"",
+		p.TargetURL,
+		color,
+		&MSTeamsFact{"CommitStatus:", p.Context},
+	), nil
+}
+
 func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
 	facts := make([]MSTeamsFact, 0, 2)
 	if r != nil {
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index e66895832b922..6864fc822abde 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -110,6 +110,10 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e
 	return PackagistPayload{}, nil
 }
 
+func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) {
+	return PackagistPayload{}, nil
+}
+
 func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	meta := &PackagistMeta{}
 	if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index ab280a25b661e..c29ad8ac9206e 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -28,6 +28,7 @@ type payloadConvertor[T any] interface {
 	Release(*api.ReleasePayload) (T, error)
 	Wiki(*api.WikiPayload) (T, error)
 	Package(*api.PackagePayload) (T, error)
+	Status(*api.CommitStatusPayload) (T, error)
 }
 
 func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
@@ -77,6 +78,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
 		return convertUnmarshalledJSON(rc.Wiki, data)
 	case webhook_module.HookEventPackage:
 		return convertUnmarshalledJSON(rc.Package, data)
+	case webhook_module.HookEventStatus:
+		return convertUnmarshalledJSON(rc.Status, data)
 	}
 	return t, fmt.Errorf("newPayload unsupported event: %s", event)
 }
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index e0e938d53db25..a5edb830f3d8d 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -167,6 +167,12 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
 	return s.createPayload(text, nil), nil
 }
 
+func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) {
+	text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true)
+
+	return s.createPayload(text, nil), nil
+}
+
 // Push implements payloadConvertor Push method
 func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
 	// n new commits
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 6fbf995801cbd..485e2d990bae7 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -174,6 +174,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
 	return createTelegramPayloadHTML(text), nil
 }
 
+func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) {
+	text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true)
+
+	return createTelegramPayloadHTML(text), nil
+}
+
 func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
 	// https://core.telegram.org/bots/api#formatting-options
 	return TelegramPayload{
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 44e0ff7de51e5..1c834b4020aee 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -175,6 +175,12 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
 	return newWechatworkMarkdownPayload(text), nil
 }
 
+func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) {
+	text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+	return newWechatworkMarkdownPayload(text), nil
+}
+
 func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
 	var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
 	return newJSONRequest(pc, w, t, true)
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index cf3b0eb053057..b31003f2f1b9c 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -109,6 +109,17 @@
 			</div>
 		</div>
 
+		<!-- Status -->
+		<div class="seven wide column">
+			<div class="field">
+				<div class="ui checkbox">
+					<input name="status" type="checkbox" {{if .Webhook.HookEvents.Get "status"}}checked{{end}}>
+					<label>{{ctx.Locale.Tr "repo.settings.event_statuses"}}</label>
+					<span class="help">{{ctx.Locale.Tr "repo.settings.event_statuses_desc"}}</span>
+				</div>
+			</div>
+		</div>
+
 		<!-- Issue Events -->
 		<div class="fourteen wide column">
 			<label>{{ctx.Locale.Tr "repo.settings.event_header_issue"}}</label>

From 2df7d0835a206a7b00f8084d86ea0c311a7a1fd5 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 5 Feb 2025 12:41:48 +0800
Subject: [PATCH 100/206] Fix unnecessary comment when moving issue on the same
 project column (#33496) (#33499)

Backport #33496 by @lunny

Fix #33482

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/issues/issue_project.go | 10 ++++++----
 modules/indexer/issues/util.go |  7 ++++++-
 services/projects/issue.go     | 31 +++++++++++++++++++------------
 3 files changed, 31 insertions(+), 17 deletions(-)

diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index c4515fd898595..f520604321c9d 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -38,13 +38,15 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
 }
 
 // ProjectColumnID return project column id if issue was assigned to one
-func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
+func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
 	var ip project_model.ProjectIssue
 	has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
-	if err != nil || !has {
-		return 0
+	if err != nil {
+		return 0, err
+	} else if !has {
+		return 0, nil
 	}
-	return ip.ProjectColumnID
+	return ip.ProjectColumnID, nil
 }
 
 // LoadIssuesFromColumn load issues assigned to this column
diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go
index deb19adc49dd9..19d835a1d80aa 100644
--- a/modules/indexer/issues/util.go
+++ b/modules/indexer/issues/util.go
@@ -92,6 +92,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
 		projectID = issue.Project.ID
 	}
 
+	projectColumnID, err := issue.ProjectColumnID(ctx)
+	if err != nil {
+		return nil, false, err
+	}
+
 	return &internal.IndexerData{
 		ID:                 issue.ID,
 		RepoID:             issue.RepoID,
@@ -106,7 +111,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
 		NoLabel:            len(labels) == 0,
 		MilestoneID:        issue.MilestoneID,
 		ProjectID:          projectID,
-		ProjectColumnID:    issue.ProjectColumnID(ctx),
+		ProjectColumnID:    projectColumnID,
 		PosterID:           issue.PosterID,
 		AssigneeID:         issue.AssigneeID,
 		MentionIDs:         mentionIDs,
diff --git a/services/projects/issue.go b/services/projects/issue.go
index db1621a39f933..6ca0f168060ad 100644
--- a/services/projects/issue.go
+++ b/services/projects/issue.go
@@ -55,22 +55,29 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
 				continue
 			}
 
-			_, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
+			projectColumnID, err := curIssue.ProjectColumnID(ctx)
 			if err != nil {
 				return err
 			}
 
-			// add timeline to issue
-			if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
-				Type:               issues_model.CommentTypeProjectColumn,
-				Doer:               doer,
-				Repo:               curIssue.Repo,
-				Issue:              curIssue,
-				ProjectID:          column.ProjectID,
-				ProjectTitle:       project.Title,
-				ProjectColumnID:    column.ID,
-				ProjectColumnTitle: column.Title,
-			}); err != nil {
+			if projectColumnID != column.ID {
+				// add timeline to issue
+				if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
+					Type:               issues_model.CommentTypeProjectColumn,
+					Doer:               doer,
+					Repo:               curIssue.Repo,
+					Issue:              curIssue,
+					ProjectID:          column.ProjectID,
+					ProjectTitle:       project.Title,
+					ProjectColumnID:    column.ID,
+					ProjectColumnTitle: column.Title,
+				}); err != nil {
+					return err
+				}
+			}
+
+			_, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
+			if err != nil {
 				return err
 			}
 		}

From 92436b8b2ac4e6bb712905b57ffab4d73abddcb1 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 5 Feb 2025 03:06:58 -0800
Subject: [PATCH 101/206] add `timetzdata` build tag to binary releases
 (#33463) (#33503)

Backport #33463

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 main_timezones.go | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 main_timezones.go

diff --git a/main_timezones.go b/main_timezones.go
new file mode 100644
index 0000000000000..e1233007c62f2
--- /dev/null
+++ b/main_timezones.go
@@ -0,0 +1,16 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+//go:build windows
+
+package main
+
+// Golang has the ability to load OS's timezone data from most UNIX systems (https://github.com/golang/go/blob/master/src/time/zoneinfo_unix.go)
+// Even if the timezone data is missing, users could install the related packages to get it.
+// But on Windows, although `zoneinfo_windows.go` tries to load the timezone data from Windows registry,
+// some users still suffer from the issue that the timezone data is missing: https://github.com/go-gitea/gitea/issues/33235
+// So we import the tzdata package to make sure the timezone data is included in the binary.
+//
+// For non-Windows package builders, they could still use the "TAGS=timetzdata" to include the tzdata package in the binary.
+// If we decided to add the tzdata for other platforms, modify the "go:build" directive above.
+import _ "time/tzdata"

From f076ada601810cabd7c346d05f66790087c76444 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 5 Feb 2025 10:09:17 -0800
Subject: [PATCH 102/206] Add changelog for 1.23.2 (#33494)

Wait
- #33499
- #33493
- #33503

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61644628e7f4f..329b6b5feaba8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,52 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
+
+* BREAKING
+  * Add tests for webhook and fix some webhook bugs (#33396) (#33442)
+    * Package webhook’s Organization was incorrectly used as the User struct. This PR fixes the issue.
+    * This changelog is just a hint. The change is not really breaking because most fields are the same, most users are not affected.
+* ENHANCEMENTS
+  * Clone button enhancements (#33362) (#33404)
+  * Repo homepage styling tweaks (#33289) (#33381)
+  * Add a confirm dialog for "sync fork" (#33270) (#33273)
+  * Make tracked time representation display as hours (#33315) (#33334)
+  * Improve sync fork behavior (#33319) (#33332)
+* BUGFIXES
+  * Fix code button alignment (#33345) (#33351)
+  * Correct bot label `vertical-align` (#33477) (#33480)
+  * Fix SSH LFS memory usage (#33455) (#33460)
+  * Fix issue sidebar dropdown keyboard support (#33447) (#33450)
+  * Fix user avatar (#33439)
+  * Fix `GetCommitBranchStart` bug (#33298) (#33421)
+  * Add pubdate for repository rss and add some tests (#33411) (#33416)
+  * Add missed auto merge feed message on dashboard (#33309) (#33405)
+  * Fix issue suggestion bug (#33389) (#33391)
+  * Make issue suggestion work for all editors (#33340) (#33342)
+  * Fix issue count (#33338) (#33341)
+  * Fix Account linking page (#33325) (#33327)
+  * Fix closed dependency title (#33285) (#33287)
+  * Fix sidebar milestone link (#33269) (#33272)
+  * Fix missing license when sync mirror (#33255) (#33258)
+  * Fix upload file form (#33230) (#33233)
+  * Fix mirror bug (#33224) (#33225)
+  * Fix system admin cannot fork or get private fork with API (#33401) (#33417)
+  * Fix push message behavior (#33215) (#33317)
+  * Trivial fixes (#33304) (#33312)
+  * Fix "stop time tracking button" on navbar (#33084) (#33300)
+  * Fix tag route and empty repo (#33253)
+  * Fix cache test triggered by non memory cache (#33220) (#33221)
+  * Revert empty lfs ref name (#33454) (#33457)
+  * Fix flex width (#33414) (#33418)
+  * Fix commit status events (#33320) #33493
+  * Fix unnecessary comment when moving issue on the same project column (#33496) #33499
+  * Add timetzdata build tag to binary releases (#33463) #33503
+* MISC
+  * Use ProtonMail/go-crypto to replace keybase/go-crypto (#33402) (#33410)
+  * Update katex to latest version (#33361)
+  * Update go tool dependencies (#32916) (#33355)
+
 ## [1.23.1](https://github.com/go-gitea/gitea/releases/tag/v1.23.1) - 2025-01-09
 
 * ENHANCEMENTS

From 58daaf66e892e4fae08e65244ce4ed924996a498 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 5 Feb 2025 16:48:22 -0800
Subject: [PATCH 103/206] Fix a bug caused by status webhook template (#33512)

Fix #33511
---
 templates/repo/settings/webhook/settings.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index b31003f2f1b9c..07e7305866814 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -113,7 +113,7 @@
 		<div class="seven wide column">
 			<div class="field">
 				<div class="ui checkbox">
-					<input name="status" type="checkbox" {{if .Webhook.HookEvents.Get "status"}}checked{{end}}>
+					<input name="status" type="checkbox" {{if .Webhook.Status}}checked{{end}}>
 					<label>{{ctx.Locale.Tr "repo.settings.event_statuses"}}</label>
 					<span class="help">{{ctx.Locale.Tr "repo.settings.event_statuses_desc"}}</span>
 				</div>

From 312565e3c27b13d8f5eab07d45c2f1c3d48b5d8f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 5 Feb 2025 20:32:13 -0800
Subject: [PATCH 104/206] Add changelog for 1.23.3 (#33515)

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
---
 CHANGELOG.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 329b6b5feaba8..e04da654a97ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-05
+
+* Security
+  * Build Gitea with Golang v1.23.6 to fix security bugs
+* BUGFIXES
+  * 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
 
 * BREAKING

From a014d071e49e95286f8a9f3748e4d76794746ff3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 11 Feb 2025 01:22:39 +0800
Subject: [PATCH 105/206] Rework suggestion backend (#33538) (#33546)

Backport #33538 by @lunny

Fix #33522

The suggestion backend logic now is

- If the keyword is empty, returned the latest 5 issues/prs with index
desc order
- If the keyword is digital, find all issues/prs which `index` has a
prefix with that, with index asc order
- If the keyword is non-digital or if the queried records less than 5,
searching issues/prs title with a `like`, with index desc order

## Empty keyword
<img width="310" alt="image"
src="https://github.com/user-attachments/assets/1912c634-0d98-4eeb-8542-d54240901f77"
/>

## Digital
<img width="479" alt="image"
src="https://github.com/user-attachments/assets/0356a936-7110-4a24-b21e-7400201bf9b8"
/>

## Digital and title contains the digital
<img width="363" alt="image"
src="https://github.com/user-attachments/assets/6e12f908-28fe-48de-8ccc-09cbeab024d4"
/>

## non-Digital
<img width="435" alt="image"
src="https://github.com/user-attachments/assets/2722bb53-baa2-4d67-a224-522a65f73856"
/>
<img width="477" alt="image"
src="https://github.com/user-attachments/assets/06708dd9-80d1-4a88-b32b-d29072dd1ba6"
/>

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/issue.go                | 40 +++++++++++++++
 routers/web/repo/issue_suggestions.go | 52 ++-----------------
 services/issue/suggestion.go          | 73 +++++++++++++++++++++++++++
 services/issue/suggestion_test.go     | 57 +++++++++++++++++++++
 4 files changed, 173 insertions(+), 49 deletions(-)
 create mode 100644 services/issue/suggestion.go
 create mode 100644 services/issue/suggestion_test.go

diff --git a/models/issues/issue.go b/models/issues/issue.go
index fe347c271560e..f4b575d80470d 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -17,6 +17,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -531,6 +532,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
 	return issue, nil
 }
 
+func isPullToCond(isPull optional.Option[bool]) builder.Cond {
+	if isPull.Has() {
+		return builder.Eq{"is_pull": isPull.Value()}
+	}
+	return builder.NewCond()
+}
+
+func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
+	issues := make([]*Issue, 0, pageSize)
+	err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+		And(isPullToCond(isPull)).
+		OrderBy("updated_unix DESC").
+		Limit(pageSize).
+		Find(&issues)
+	return issues, err
+}
+
+func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
+	cond := builder.NewCond()
+	if excludedID > 0 {
+		cond = cond.And(builder.Neq{"`id`": excludedID})
+	}
+
+	// It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
+	// The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) +  content"
+	// But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
+	// So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
+	cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
+
+	issues := make([]*Issue, 0, pageSize)
+	err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+		And(isPullToCond(isPull)).
+		And(cond).
+		OrderBy("updated_unix DESC, `index` DESC").
+		Limit(pageSize).
+		Find(&issues)
+	return issues, err
+}
+
 // GetIssueWithAttrsByIndex returns issue by index in a repository.
 func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
 	issue, err := GetIssueByIndex(ctx, repoID, index)
diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go
index 46e9f339a5a53..9ef39425041f8 100644
--- a/routers/web/repo/issue_suggestions.go
+++ b/routers/web/repo/issue_suggestions.go
@@ -6,13 +6,10 @@ package repo
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models/db"
-	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/unit"
-	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 	"code.gitea.io/gitea/modules/optional"
-	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/services/context"
+	issue_service "code.gitea.io/gitea/services/issue"
 )
 
 // IssueSuggestions returns a list of issue suggestions
@@ -29,54 +26,11 @@ func IssueSuggestions(ctx *context.Context) {
 		isPull = optional.Some(false)
 	}
 
-	searchOpt := &issue_indexer.SearchOptions{
-		Paginator: &db.ListOptions{
-			Page:     0,
-			PageSize: 5,
-		},
-		Keyword:  keyword,
-		RepoIDs:  []int64{ctx.Repo.Repository.ID},
-		IsPull:   isPull,
-		IsClosed: nil,
-		SortBy:   issue_indexer.SortByUpdatedDesc,
-	}
-
-	ids, _, err := issue_indexer.SearchIssues(ctx, searchOpt)
-	if err != nil {
-		ctx.ServerError("SearchIssues", err)
-		return
-	}
-	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
+	suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword)
 	if err != nil {
-		ctx.ServerError("FindIssuesByIDs", err)
+		ctx.ServerError("GetSuggestion", err)
 		return
 	}
 
-	suggestions := make([]*structs.Issue, 0, len(issues))
-
-	for _, issue := range issues {
-		suggestion := &structs.Issue{
-			ID:    issue.ID,
-			Index: issue.Index,
-			Title: issue.Title,
-			State: issue.State(),
-		}
-
-		if issue.IsPull {
-			if err := issue.LoadPullRequest(ctx); err != nil {
-				ctx.ServerError("LoadPullRequest", err)
-				return
-			}
-			if issue.PullRequest != nil {
-				suggestion.PullRequest = &structs.PullRequestMeta{
-					HasMerged:        issue.PullRequest.HasMerged,
-					IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
-				}
-			}
-		}
-
-		suggestions = append(suggestions, suggestion)
-	}
-
 	ctx.JSON(http.StatusOK, suggestions)
 }
diff --git a/services/issue/suggestion.go b/services/issue/suggestion.go
new file mode 100644
index 0000000000000..22eddb19042bb
--- /dev/null
+++ b/services/issue/suggestion.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"context"
+	"strconv"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/optional"
+	"code.gitea.io/gitea/modules/structs"
+)
+
+func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) {
+	var issues issues_model.IssueList
+	var err error
+	pageSize := 5
+	if keyword == "" {
+		issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		indexKeyword, _ := strconv.ParseInt(keyword, 10, 64)
+		var issueByIndex *issues_model.Issue
+		var excludedID int64
+		if indexKeyword > 0 {
+			issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword)
+			if err != nil && !issues_model.IsErrIssueNotExist(err) {
+				return nil, err
+			}
+			if issueByIndex != nil {
+				excludedID = issueByIndex.ID
+				pageSize--
+			}
+		}
+
+		issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize)
+		if err != nil {
+			return nil, err
+		}
+
+		if issueByIndex != nil {
+			issues = append([]*issues_model.Issue{issueByIndex}, issues...)
+		}
+	}
+
+	if err := issues.LoadPullRequests(ctx); err != nil {
+		return nil, err
+	}
+
+	suggestions := make([]*structs.Issue, 0, len(issues))
+	for _, issue := range issues {
+		suggestion := &structs.Issue{
+			ID:    issue.ID,
+			Index: issue.Index,
+			Title: issue.Title,
+			State: issue.State(),
+		}
+
+		if issue.IsPull && issue.PullRequest != nil {
+			suggestion.PullRequest = &structs.PullRequestMeta{
+				HasMerged:        issue.PullRequest.HasMerged,
+				IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
+			}
+		}
+		suggestions = append(suggestions, suggestion)
+	}
+
+	return suggestions, nil
+}
diff --git a/services/issue/suggestion_test.go b/services/issue/suggestion_test.go
new file mode 100644
index 0000000000000..84cfd520ac40a
--- /dev/null
+++ b/services/issue/suggestion_test.go
@@ -0,0 +1,57 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/optional"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_Suggestion(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+	testCases := []struct {
+		keyword         string
+		isPull          optional.Option[bool]
+		expectedIndexes []int64
+	}{
+		{
+			keyword:         "",
+			expectedIndexes: []int64{5, 1, 4, 2, 3},
+		},
+		{
+			keyword:         "1",
+			expectedIndexes: []int64{1},
+		},
+		{
+			keyword:         "issue",
+			expectedIndexes: []int64{4, 1, 2, 3},
+		},
+		{
+			keyword:         "pull",
+			expectedIndexes: []int64{5},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.keyword, func(t *testing.T) {
+			issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword)
+			assert.NoError(t, err)
+
+			issueIndexes := make([]int64, 0, len(issues))
+			for _, issue := range issues {
+				issueIndexes = append(issueIndexes, issue.Index)
+			}
+			assert.EqualValues(t, testCase.expectedIndexes, issueIndexes)
+		})
+	}
+}

From 7c17d0a73e5b93843824d87d0b70206959c1776f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 10 Feb 2025 19:52:09 -0800
Subject: [PATCH 106/206] Enhance routers for the Actions variable operations
 (#33547) (#33553)

Backport #33547

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/actions/variable.go                  |  22 ++-
 routers/api/v1/org/action.go                |   6 +-
 routers/api/v1/repo/action.go               |   6 +-
 routers/api/v1/user/action.go               |   6 +-
 routers/web/repo/setting/variables.go       | 140 ---------------
 routers/web/shared/actions/variables.go     | 186 ++++++++++++++++++--
 routers/web/web.go                          |   9 +-
 services/actions/variables.go               |  15 +-
 tests/integration/actions_variables_test.go | 149 ++++++++++++++++
 9 files changed, 363 insertions(+), 176 deletions(-)
 delete mode 100644 routers/web/repo/setting/variables.go
 create mode 100644 tests/integration/actions_variables_test.go

diff --git a/models/actions/variable.go b/models/actions/variable.go
index d0f917d923280..163bb12c9360c 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin
 
 type FindVariablesOpts struct {
 	db.ListOptions
+	IDs     []int64
 	RepoID  int64
 	OwnerID int64 // it will be ignored if RepoID is set
 	Name    string
@@ -65,6 +66,15 @@ type FindVariablesOpts struct {
 
 func (opts FindVariablesOpts) ToConds() builder.Cond {
 	cond := builder.NewCond()
+
+	if len(opts.IDs) > 0 {
+		if len(opts.IDs) == 1 {
+			cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+		} else {
+			cond = cond.And(builder.In("id", opts.IDs))
+		}
+	}
+
 	// Since we now support instance-level variables,
 	// there is no need to check for null values for `owner_id` and `repo_id`
 	cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
@@ -85,12 +95,12 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
 	return db.Find[ActionVariable](ctx, opts)
 }
 
-func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
-	count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
-		Update(&ActionVariable{
-			Name: variable.Name,
-			Data: variable.Data,
-		})
+func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
+	variable.Name = strings.ToUpper(variable.Name)
+	count, err := db.GetEngine(ctx).
+		ID(variable.ID).
+		Cols(cols...).
+		Update(variable)
 	return count != 0, err
 }
 
diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go
index 199ee7d7773b9..05919c523439b 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -450,7 +450,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
 	if opt.Name == "" {
 		opt.Name = ctx.PathParam("variablename")
 	}
-	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+	v.Name = opt.Name
+	v.Data = opt.Value
+
+	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
 		} else {
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index d27e8d2427b39..8f464c8dbd9c3 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -414,7 +414,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
 	if opt.Name == "" {
 		opt.Name = ctx.PathParam("variablename")
 	}
-	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+	v.Name = opt.Name
+	v.Data = opt.Value
+
+	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
 		} else {
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index 22707196f4cca..baa4b3b81ec74 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -212,7 +212,11 @@ func UpdateVariable(ctx *context.APIContext) {
 	if opt.Name == "" {
 		opt.Name = ctx.PathParam("variablename")
 	}
-	if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+	v.Name = opt.Name
+	v.Data = opt.Value
+
+	if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
 		if errors.Is(err, util.ErrInvalidArgument) {
 			ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
 		} else {
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
deleted file mode 100644
index cc2e619f66248..0000000000000
--- a/routers/web/repo/setting/variables.go
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package setting
-
-import (
-	"errors"
-	"net/http"
-
-	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/setting"
-	shared "code.gitea.io/gitea/routers/web/shared/actions"
-	shared_user "code.gitea.io/gitea/routers/web/shared/user"
-	"code.gitea.io/gitea/services/context"
-)
-
-const (
-	tplRepoVariables  base.TplName = "repo/settings/actions"
-	tplOrgVariables   base.TplName = "org/settings/actions"
-	tplUserVariables  base.TplName = "user/settings/actions"
-	tplAdminVariables base.TplName = "admin/actions"
-)
-
-type variablesCtx struct {
-	OwnerID           int64
-	RepoID            int64
-	IsRepo            bool
-	IsOrg             bool
-	IsUser            bool
-	IsGlobal          bool
-	VariablesTemplate base.TplName
-	RedirectLink      string
-}
-
-func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
-	if ctx.Data["PageIsRepoSettings"] == true {
-		return &variablesCtx{
-			OwnerID:           0,
-			RepoID:            ctx.Repo.Repository.ID,
-			IsRepo:            true,
-			VariablesTemplate: tplRepoVariables,
-			RedirectLink:      ctx.Repo.RepoLink + "/settings/actions/variables",
-		}, nil
-	}
-
-	if ctx.Data["PageIsOrgSettings"] == true {
-		err := shared_user.LoadHeaderCount(ctx)
-		if err != nil {
-			ctx.ServerError("LoadHeaderCount", err)
-			return nil, nil
-		}
-		return &variablesCtx{
-			OwnerID:           ctx.ContextUser.ID,
-			RepoID:            0,
-			IsOrg:             true,
-			VariablesTemplate: tplOrgVariables,
-			RedirectLink:      ctx.Org.OrgLink + "/settings/actions/variables",
-		}, nil
-	}
-
-	if ctx.Data["PageIsUserSettings"] == true {
-		return &variablesCtx{
-			OwnerID:           ctx.Doer.ID,
-			RepoID:            0,
-			IsUser:            true,
-			VariablesTemplate: tplUserVariables,
-			RedirectLink:      setting.AppSubURL + "/user/settings/actions/variables",
-		}, nil
-	}
-
-	if ctx.Data["PageIsAdmin"] == true {
-		return &variablesCtx{
-			OwnerID:           0,
-			RepoID:            0,
-			IsGlobal:          true,
-			VariablesTemplate: tplAdminVariables,
-			RedirectLink:      setting.AppSubURL + "/-/admin/actions/variables",
-		}, nil
-	}
-
-	return nil, errors.New("unable to set Variables context")
-}
-
-func Variables(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("actions.variables")
-	ctx.Data["PageType"] = "variables"
-	ctx.Data["PageIsSharedSettingsVariables"] = true
-
-	vCtx, err := getVariablesCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getVariablesCtx", err)
-		return
-	}
-
-	shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID)
-	if ctx.Written() {
-		return
-	}
-
-	ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
-}
-
-func VariableCreate(ctx *context.Context) {
-	vCtx, err := getVariablesCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getVariablesCtx", err)
-		return
-	}
-
-	if ctx.HasError() { // form binding validation error
-		ctx.JSONError(ctx.GetErrMsg())
-		return
-	}
-
-	shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink)
-}
-
-func VariableUpdate(ctx *context.Context) {
-	vCtx, err := getVariablesCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getVariablesCtx", err)
-		return
-	}
-
-	if ctx.HasError() { // form binding validation error
-		ctx.JSONError(ctx.GetErrMsg())
-		return
-	}
-
-	shared.UpdateVariable(ctx, vCtx.RedirectLink)
-}
-
-func VariableDelete(ctx *context.Context) {
-	vCtx, err := getVariablesCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getVariablesCtx", err)
-		return
-	}
-	shared.DeleteVariable(ctx, vCtx.RedirectLink)
-}
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index 5c5768243ab30..dfbd5f00eb41e 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,31 +4,127 @@
 package actions
 
 import (
+	"errors"
+	"net/http"
+
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	actions_service "code.gitea.io/gitea/services/actions"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
-func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
+const (
+	tplRepoVariables  base.TplName = "repo/settings/actions"
+	tplOrgVariables   base.TplName = "org/settings/actions"
+	tplUserVariables  base.TplName = "user/settings/actions"
+	tplAdminVariables base.TplName = "admin/actions"
+)
+
+type variablesCtx struct {
+	OwnerID           int64
+	RepoID            int64
+	IsRepo            bool
+	IsOrg             bool
+	IsUser            bool
+	IsGlobal          bool
+	VariablesTemplate base.TplName
+	RedirectLink      string
+}
+
+func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
+	if ctx.Data["PageIsRepoSettings"] == true {
+		return &variablesCtx{
+			OwnerID:           0,
+			RepoID:            ctx.Repo.Repository.ID,
+			IsRepo:            true,
+			VariablesTemplate: tplRepoVariables,
+			RedirectLink:      ctx.Repo.RepoLink + "/settings/actions/variables",
+		}, nil
+	}
+
+	if ctx.Data["PageIsOrgSettings"] == true {
+		err := shared_user.LoadHeaderCount(ctx)
+		if err != nil {
+			ctx.ServerError("LoadHeaderCount", err)
+			return nil, nil
+		}
+		return &variablesCtx{
+			OwnerID:           ctx.ContextUser.ID,
+			RepoID:            0,
+			IsOrg:             true,
+			VariablesTemplate: tplOrgVariables,
+			RedirectLink:      ctx.Org.OrgLink + "/settings/actions/variables",
+		}, nil
+	}
+
+	if ctx.Data["PageIsUserSettings"] == true {
+		return &variablesCtx{
+			OwnerID:           ctx.Doer.ID,
+			RepoID:            0,
+			IsUser:            true,
+			VariablesTemplate: tplUserVariables,
+			RedirectLink:      setting.AppSubURL + "/user/settings/actions/variables",
+		}, nil
+	}
+
+	if ctx.Data["PageIsAdmin"] == true {
+		return &variablesCtx{
+			OwnerID:           0,
+			RepoID:            0,
+			IsGlobal:          true,
+			VariablesTemplate: tplAdminVariables,
+			RedirectLink:      setting.AppSubURL + "/-/admin/actions/variables",
+		}, nil
+	}
+
+	return nil, errors.New("unable to set Variables context")
+}
+
+func Variables(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("actions.variables")
+	ctx.Data["PageType"] = "variables"
+	ctx.Data["PageIsSharedSettingsVariables"] = true
+
+	vCtx, err := getVariablesCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getVariablesCtx", err)
+		return
+	}
+
 	variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{
-		OwnerID: ownerID,
-		RepoID:  repoID,
+		OwnerID: vCtx.OwnerID,
+		RepoID:  vCtx.RepoID,
 	})
 	if err != nil {
 		ctx.ServerError("FindVariables", err)
 		return
 	}
 	ctx.Data["Variables"] = variables
+
+	ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
 }
 
-func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
+func VariableCreate(ctx *context.Context) {
+	vCtx, err := getVariablesCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getVariablesCtx", err)
+		return
+	}
+
+	if ctx.HasError() { // form binding validation error
+		ctx.JSONError(ctx.GetErrMsg())
+		return
+	}
+
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
 
-	v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
+	v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data)
 	if err != nil {
 		log.Error("CreateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
@@ -36,30 +132,92 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str
 	}
 
 	ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
-	ctx.JSONRedirect(redirectURL)
+	ctx.JSONRedirect(vCtx.RedirectLink)
 }
 
-func UpdateVariable(ctx *context.Context, redirectURL string) {
-	id := ctx.PathParamInt64(":variable_id")
+func VariableUpdate(ctx *context.Context) {
+	vCtx, err := getVariablesCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getVariablesCtx", err)
+		return
+	}
+
+	if ctx.HasError() { // form binding validation error
+		ctx.JSONError(ctx.GetErrMsg())
+		return
+	}
+
+	id := ctx.PathParamInt64("variable_id")
+
+	variable := findActionsVariable(ctx, id, vCtx)
+	if ctx.Written() {
+		return
+	}
+
 	form := web.GetForm(ctx).(*forms.EditVariableForm)
+	variable.Name = form.Name
+	variable.Data = form.Data
 
-	if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+	if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
 		log.Error("UpdateVariable: %v", err)
 		ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
 		return
 	}
 	ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
-	ctx.JSONRedirect(redirectURL)
+	ctx.JSONRedirect(vCtx.RedirectLink)
 }
 
-func DeleteVariable(ctx *context.Context, redirectURL string) {
-	id := ctx.PathParamInt64(":variable_id")
+func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable {
+	opts := actions_model.FindVariablesOpts{
+		IDs: []int64{id},
+	}
+	switch {
+	case vCtx.IsRepo:
+		opts.RepoID = vCtx.RepoID
+		if opts.RepoID == 0 {
+			panic("RepoID is 0")
+		}
+	case vCtx.IsOrg, vCtx.IsUser:
+		opts.OwnerID = vCtx.OwnerID
+		if opts.OwnerID == 0 {
+			panic("OwnerID is 0")
+		}
+	case vCtx.IsGlobal:
+		// do nothing
+	default:
+		panic("invalid actions variable")
+	}
+
+	got, err := actions_model.FindVariables(ctx, opts)
+	if err != nil {
+		ctx.ServerError("FindVariables", err)
+		return nil
+	} else if len(got) == 0 {
+		ctx.NotFound("FindVariables", nil)
+		return nil
+	}
+	return got[0]
+}
+
+func VariableDelete(ctx *context.Context) {
+	vCtx, err := getVariablesCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getVariablesCtx", err)
+		return
+	}
+
+	id := ctx.PathParamInt64("variable_id")
+
+	variable := findActionsVariable(ctx, id, vCtx)
+	if ctx.Written() {
+		return
+	}
 
-	if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
+	if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil {
 		log.Error("Delete variable [%d] failed: %v", id, err)
 		ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
 		return
 	}
 	ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
-	ctx.JSONRedirect(redirectURL)
+	ctx.JSONRedirect(vCtx.RedirectLink)
 }
diff --git a/routers/web/web.go b/routers/web/web.go
index b6c2fe8b4b95d..bf9f50139f4d1 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -37,6 +37,7 @@ import (
 	"code.gitea.io/gitea/routers/web/repo"
 	"code.gitea.io/gitea/routers/web/repo/actions"
 	repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
+	shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
 	"code.gitea.io/gitea/routers/web/shared/project"
 	"code.gitea.io/gitea/routers/web/user"
 	user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -442,10 +443,10 @@ func registerRoutes(m *web.Router) {
 
 	addSettingsVariablesRoutes := func() {
 		m.Group("/variables", func() {
-			m.Get("", repo_setting.Variables)
-			m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate)
-			m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate)
-			m.Post("/{variable_id}/delete", repo_setting.VariableDelete)
+			m.Get("", shared_actions.Variables)
+			m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate)
+			m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate)
+			m.Post("/{variable_id}/delete", shared_actions.VariableDelete)
 		})
 	}
 
diff --git a/services/actions/variables.go b/services/actions/variables.go
index 8dde9c4af5c16..95f088dbd334e 100644
--- a/services/actions/variables.go
+++ b/services/actions/variables.go
@@ -6,7 +6,6 @@ package actions
 import (
 	"context"
 	"regexp"
-	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/modules/log"
@@ -31,20 +30,18 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
 	return v, nil
 }
 
-func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
-	if err := secret_service.ValidateName(name); err != nil {
+func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) {
+	if err := secret_service.ValidateName(variable.Name); err != nil {
 		return false, err
 	}
 
-	if err := envNameCIRegexMatch(name); err != nil {
+	if err := envNameCIRegexMatch(variable.Name); err != nil {
 		return false, err
 	}
 
-	return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
-		ID:   variableID,
-		Name: strings.ToUpper(name),
-		Data: util.ReserveLineBreakForTextarea(data),
-	})
+	variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
+
+	return actions_model.UpdateVariableCols(ctx, variable, "name", "data")
 }
 
 func DeleteVariableByID(ctx context.Context, variableID int64) error {
diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go
new file mode 100644
index 0000000000000..12c1c3f628fd6
--- /dev/null
+++ b/tests/integration/actions_variables_test.go
@@ -0,0 +1,149 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestActionsVariables(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	ctx := context.Background()
+
+	require.NoError(t, db.DeleteAllRecords("action_variable"))
+
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	_, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var")
+	user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
+	userWebURL := "/user/settings/actions/variables"
+
+	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+	_, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var")
+	org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
+	orgWebURL := "/org/org3/settings/actions/variables"
+
+	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	_, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var")
+	repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
+	repoWebURL := "/user2/repo1/settings/actions/variables"
+
+	_, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var")
+	globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
+	adminWebURL := "/-/admin/actions/variables"
+
+	sessionAdmin := loginUser(t, "user1")
+	sessionUser2 := loginUser(t, user2.Name)
+
+	doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
+			"_csrf": GetUserCSRFToken(t, sess),
+			"name":  "VAR",
+			"data":  data,
+		})
+		sess.MakeRequest(t, req, expectedStatus)
+	}
+
+	doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
+			"_csrf": GetUserCSRFToken(t, sess),
+		})
+		sess.MakeRequest(t, req, expectedStatus)
+	}
+
+	assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+		doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusNotFound)
+		doDelete(t, sess, baseURL, id, http.StatusNotFound)
+		v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
+		assert.Contains(t, v.Data, "-var")
+	}
+
+	assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+		doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusOK)
+		v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
+		assert.Equal(t, "ChangedData", v.Data)
+		doDelete(t, sess, baseURL, id, http.StatusOK)
+		unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{ID: id})
+	}
+
+	t.Run("UpdateUserVar", func(t *testing.T) {
+		theVar := user2Var
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+		})
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+		})
+	})
+
+	t.Run("UpdateOrgVar", func(t *testing.T) {
+		theVar := org3Var
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+		})
+	})
+
+	t.Run("UpdateRepoVar", func(t *testing.T) {
+		theVar := repo1Var
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+		})
+	})
+
+	t.Run("UpdateGlobalVar", func(t *testing.T) {
+		theVar := globalVar
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+		})
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+		})
+	})
+
+	t.Run("UpdateSuccess", func(t *testing.T) {
+		t.Run("User", func(t *testing.T) {
+			assertSuccess(t, sessionUser2, userWebURL, user2Var.ID)
+		})
+		t.Run("Org", func(t *testing.T) {
+			assertSuccess(t, sessionAdmin, orgWebURL, org3Var.ID)
+		})
+		t.Run("Repo", func(t *testing.T) {
+			assertSuccess(t, sessionUser2, repoWebURL, repo1Var.ID)
+		})
+		t.Run("Admin", func(t *testing.T) {
+			assertSuccess(t, sessionAdmin, adminWebURL, globalVar.ID)
+		})
+	})
+}

From 7794ff087419b8a304203f63432f4159806537fa Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 10 Feb 2025 23:25:56 -0800
Subject: [PATCH 107/206] Enhance routers for the Actions runner operations
 (#33549) (#33555)

Backport #33549

- Find the runner before deleting
- Move the main logic from `routers/web/repo/setting/runners.go` to
`routers/web/shared/actions/runners.go`.

Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/actions/runner.go                      |   9 +
 routers/web/repo/setting/runners.go           | 187 --------------
 routers/web/shared/actions/runners.go         | 235 ++++++++++++++++--
 routers/web/web.go                            |  12 +-
 .../integration/actions_runner_modify_test.go | 151 +++++++++++
 5 files changed, 387 insertions(+), 207 deletions(-)
 delete mode 100644 routers/web/repo/setting/runners.go
 create mode 100644 tests/integration/actions_runner_modify_test.go

diff --git a/models/actions/runner.go b/models/actions/runner.go
index b35a76680c2cb..c4e5f9d1219f5 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -167,6 +167,7 @@ func init() {
 
 type FindRunnerOptions struct {
 	db.ListOptions
+	IDs           []int64
 	RepoID        int64
 	OwnerID       int64 // it will be ignored if RepoID is set
 	Sort          string
@@ -178,6 +179,14 @@ type FindRunnerOptions struct {
 func (opts FindRunnerOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
 
+	if len(opts.IDs) > 0 {
+		if len(opts.IDs) == 1 {
+			cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+		} else {
+			cond = cond.And(builder.In("id", opts.IDs))
+		}
+	}
+
 	if opts.RepoID > 0 {
 		c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
 		if opts.WithAvailable {
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
deleted file mode 100644
index 3141d8f42ad4d..0000000000000
--- a/routers/web/repo/setting/runners.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package setting
-
-import (
-	"errors"
-	"net/http"
-	"net/url"
-
-	actions_model "code.gitea.io/gitea/models/actions"
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/setting"
-	actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
-	shared_user "code.gitea.io/gitea/routers/web/shared/user"
-	"code.gitea.io/gitea/services/context"
-)
-
-const (
-	// TODO: Separate secrets from runners when layout is ready
-	tplRepoRunners     base.TplName = "repo/settings/actions"
-	tplOrgRunners      base.TplName = "org/settings/actions"
-	tplAdminRunners    base.TplName = "admin/actions"
-	tplUserRunners     base.TplName = "user/settings/actions"
-	tplRepoRunnerEdit  base.TplName = "repo/settings/runner_edit"
-	tplOrgRunnerEdit   base.TplName = "org/settings/runners_edit"
-	tplAdminRunnerEdit base.TplName = "admin/runners/edit"
-	tplUserRunnerEdit  base.TplName = "user/settings/runner_edit"
-)
-
-type runnersCtx struct {
-	OwnerID            int64
-	RepoID             int64
-	IsRepo             bool
-	IsOrg              bool
-	IsAdmin            bool
-	IsUser             bool
-	RunnersTemplate    base.TplName
-	RunnerEditTemplate base.TplName
-	RedirectLink       string
-}
-
-func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
-	if ctx.Data["PageIsRepoSettings"] == true {
-		return &runnersCtx{
-			RepoID:             ctx.Repo.Repository.ID,
-			OwnerID:            0,
-			IsRepo:             true,
-			RunnersTemplate:    tplRepoRunners,
-			RunnerEditTemplate: tplRepoRunnerEdit,
-			RedirectLink:       ctx.Repo.RepoLink + "/settings/actions/runners/",
-		}, nil
-	}
-
-	if ctx.Data["PageIsOrgSettings"] == true {
-		err := shared_user.LoadHeaderCount(ctx)
-		if err != nil {
-			ctx.ServerError("LoadHeaderCount", err)
-			return nil, nil
-		}
-		return &runnersCtx{
-			RepoID:             0,
-			OwnerID:            ctx.Org.Organization.ID,
-			IsOrg:              true,
-			RunnersTemplate:    tplOrgRunners,
-			RunnerEditTemplate: tplOrgRunnerEdit,
-			RedirectLink:       ctx.Org.OrgLink + "/settings/actions/runners/",
-		}, nil
-	}
-
-	if ctx.Data["PageIsAdmin"] == true {
-		return &runnersCtx{
-			RepoID:             0,
-			OwnerID:            0,
-			IsAdmin:            true,
-			RunnersTemplate:    tplAdminRunners,
-			RunnerEditTemplate: tplAdminRunnerEdit,
-			RedirectLink:       setting.AppSubURL + "/-/admin/actions/runners/",
-		}, nil
-	}
-
-	if ctx.Data["PageIsUserSettings"] == true {
-		return &runnersCtx{
-			OwnerID:            ctx.Doer.ID,
-			RepoID:             0,
-			IsUser:             true,
-			RunnersTemplate:    tplUserRunners,
-			RunnerEditTemplate: tplUserRunnerEdit,
-			RedirectLink:       setting.AppSubURL + "/user/settings/actions/runners/",
-		}, nil
-	}
-
-	return nil, errors.New("unable to set Runners context")
-}
-
-// Runners render settings/actions/runners page for repo level
-func Runners(ctx *context.Context) {
-	ctx.Data["PageIsSharedSettingsRunners"] = true
-	ctx.Data["Title"] = ctx.Tr("actions.actions")
-	ctx.Data["PageType"] = "runners"
-
-	rCtx, err := getRunnersCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getRunnersCtx", err)
-		return
-	}
-
-	page := ctx.FormInt("page")
-	if page <= 1 {
-		page = 1
-	}
-
-	opts := actions_model.FindRunnerOptions{
-		ListOptions: db.ListOptions{
-			Page:     page,
-			PageSize: 100,
-		},
-		Sort:   ctx.Req.URL.Query().Get("sort"),
-		Filter: ctx.Req.URL.Query().Get("q"),
-	}
-	if rCtx.IsRepo {
-		opts.RepoID = rCtx.RepoID
-		opts.WithAvailable = true
-	} else if rCtx.IsOrg || rCtx.IsUser {
-		opts.OwnerID = rCtx.OwnerID
-		opts.WithAvailable = true
-	}
-	actions_shared.RunnersList(ctx, opts)
-
-	ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
-}
-
-// RunnersEdit renders runner edit page for repository level
-func RunnersEdit(ctx *context.Context) {
-	ctx.Data["PageIsSharedSettingsRunners"] = true
-	ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
-	rCtx, err := getRunnersCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getRunnersCtx", err)
-		return
-	}
-
-	page := ctx.FormInt("page")
-	if page <= 1 {
-		page = 1
-	}
-
-	actions_shared.RunnerDetails(ctx, page,
-		ctx.PathParamInt64(":runnerid"), rCtx.OwnerID, rCtx.RepoID,
-	)
-	ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
-}
-
-func RunnersEditPost(ctx *context.Context) {
-	rCtx, err := getRunnersCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getRunnersCtx", err)
-		return
-	}
-	actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64(":runnerid"),
-		rCtx.OwnerID, rCtx.RepoID,
-		rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid")))
-}
-
-func ResetRunnerRegistrationToken(ctx *context.Context) {
-	rCtx, err := getRunnersCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getRunnersCtx", err)
-		return
-	}
-	actions_shared.RunnerResetRegistrationToken(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink)
-}
-
-// RunnerDeletePost response for deleting runner
-func RunnerDeletePost(ctx *context.Context) {
-	rCtx, err := getRunnersCtx(ctx)
-	if err != nil {
-		ctx.ServerError("getRunnersCtx", err)
-		return
-	}
-	actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64(":runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam(":runnerid")))
-}
-
-func RedirectToDefaultSetting(ctx *context.Context) {
-	ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
-}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 6d77bdd2fab74..2266232ebe976 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -5,18 +5,131 @@ package actions
 
 import (
 	"errors"
+	"net/http"
+	"net/url"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	shared_user "code.gitea.io/gitea/routers/web/shared/user"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/services/forms"
 )
 
-// RunnersList prepares data for runners list
-func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
+const (
+	// TODO: Separate secrets from runners when layout is ready
+	tplRepoRunners     base.TplName = "repo/settings/actions"
+	tplOrgRunners      base.TplName = "org/settings/actions"
+	tplAdminRunners    base.TplName = "admin/actions"
+	tplUserRunners     base.TplName = "user/settings/actions"
+	tplRepoRunnerEdit  base.TplName = "repo/settings/runner_edit"
+	tplOrgRunnerEdit   base.TplName = "org/settings/runners_edit"
+	tplAdminRunnerEdit base.TplName = "admin/runners/edit"
+	tplUserRunnerEdit  base.TplName = "user/settings/runner_edit"
+)
+
+type runnersCtx struct {
+	OwnerID            int64
+	RepoID             int64
+	IsRepo             bool
+	IsOrg              bool
+	IsAdmin            bool
+	IsUser             bool
+	RunnersTemplate    base.TplName
+	RunnerEditTemplate base.TplName
+	RedirectLink       string
+}
+
+func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
+	if ctx.Data["PageIsRepoSettings"] == true {
+		return &runnersCtx{
+			RepoID:             ctx.Repo.Repository.ID,
+			OwnerID:            0,
+			IsRepo:             true,
+			RunnersTemplate:    tplRepoRunners,
+			RunnerEditTemplate: tplRepoRunnerEdit,
+			RedirectLink:       ctx.Repo.RepoLink + "/settings/actions/runners/",
+		}, nil
+	}
+
+	if ctx.Data["PageIsOrgSettings"] == true {
+		err := shared_user.LoadHeaderCount(ctx)
+		if err != nil {
+			ctx.ServerError("LoadHeaderCount", err)
+			return nil, nil
+		}
+		return &runnersCtx{
+			RepoID:             0,
+			OwnerID:            ctx.Org.Organization.ID,
+			IsOrg:              true,
+			RunnersTemplate:    tplOrgRunners,
+			RunnerEditTemplate: tplOrgRunnerEdit,
+			RedirectLink:       ctx.Org.OrgLink + "/settings/actions/runners/",
+		}, nil
+	}
+
+	if ctx.Data["PageIsAdmin"] == true {
+		return &runnersCtx{
+			RepoID:             0,
+			OwnerID:            0,
+			IsAdmin:            true,
+			RunnersTemplate:    tplAdminRunners,
+			RunnerEditTemplate: tplAdminRunnerEdit,
+			RedirectLink:       setting.AppSubURL + "/-/admin/actions/runners/",
+		}, nil
+	}
+
+	if ctx.Data["PageIsUserSettings"] == true {
+		return &runnersCtx{
+			OwnerID:            ctx.Doer.ID,
+			RepoID:             0,
+			IsUser:             true,
+			RunnersTemplate:    tplUserRunners,
+			RunnerEditTemplate: tplUserRunnerEdit,
+			RedirectLink:       setting.AppSubURL + "/user/settings/actions/runners/",
+		}, nil
+	}
+
+	return nil, errors.New("unable to set Runners context")
+}
+
+// Runners render settings/actions/runners page for repo level
+func Runners(ctx *context.Context) {
+	ctx.Data["PageIsSharedSettingsRunners"] = true
+	ctx.Data["Title"] = ctx.Tr("actions.actions")
+	ctx.Data["PageType"] = "runners"
+
+	rCtx, err := getRunnersCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getRunnersCtx", err)
+		return
+	}
+
+	page := ctx.FormInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	opts := actions_model.FindRunnerOptions{
+		ListOptions: db.ListOptions{
+			Page:     page,
+			PageSize: 100,
+		},
+		Sort:   ctx.Req.URL.Query().Get("sort"),
+		Filter: ctx.Req.URL.Query().Get("q"),
+	}
+	if rCtx.IsRepo {
+		opts.RepoID = rCtx.RepoID
+		opts.WithAvailable = true
+	} else if rCtx.IsOrg || rCtx.IsUser {
+		opts.OwnerID = rCtx.OwnerID
+		opts.WithAvailable = true
+	}
+
 	runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
 	if err != nil {
 		ctx.ServerError("CountRunners", err)
@@ -53,10 +166,29 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
 	pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
 
 	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
 }
 
-// RunnerDetails prepares data for runners edit page
-func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int64) {
+// RunnersEdit renders runner edit page for repository level
+func RunnersEdit(ctx *context.Context) {
+	ctx.Data["PageIsSharedSettingsRunners"] = true
+	ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
+	rCtx, err := getRunnersCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getRunnersCtx", err)
+		return
+	}
+
+	page := ctx.FormInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	runnerID := ctx.PathParamInt64("runnerid")
+	ownerID := rCtx.OwnerID
+	repoID := rCtx.RepoID
+
 	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
 	if err != nil {
 		ctx.ServerError("GetRunnerByID", err)
@@ -97,10 +229,22 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
 	ctx.Data["Tasks"] = tasks
 	pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
 	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
 }
 
-// RunnerDetailsEditPost response for edit runner details
-func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) {
+func RunnersEditPost(ctx *context.Context) {
+	rCtx, err := getRunnersCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getRunnersCtx", err)
+		return
+	}
+
+	runnerID := ctx.PathParamInt64("runnerid")
+	ownerID := rCtx.OwnerID
+	repoID := rCtx.RepoID
+	redirectTo := rCtx.RedirectLink
+
 	runner, err := actions_model.GetRunnerByID(ctx, runnerID)
 	if err != nil {
 		log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
@@ -129,10 +273,18 @@ func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64
 	ctx.Redirect(redirectTo)
 }
 
-// RunnerResetRegistrationToken reset registration token
-func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) {
-	_, err := actions_model.NewRunnerToken(ctx, ownerID, repoID)
+func ResetRunnerRegistrationToken(ctx *context.Context) {
+	rCtx, err := getRunnersCtx(ctx)
 	if err != nil {
+		ctx.ServerError("getRunnersCtx", err)
+		return
+	}
+
+	ownerID := rCtx.OwnerID
+	repoID := rCtx.RepoID
+	redirectTo := rCtx.RedirectLink
+
+	if _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID); err != nil {
 		ctx.ServerError("ResetRunnerRegistrationToken", err)
 		return
 	}
@@ -140,11 +292,28 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
 	ctx.JSONRedirect(redirectTo)
 }
 
-// RunnerDeletePost response for deleting a runner
-func RunnerDeletePost(ctx *context.Context, runnerID int64,
-	successRedirectTo, failedRedirectTo string,
-) {
-	if err := actions_model.DeleteRunner(ctx, runnerID); err != nil {
+// RunnerDeletePost response for deleting runner
+func RunnerDeletePost(ctx *context.Context) {
+	rCtx, err := getRunnersCtx(ctx)
+	if err != nil {
+		ctx.ServerError("getRunnersCtx", err)
+		return
+	}
+
+	runner := findActionsRunner(ctx, rCtx)
+	if ctx.Written() {
+		return
+	}
+
+	if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) {
+		ctx.NotFound("RunnerDeletePost", util.NewPermissionDeniedErrorf("no permission to delete this runner"))
+		return
+	}
+
+	successRedirectTo := rCtx.RedirectLink
+	failedRedirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
+
+	if err := actions_model.DeleteRunner(ctx, runner.ID); err != nil {
 		log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
 		ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed"))
 
@@ -158,3 +327,41 @@ func RunnerDeletePost(ctx *context.Context, runnerID int64,
 
 	ctx.JSONRedirect(successRedirectTo)
 }
+
+func RedirectToDefaultSetting(ctx *context.Context) {
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
+}
+
+func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner {
+	runnerID := ctx.PathParamInt64("runnerid")
+	opts := &actions_model.FindRunnerOptions{
+		IDs: []int64{runnerID},
+	}
+	switch {
+	case rCtx.IsRepo:
+		opts.RepoID = rCtx.RepoID
+		if opts.RepoID == 0 {
+			panic("repoID is 0")
+		}
+	case rCtx.IsOrg, rCtx.IsUser:
+		opts.OwnerID = rCtx.OwnerID
+		if opts.OwnerID == 0 {
+			panic("ownerID is 0")
+		}
+	case rCtx.IsAdmin:
+		// do nothing
+	default:
+		panic("invalid actions runner context")
+	}
+
+	got, err := db.Find[actions_model.ActionRunner](ctx, opts)
+	if err != nil {
+		ctx.ServerError("FindRunner", err)
+		return nil
+	} else if len(got) == 0 {
+		ctx.NotFound("FindRunner", errors.New("runner not found"))
+		return nil
+	}
+
+	return got[0]
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index bf9f50139f4d1..703554489753b 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -460,11 +460,11 @@ func registerRoutes(m *web.Router) {
 
 	addSettingsRunnersRoutes := func() {
 		m.Group("/runners", func() {
-			m.Get("", repo_setting.Runners)
-			m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
-				Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
-			m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
-			m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
+			m.Get("", shared_actions.Runners)
+			m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
+				Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
+			m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
+			m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
 		})
 	}
 
@@ -1133,7 +1133,7 @@ func registerRoutes(m *web.Router) {
 			})
 		})
 		m.Group("/actions", func() {
-			m.Get("", repo_setting.RedirectToDefaultSetting)
+			m.Get("", shared_actions.RedirectToDefaultSetting)
 			addSettingsRunnersRoutes()
 			addSettingsSecretsRoutes()
 			addSettingsVariablesRoutes()
diff --git a/tests/integration/actions_runner_modify_test.go b/tests/integration/actions_runner_modify_test.go
new file mode 100644
index 0000000000000..feb3bc0893fb2
--- /dev/null
+++ b/tests/integration/actions_runner_modify_test.go
@@ -0,0 +1,151 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	actions_model "code.gitea.io/gitea/models/actions"
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestActionsRunnerModify(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	ctx := context.Background()
+
+	require.NoError(t, db.DeleteAllRecords("action_runner"))
+
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner", TokenHash: "a", UUID: "a"})
+	user2Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner"})
+	userWebURL := "/user/settings/actions/runners"
+
+	org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+	require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner", TokenHash: "b", UUID: "b"}))
+	org3Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner"})
+	orgWebURL := "/org/org3/settings/actions/runners"
+
+	repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+	_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner", TokenHash: "c", UUID: "c"})
+	repo1Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner"})
+	repoWebURL := "/user2/repo1/settings/actions/runners"
+
+	_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "global-runner", TokenHash: "d", UUID: "d"})
+	globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "global-runner"})
+	adminWebURL := "/-/admin/actions/runners"
+
+	sessionAdmin := loginUser(t, "user1")
+	sessionUser2 := loginUser(t, user2.Name)
+
+	doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
+			"_csrf":       GetUserCSRFToken(t, sess),
+			"description": description,
+		})
+		sess.MakeRequest(t, req, expectedStatus)
+	}
+
+	doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
+		req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
+			"_csrf": GetUserCSRFToken(t, sess),
+		})
+		sess.MakeRequest(t, req, expectedStatus)
+	}
+
+	assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+		doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
+		doDelete(t, sess, baseURL, id, http.StatusNotFound)
+		v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
+		assert.Empty(t, v.Description)
+	}
+
+	assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+		doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
+		v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
+		assert.Equal(t, "ChangedDescription", v.Description)
+		doDelete(t, sess, baseURL, id, http.StatusOK)
+		unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
+	}
+
+	t.Run("UpdateUserRunner", func(t *testing.T) {
+		theRunner := user2Runner
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+		})
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			t.Skip("Admin can update any runner (not right but not too bad)")
+			assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+		})
+	})
+
+	t.Run("UpdateOrgRunner", func(t *testing.T) {
+		theRunner := org3Runner
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			t.Skip("Admin can update any runner (not right but not too bad)")
+			assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+		})
+	})
+
+	t.Run("UpdateRepoRunner", func(t *testing.T) {
+		theRunner := repo1Runner
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+		})
+		t.Run("FromAdmin", func(t *testing.T) {
+			t.Skip("Admin can update any runner (not right but not too bad)")
+			assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+		})
+	})
+
+	t.Run("UpdateGlobalRunner", func(t *testing.T) {
+		theRunner := globalRunner
+		t.Run("FromOrg", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+		})
+		t.Run("FromUser", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+		})
+		t.Run("FromRepo", func(t *testing.T) {
+			assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+		})
+	})
+
+	t.Run("UpdateSuccess", func(t *testing.T) {
+		t.Run("User", func(t *testing.T) {
+			assertSuccess(t, sessionUser2, userWebURL, user2Runner.ID)
+		})
+		t.Run("Org", func(t *testing.T) {
+			assertSuccess(t, sessionAdmin, orgWebURL, org3Runner.ID)
+		})
+		t.Run("Repo", func(t *testing.T) {
+			assertSuccess(t, sessionUser2, repoWebURL, repo1Runner.ID)
+		})
+		t.Run("Admin", func(t *testing.T) {
+			assertSuccess(t, sessionAdmin, adminWebURL, globalRunner.ID)
+		})
+	})
+}

From 8fa39258744635e8c28f61b96aca27a762dfa36b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 11 Feb 2025 19:46:27 +0800
Subject: [PATCH 108/206] Fix context usage (#33554) (#33557)

Backport #33554
---
 routers/web/web.go                             | 2 +-
 services/auth/auth.go                          | 2 +-
 services/auth/sspi.go                          | 2 +-
 services/context/context.go                    | 7 ++++---
 services/markup/processorhelper.go             | 4 ++--
 services/markup/processorhelper_codepreview.go | 4 ++--
 6 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/routers/web/web.go b/routers/web/web.go
index 703554489753b..ae5f51d40385c 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1635,7 +1635,7 @@ func registerRoutes(m *web.Router) {
 	}
 
 	m.NotFound(func(w http.ResponseWriter, req *http.Request) {
-		ctx := context.GetWebContext(req)
+		ctx := context.GetWebContext(req.Context())
 		routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
 		ctx.NotFound("", nil)
 	})
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 43ff95f05302e..5e186d7e69354 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -104,7 +104,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
 	middleware.SetLocaleCookie(resp, user.Language, 0)
 
 	// force to generate a new CSRF token
-	if ctx := gitea_context.GetWebContext(req); ctx != nil {
+	if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
 		ctx.Csrf.PrepareForSessionUser(ctx)
 	}
 }
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 7f8a03a4c67da..0152f18c7480e 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -89,7 +89,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
 		store.GetData()["EnableSSPI"] = true
 		// in this case, the Verify function is called in Gitea's web context
 		// FIXME: it doesn't look good to render the page here, why not redirect?
-		gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn)
+		gitea_context.GetWebContext(req.Context()).HTML(http.StatusUnauthorized, tplSignIn)
 		return nil, err
 	}
 	if outToken != "" {
diff --git a/services/context/context.go b/services/context/context.go
index 812a8c27eeb53..2f6a465740570 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -77,9 +77,9 @@ type webContextKeyType struct{}
 
 var WebContextKey = webContextKeyType{}
 
-func GetWebContext(req *http.Request) *Context {
-	ctx, _ := req.Context().Value(WebContextKey).(*Context)
-	return ctx
+func GetWebContext(ctx context.Context) *Context {
+	webCtx, _ := ctx.Value(WebContextKey).(*Context)
+	return webCtx
 }
 
 // ValidateContext is a special context for form validation middleware. It may be different from other contexts.
@@ -133,6 +133,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
 	}
 	ctx.TemplateContext = NewTemplateContextForWeb(ctx)
 	ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
+	ctx.AppendContextValue(WebContextKey, ctx)
 	return ctx
 }
 
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 1f1abf496a3e0..b4ad2de711b3e 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -20,8 +20,8 @@ func ProcessorHelper() *markup.RenderHelperFuncs {
 				return false
 			}
 
-			giteaCtx, ok := ctx.(*gitea_context.Context)
-			if !ok {
+			giteaCtx := gitea_context.GetWebContext(ctx)
+			if giteaCtx == nil {
 				// when using general context, use user's visibility to check
 				return mentionedUser.Visibility.IsPublic()
 			}
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
index 0500e57e4610e..cecfadae8655b 100644
--- a/services/markup/processorhelper_codepreview.go
+++ b/services/markup/processorhelper_codepreview.go
@@ -35,8 +35,8 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
 		return "", err
 	}
 
-	webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
-	if !ok {
+	webCtx := gitea_context.GetWebContext(ctx)
+	if webCtx == nil {
 		return "", fmt.Errorf("context is not a web context")
 	}
 	doer := webCtx.Doer

From da33b708af1a98c2ead4b4714fcc3055b2d4c6bd Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 11 Feb 2025 19:53:56 -0800
Subject: [PATCH 109/206] Add a transaction to `pickTask` (#33543) (#33563)

Backport #33543

In the old `pickTask`, when getting secrets or variables failed, the
task could get stuck in the `running` status (task status is `running`
but the runner did not fetch the task). To fix this issue, these steps
should be in one transaction.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/api/actions/runner/runner.go          |  2 +-
 services/actions/init_test.go                 |  4 +-
 .../utils.go => services/actions/task.go      | 96 +++++++++++--------
 .../actions/task_test.go                      |  5 +-
 4 files changed, 60 insertions(+), 47 deletions(-)
 rename routers/api/actions/runner/utils.go => services/actions/task.go (88%)
 rename routers/api/actions/runner/utils_test.go => services/actions/task_test.go (80%)

diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index 8f365cc92670a..2435807994f90 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -156,7 +156,7 @@ func (s *Service) FetchTask(
 		// if the task version in request is not equal to the version in db,
 		// it means there may still be some tasks not be assgined.
 		// try to pick a task for the runner that send the request.
-		if t, ok, err := pickTask(ctx, runner); err != nil {
+		if t, ok, err := actions_service.PickTask(ctx, runner); err != nil {
 			log.Error("pick task failed: %v", err)
 			return nil, status.Errorf(codes.Internal, "pick task: %v", err)
 		} else if ok {
diff --git a/services/actions/init_test.go b/services/actions/init_test.go
index 59c321ccd771e..7ef07022041da 100644
--- a/services/actions/init_test.go
+++ b/services/actions/init_test.go
@@ -17,9 +17,7 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	unittest.MainTest(m, &unittest.TestOptions{
-		FixtureFiles: []string{"action_runner_token.yml"},
-	})
+	unittest.MainTest(m)
 	os.Exit(m.Run())
 }
 
diff --git a/routers/api/actions/runner/utils.go b/services/actions/task.go
similarity index 88%
rename from routers/api/actions/runner/utils.go
rename to services/actions/task.go
index 539be8d88905a..f2d14b329c763 100644
--- a/routers/api/actions/runner/utils.go
+++ b/services/actions/task.go
@@ -1,7 +1,7 @@
 // Copyright 2022 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package runner
+package actions
 
 import (
 	"context"
@@ -16,51 +16,68 @@ import (
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/services/actions"
 
 	runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
 	"google.golang.org/protobuf/types/known/structpb"
 )
 
-func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
-	t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
-	if err != nil {
-		return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err)
-	}
-	if !ok {
-		return nil, false, nil
-	}
+func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
+	var (
+		task *runnerv1.Task
+		job  *actions_model.ActionRunJob
+	)
 
-	secrets, err := secret_model.GetSecretsOfTask(ctx, t)
-	if err != nil {
-		return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
-	}
+	if err := db.WithTx(ctx, func(ctx context.Context) error {
+		t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
+		if err != nil {
+			return fmt.Errorf("CreateTaskForRunner: %w", err)
+		}
+		if !ok {
+			return nil
+		}
 
-	vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
-	if err != nil {
-		return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
-	}
+		if err := t.LoadAttributes(ctx); err != nil {
+			return fmt.Errorf("task LoadAttributes: %w", err)
+		}
+		job = t.Job
 
-	actions.CreateCommitStatus(ctx, t.Job)
+		secrets, err := secret_model.GetSecretsOfTask(ctx, t)
+		if err != nil {
+			return fmt.Errorf("GetSecretsOfTask: %w", err)
+		}
+
+		vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
+		if err != nil {
+			return fmt.Errorf("GetVariablesOfRun: %w", err)
+		}
+
+		needs, err := findTaskNeeds(ctx, job)
+		if err != nil {
+			return fmt.Errorf("findTaskNeeds: %w", err)
+		}
+
+		taskContext := generateTaskContext(t)
 
-	task := &runnerv1.Task{
-		Id:              t.ID,
-		WorkflowPayload: t.Job.WorkflowPayload,
-		Context:         generateTaskContext(t),
-		Secrets:         secrets,
-		Vars:            vars,
+		task = &runnerv1.Task{
+			Id:              t.ID,
+			WorkflowPayload: t.Job.WorkflowPayload,
+			Context:         taskContext,
+			Secrets:         secrets,
+			Vars:            vars,
+			Needs:           needs,
+		}
+
+		return nil
+	}); err != nil {
+		return nil, false, err
 	}
 
-	if needs, err := findTaskNeeds(ctx, t); err != nil {
-		log.Error("Cannot find needs for task %v: %v", t.ID, err)
-		// Go on with empty needs.
-		// If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner.
-		// In contrast, missing needs is less serious.
-		// And the task will fail and the runner will report the error in the logs.
-	} else {
-		task.Needs = needs
+	if task == nil {
+		return nil, false, nil
 	}
 
+	CreateCommitStatus(ctx, job)
+
 	return task, true, nil
 }
 
@@ -95,7 +112,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
 
 	refName := git.RefName(ref)
 
-	giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
+	giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
 	if err != nil {
 		log.Error("actions.CreateAuthorizationToken failed: %v", err)
 	}
@@ -148,16 +165,13 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
 	return taskContext
 }
 
-func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) {
-	if err := task.LoadAttributes(ctx); err != nil {
-		return nil, fmt.Errorf("LoadAttributes: %w", err)
-	}
-	if len(task.Job.Needs) == 0 {
+func findTaskNeeds(ctx context.Context, job *actions_model.ActionRunJob) (map[string]*runnerv1.TaskNeed, error) {
+	if len(job.Needs) == 0 {
 		return nil, nil
 	}
-	needs := container.SetOf(task.Job.Needs...)
+	needs := container.SetOf(job.Needs...)
 
-	jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
+	jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: job.RunID})
 	if err != nil {
 		return nil, fmt.Errorf("FindRunJobs: %w", err)
 	}
diff --git a/routers/api/actions/runner/utils_test.go b/services/actions/task_test.go
similarity index 80%
rename from routers/api/actions/runner/utils_test.go
rename to services/actions/task_test.go
index d7a6f84550f1d..035b62c5bf444 100644
--- a/routers/api/actions/runner/utils_test.go
+++ b/services/actions/task_test.go
@@ -1,7 +1,7 @@
 // Copyright 2024 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package runner
+package actions
 
 import (
 	"context"
@@ -17,8 +17,9 @@ func Test_findTaskNeeds(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
 
 	task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
+	job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
 
-	ret, err := findTaskNeeds(context.Background(), task)
+	ret, err := findTaskNeeds(context.Background(), job)
 	assert.NoError(t, err)
 	assert.Len(t, ret, 1)
 	assert.Contains(t, ret, "job1")

From 744f7c8200190964b72b145040ea58801949832b Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 13 Feb 2025 03:27:37 +0800
Subject: [PATCH 110/206] Skip deletion error for action artifacts (#33476)
 (#33568)

Fix #33567
---
 services/actions/cleanup.go | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 1223ebcab6349..ee1d1677133e2 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -52,9 +52,9 @@ func cleanExpiredArtifacts(taskCtx context.Context) error {
 		}
 		if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
 			log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
-			continue
+			// go on
 		}
-		log.Info("Artifact %d set expired", artifact.ID)
+		log.Info("Artifact %d is deleted (due to expiration)", artifact.ID)
 	}
 	return nil
 }
@@ -76,9 +76,9 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
 			}
 			if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
 				log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
-				continue
+				// go on
 			}
-			log.Info("Artifact %d set deleted", artifact.ID)
+			log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID)
 		}
 		if len(artifacts) < deleteArtifactBatchSize {
 			log.Debug("No more artifacts pending deletion")
@@ -103,8 +103,7 @@ func CleanupLogs(ctx context.Context) error {
 		for _, task := range tasks {
 			if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
 				log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
-				// do not return error here, continue to next task
-				continue
+				// do not return error here, go on
 			}
 			task.LogIndexes = nil // clear log indexes since it's a heavy field
 			task.LogExpired = true

From 76bd60fc1defa8dc6646ce78d580b4784d43684a Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 13 Feb 2025 07:33:11 +0800
Subject: [PATCH 111/206] Fix various problems (artifact order, api empty
 slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569)
 (#33577)

Backport #33569 by @wxiaoguang

* Make artifact list output has stable order
* Fix #33506
* Fix #33521
* Fix #33288
* Fix #33196
* Fix #33561

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/actions/artifact.go     |  8 +++++++-
 modules/util/slice.go          |  7 +++++++
 routers/common/codesearch.go   |  2 +-
 routers/web/repo/issue_new.go  | 11 +++++++----
 services/convert/issue.go      |  2 +-
 services/convert/pull.go       |  7 ++++++-
 services/convert/repository.go |  3 ++-
 services/mirror/mirror_push.go |  3 +++
 services/repository/adopt.go   | 21 +++++++++++----------
 9 files changed, 45 insertions(+), 19 deletions(-)

diff --git a/models/actions/artifact.go b/models/actions/artifact.go
index 0bc66ba24e846..706eb2e43ad61 100644
--- a/models/actions/artifact.go
+++ b/models/actions/artifact.go
@@ -114,6 +114,12 @@ type FindArtifactsOptions struct {
 	Status       int
 }
 
+func (opts FindArtifactsOptions) ToOrders() string {
+	return "id"
+}
+
+var _ db.FindOptionsOrder = (*FindArtifactsOptions)(nil)
+
 func (opts FindArtifactsOptions) ToConds() builder.Cond {
 	cond := builder.NewCond()
 	if opts.RepoID > 0 {
@@ -132,7 +138,7 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
 	return cond
 }
 
-// ActionArtifactMeta is the meta data of an artifact
+// ActionArtifactMeta is the meta-data of an artifact
 type ActionArtifactMeta struct {
 	ArtifactName string
 	FileSize     int64
diff --git a/modules/util/slice.go b/modules/util/slice.go
index 9c878c24bea7c..da6886491e5d8 100644
--- a/modules/util/slice.go
+++ b/modules/util/slice.go
@@ -71,3 +71,10 @@ func KeysOfMap[K comparable, V any](m map[K]V) []K {
 	}
 	return keys
 }
+
+func SliceNilAsEmpty[T any](a []T) []T {
+	if a == nil {
+		return []T{}
+	}
+	return a
+}
diff --git a/routers/common/codesearch.go b/routers/common/codesearch.go
index a14af126e55bc..7cd01068b0a8f 100644
--- a/routers/common/codesearch.go
+++ b/routers/common/codesearch.go
@@ -25,7 +25,7 @@ func PrepareCodeSearch(ctx *context.Context) (ret struct {
 	}
 	isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(fuzzyDefault)
 	if isFuzzy && !fuzzyAllow {
-		ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons")
+		ctx.Flash.Info("Fuzzy search is disabled by default due to performance reasons", true)
 		isFuzzy = false
 	}
 
diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go
index 32daa3e48fabd..9115fc1771e1d 100644
--- a/routers/web/repo/issue_new.go
+++ b/routers/web/repo/issue_new.go
@@ -276,13 +276,16 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
 	}
 	pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
 
+	// prepare assignees
 	candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
 	inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
-	if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
-		ctx.NotFound("", nil)
-		return ret
+	var assigneeIDStrings []string
+	for _, inputAssigneeID := range inputAssigneeIDs {
+		if candidateAssignees.Contains(inputAssigneeID) {
+			assigneeIDStrings = append(assigneeIDStrings, strconv.FormatInt(inputAssigneeID, 10))
+		}
 	}
-	pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
+	pageMetaData.AssigneesData.SelectedAssigneeIDs = strings.Join(assigneeIDStrings, ",")
 
 	// Check if the passed reviewers (user/team) actually exist
 	var reviewers []*user_model.User
diff --git a/services/convert/issue.go b/services/convert/issue.go
index 37935accca269..62d0a3b3e6827 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -67,7 +67,7 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
 		if err := issue.LoadLabels(ctx); err != nil {
 			return &api.Issue{}
 		}
-		apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
+		apiIssue.Labels = util.SliceNilAsEmpty(ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner))
 		apiIssue.Repo = &api.RepositoryMeta{
 			ID:       issue.Repo.ID,
 			Name:     issue.Repo.Name,
diff --git a/services/convert/pull.go b/services/convert/pull.go
index a1ab7eeb8eca6..209d2bd79d2e1 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -18,6 +18,7 @@ import (
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // ToAPIPullRequest assumes following fields have been assigned with valid values:
@@ -77,7 +78,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		Labels:         apiIssue.Labels,
 		Milestone:      apiIssue.Milestone,
 		Assignee:       apiIssue.Assignee,
-		Assignees:      apiIssue.Assignees,
+		Assignees:      util.SliceNilAsEmpty(apiIssue.Assignees),
 		State:          apiIssue.State,
 		Draft:          pr.IsWorkInProgress(ctx),
 		IsLocked:       apiIssue.IsLocked,
@@ -94,6 +95,10 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		Updated:        pr.Issue.UpdatedUnix.AsTimePtr(),
 		PinOrder:       apiIssue.PinOrder,
 
+		// output "[]" rather than null to align to github outputs
+		RequestedReviewers:      []*api.User{},
+		RequestedReviewersTeams: []*api.Team{},
+
 		AllowMaintainerEdit: pr.AllowMaintainerEdit,
 
 		Base: &api.PRBranchInfo{
diff --git a/services/convert/repository.go b/services/convert/repository.go
index e026d0f4405f4..644a2a11fa2d2 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -15,6 +15,7 @@ import (
 	unit_model "code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
 )
 
 // ToRepo converts a Repository to api.Repository
@@ -241,7 +242,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 		MirrorInterval:                mirrorInterval,
 		MirrorUpdated:                 mirrorUpdated,
 		RepoTransfer:                  transfer,
-		Topics:                        repo.Topics,
+		Topics:                        util.SliceNilAsEmpty(repo.Topics),
 		ObjectFormatName:              repo.ObjectFormatName,
 		Licenses:                      repoLicenses.StringList(),
 	}
diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go
index 21ba0afeffd6e..2429f686dd9c2 100644
--- a/services/mirror/mirror_push.go
+++ b/services/mirror/mirror_push.go
@@ -19,6 +19,7 @@ import (
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
+	"code.gitea.io/gitea/modules/proxy"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -167,11 +168,13 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
 
 		log.Trace("Pushing %s mirror[%d] remote %s", path, m.ID, m.RemoteName)
 
+		envs := proxy.EnvWithProxy(remoteURL.URL)
 		if err := git.Push(ctx, path, git.PushOptions{
 			Remote:  m.RemoteName,
 			Force:   true,
 			Mirror:  true,
 			Timeout: timeout,
+			Env:     envs,
 		}); err != nil {
 			log.Error("Error pushing %s mirror[%d] remote %s: %v", path, m.ID, m.RemoteName, err)
 
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 615f4d482c034..1f18702601f51 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -52,8 +52,9 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
 		IsEmpty:                         !opts.AutoInit,
 	}
 
+	repoPath := repo_model.RepoPath(u.Name, repo.Name)
+
 	if err := db.WithTx(ctx, func(ctx context.Context) error {
-		repoPath := repo_model.RepoPath(u.Name, repo.Name)
 		isExist, err := util.IsExist(repoPath)
 		if err != nil {
 			log.Error("Unable to check if %s exists. Error: %v", repoPath, err)
@@ -75,7 +76,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
 		if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
 			return fmt.Errorf("getRepositoryByID: %w", err)
 		}
+		return nil
+	}); err != nil {
+		return nil, err
+	}
 
+	if err := func() error {
 		if err := adoptRepository(ctx, repoPath, repo, opts.DefaultBranch); err != nil {
 			return fmt.Errorf("adoptRepository: %w", err)
 		}
@@ -84,13 +90,6 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
 			return fmt.Errorf("checkDaemonExportOK: %w", err)
 		}
 
-		// Initialize Issue Labels if selected
-		if len(opts.IssueLabels) > 0 {
-			if err := repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
-				return fmt.Errorf("InitializeLabels: %w", err)
-			}
-		}
-
 		if stdout, _, err := git.NewCommand(ctx, "update-server-info").
 			SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)).
 			RunStdString(&git.RunOpts{Dir: repoPath}); err != nil {
@@ -98,10 +97,12 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
 			return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
 		}
 		return nil
-	}); err != nil {
+	}(); err != nil {
+		if errDel := DeleteRepository(ctx, doer, repo, false /* no notify */); errDel != nil {
+			log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
+		}
 		return nil, err
 	}
-
 	notify_service.AdoptRepository(ctx, doer, u, repo)
 
 	return repo, nil

From cc6ec5673867b38f1f618cdfb65f79be8bd8f38f Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 13 Feb 2025 20:02:28 +0800
Subject: [PATCH 112/206] Only show the latest version in the Arch index
 (#33262) (#33580)

Backport #33262 by ExplodingDragon

Only show the latest version of the package in the arch repo.

closes #33534

Co-authored-by: Exploding Dragon <explodingfkl@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/packages/arch/repository.go        |  38 ++++---
 services/packages/arch/vercmp.go            | 113 ++++++++++++++++++++
 services/packages/arch/vercmp_test.go       |  27 +++++
 tests/integration/api_packages_arch_test.go |  92 +++++++++++-----
 4 files changed, 229 insertions(+), 41 deletions(-)
 create mode 100644 services/packages/arch/vercmp.go
 create mode 100644 services/packages/arch/vercmp_test.go

diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go
index 7fb4222cf6177..a12af82ba5f5d 100644
--- a/services/packages/arch/repository.go
+++ b/services/packages/arch/repository.go
@@ -235,6 +235,28 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		return packages_service.DeletePackageFile(ctx, pf)
 	}
 
+	vpfs := make(map[int64]*entryOptions)
+	for _, pf := range pfs {
+		current := &entryOptions{
+			File: pf,
+		}
+		current.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
+		if err != nil {
+			return err
+		}
+
+		// here we compare the versions but not using SearchLatestVersions because we shouldn't allow "downgrading" to a older version by "latest" one.
+		// https://wiki.archlinux.org/title/Downgrading_packages : randomly downgrading can mess up dependencies:
+		// If a downgrade involves a soname change, all dependencies may need downgrading or rebuilding too.
+		if old, ok := vpfs[current.Version.PackageID]; ok {
+			if compareVersions(old.Version.Version, current.Version.Version) == -1 {
+				vpfs[current.Version.PackageID] = current
+			}
+		} else {
+			vpfs[current.Version.PackageID] = current
+		}
+	}
+
 	indexContent, _ := packages_module.NewHashedBuffer()
 	defer indexContent.Close()
 
@@ -243,15 +265,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 
 	cache := make(map[int64]*packages_model.Package)
 
-	for _, pf := range pfs {
-		opts := &entryOptions{
-			File: pf,
-		}
-
-		opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
-		if err != nil {
-			return err
-		}
+	for _, opts := range vpfs {
 		if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil {
 			return err
 		}
@@ -263,12 +277,12 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 			}
 			cache[opts.Package.ID] = opts.Package
 		}
-		opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID)
+		opts.Blob, err = packages_model.GetBlobByID(ctx, opts.File.BlobID)
 		if err != nil {
 			return err
 		}
 
-		sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature)
+		sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertySignature)
 		if err != nil {
 			return err
 		}
@@ -277,7 +291,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
 		}
 		opts.Signature = sig[0].Value
 
-		meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata)
+		meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, opts.File.ID, arch_module.PropertyMetadata)
 		if err != nil {
 			return err
 		}
diff --git a/services/packages/arch/vercmp.go b/services/packages/arch/vercmp.go
new file mode 100644
index 0000000000000..0d33dda0f1232
--- /dev/null
+++ b/services/packages/arch/vercmp.go
@@ -0,0 +1,113 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"strings"
+	"unicode"
+)
+
+// https://gitlab.archlinux.org/pacman/pacman/-/blob/d55b47e5512808b67bc944feb20c2bcc6c1a4c45/lib/libalpm/version.c
+
+import (
+	"strconv"
+)
+
+func parseEVR(evr string) (epoch, version, release string) {
+	if before, after, f := strings.Cut(evr, ":"); f {
+		epoch = before
+		evr = after
+	} else {
+		epoch = "0"
+	}
+
+	if before, after, f := strings.Cut(evr, "-"); f {
+		version = before
+		release = after
+	} else {
+		version = evr
+		release = "1"
+	}
+	return epoch, version, release
+}
+
+func compareSegments(a, b []string) int {
+	lenA, lenB := len(a), len(b)
+	var l int
+	if lenA > lenB {
+		l = lenB
+	} else {
+		l = lenA
+	}
+	for i := 0; i < l; i++ {
+		if r := compare(a[i], b[i]); r != 0 {
+			return r
+		}
+	}
+	if lenA == lenB {
+		return 0
+	} else if l == lenA {
+		return -1
+	}
+	return 1
+}
+
+func compare(a, b string) int {
+	if a == b {
+		return 0
+	}
+
+	aNumeric := isNumeric(a)
+	bNumeric := isNumeric(b)
+
+	if aNumeric && bNumeric {
+		aInt, _ := strconv.Atoi(a)
+		bInt, _ := strconv.Atoi(b)
+		switch {
+		case aInt < bInt:
+			return -1
+		case aInt > bInt:
+			return 1
+		default:
+			return 0
+		}
+	}
+
+	if aNumeric {
+		return 1
+	}
+	if bNumeric {
+		return -1
+	}
+
+	return strings.Compare(a, b)
+}
+
+func isNumeric(s string) bool {
+	for _, c := range s {
+		if !unicode.IsDigit(c) {
+			return false
+		}
+	}
+	return true
+}
+
+func compareVersions(a, b string) int {
+	if a == b {
+		return 0
+	}
+
+	epochA, versionA, releaseA := parseEVR(a)
+	epochB, versionB, releaseB := parseEVR(b)
+
+	if res := compareSegments([]string{epochA}, []string{epochB}); res != 0 {
+		return res
+	}
+
+	if res := compareSegments(strings.Split(versionA, "."), strings.Split(versionB, ".")); res != 0 {
+		return res
+	}
+
+	return compareSegments([]string{releaseA}, []string{releaseB})
+}
diff --git a/services/packages/arch/vercmp_test.go b/services/packages/arch/vercmp_test.go
new file mode 100644
index 0000000000000..2014a6d429d14
--- /dev/null
+++ b/services/packages/arch/vercmp_test.go
@@ -0,0 +1,27 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestCompareVersions(t *testing.T) {
+	// https://man.archlinux.org/man/vercmp.8.en
+	checks := [][]string{
+		{"1.0a", "1.0b", "1.0beta", "1.0p", "1.0pre", "1.0rc", "1.0", "1.0.a", "1.0.1"},
+		{"1", "1.0", "1.1", "1.1.1", "1.2", "2.0", "3.0.0"},
+	}
+	for _, check := range checks {
+		for i := 0; i < len(check)-1; i++ {
+			require.Equal(t, -1, compareVersions(check[i], check[i+1]))
+			require.Equal(t, 1, compareVersions(check[i+1], check[i]))
+		}
+	}
+	require.Equal(t, 1, compareVersions("1.0-2", "1.0"))
+	require.Equal(t, 0, compareVersions("0:1.0-1", "1.0"))
+	require.Equal(t, 1, compareVersions("1:1.0-1", "2.0"))
+}
diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go
index 9c7a9dd19dec1..b9064ca3d1c96 100644
--- a/tests/integration/api_packages_arch_test.go
+++ b/tests/integration/api_packages_arch_test.go
@@ -79,6 +79,34 @@ license = MIT`)
 
 		return buf.Bytes()
 	}
+	readIndexContent := func(r io.Reader) (map[string]string, error) {
+		gzr, err := gzip.NewReader(r)
+		if err != nil {
+			return nil, err
+		}
+
+		content := make(map[string]string)
+
+		tr := tar.NewReader(gzr)
+		for {
+			hd, err := tr.Next()
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				return nil, err
+			}
+
+			buf, err := io.ReadAll(tr)
+			if err != nil {
+				return nil, err
+			}
+
+			content[hd.Name] = string(buf)
+		}
+
+		return content, nil
+	}
 
 	compressions := []string{"gz", "xz", "zst"}
 	repositories := []string{"main", "testing", "with/slash", ""}
@@ -171,35 +199,6 @@ license = MIT`)
 					MakeRequest(t, req, http.StatusConflict)
 				})
 
-				readIndexContent := func(r io.Reader) (map[string]string, error) {
-					gzr, err := gzip.NewReader(r)
-					if err != nil {
-						return nil, err
-					}
-
-					content := make(map[string]string)
-
-					tr := tar.NewReader(gzr)
-					for {
-						hd, err := tr.Next()
-						if err == io.EOF {
-							break
-						}
-						if err != nil {
-							return nil, err
-						}
-
-						buf, err := io.ReadAll(tr)
-						if err != nil {
-							return nil, err
-						}
-
-						content[hd.Name] = string(buf)
-					}
-
-					return content, nil
-				}
-
 				t.Run("Index", func(t *testing.T) {
 					defer tests.PrintCurrentTest(t)()
 
@@ -299,4 +298,39 @@ license = MIT`)
 			})
 		}
 	}
+	t.Run("KeepLastVersion", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		pkgVer1 := createPackage("gz", "gitea-test", "1.0.0", "aarch64")
+		pkgVer2 := createPackage("gz", "gitea-test", "1.0.1", "aarch64")
+		req := NewRequestWithBody(t, "PUT", rootURL, bytes.NewReader(pkgVer1)).
+			AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+		req = NewRequestWithBody(t, "PUT", rootURL, bytes.NewReader(pkgVer2)).
+			AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusCreated)
+
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		content, err := readIndexContent(resp.Body)
+		assert.NoError(t, err)
+		assert.Len(t, content, 2)
+
+		_, has := content["gitea-test-1.0.0/desc"]
+		assert.False(t, has)
+		_, has = content["gitea-test-1.0.1/desc"]
+		assert.True(t, has)
+
+		req = NewRequest(t, "DELETE", fmt.Sprintf("%s/gitea-test/1.0.1/aarch64", rootURL)).
+			AddBasicAuth(user.Name)
+		MakeRequest(t, req, http.StatusNoContent)
+
+		req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
+		resp = MakeRequest(t, req, http.StatusOK)
+		content, err = readIndexContent(resp.Body)
+		assert.NoError(t, err)
+		assert.Len(t, content, 2)
+		_, has = content["gitea-test-1.0.0/desc"]
+		assert.True(t, has)
+	})
 }

From 5e9cc919cf2a400f7ced7bce4a1818659bfc85d5 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 14 Feb 2025 16:06:46 +0800
Subject: [PATCH 113/206] Performance optimization for pull request files
 loading comments attachments (#33585) (#33592)

Backport #33585 by @lunny

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/issues/comment_code.go |  6 ++++--
 routers/web/repo/pull.go      | 12 ++++++------
 services/gitdiff/gitdiff.go   |  2 +-
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 67a77ceb13f36..b562aab5005f6 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -86,8 +86,10 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 			ids = append(ids, comment.ReviewID)
 		}
 	}
-	if err := e.In("id", ids).Find(&reviews); err != nil {
-		return nil, err
+	if len(ids) > 0 {
+		if err := e.In("id", ids).Find(&reviews); err != nil {
+			return nil, err
+		}
 	}
 
 	n := 0
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 8fd23f835ad67..e068ac45c6588 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -785,18 +785,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 		return
 	}
 
+	allComments := issues_model.CommentList{}
 	for _, file := range diff.Files {
 		for _, section := range file.Sections {
 			for _, line := range section.Lines {
-				for _, comment := range line.Comments {
-					if err := comment.LoadAttachments(ctx); err != nil {
-						ctx.ServerError("LoadAttachments", err)
-						return
-					}
-				}
+				allComments = append(allComments, line.Comments...)
 			}
 		}
 	}
+	if err := allComments.LoadAttachments(ctx); err != nil {
+		ctx.ServerError("LoadAttachments", err)
+		return
+	}
 
 	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
 	if err != nil {
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index bb1722039e19d..eb7c273d49940 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -80,7 +80,7 @@ type DiffLine struct {
 	Match       int
 	Type        DiffLineType
 	Content     string
-	Comments    []*issues_model.Comment
+	Comments    issues_model.CommentList
 	SectionInfo *DiffLineSectionInfo
 }
 

From d17f8ffcc120ef3ae998019bfaac20fb53715ece Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 14 Feb 2025 16:38:33 +0800
Subject: [PATCH 114/206] Fix PR's target branch dropdown (#33589) (#33591)

Backport #33589
---
 routers/web/repo/issue_page_meta.go | 24 ++++++++++++++++--------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go
index 272343f460ff3..93cc38bffa1cf 100644
--- a/routers/web/repo/issue_page_meta.go
+++ b/routers/web/repo/issue_page_meta.go
@@ -79,27 +79,35 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
 		return data
 	}
 
-	data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
-	if !data.CanModifyIssueOrPull {
+	// it sets "Branches" template data,
+	// it is used to render the "edit PR target branches" dropdown, and the "branch selector" in the issue's sidebar.
+	PrepareBranchList(ctx)
+	if ctx.Written() {
 		return data
 	}
 
-	data.retrieveAssigneesDataForIssueWriter(ctx)
+	// it sets the "Assignees" template data, and the data is also used to "mention" users.
+	data.retrieveAssigneesData(ctx)
 	if ctx.Written() {
 		return data
 	}
 
-	data.retrieveMilestonesDataForIssueWriter(ctx)
-	if ctx.Written() {
+	// TODO: the issue/pull permissions are quite complex and unclear
+	// A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch)
+	// A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
+	// For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
+	// Need to clarify the logic and add some tests in the future
+	data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
+	if !data.CanModifyIssueOrPull {
 		return data
 	}
 
-	data.retrieveProjectsDataForIssueWriter(ctx)
+	data.retrieveMilestonesDataForIssueWriter(ctx)
 	if ctx.Written() {
 		return data
 	}
 
-	PrepareBranchList(ctx)
+	data.retrieveProjectsDataForIssueWriter(ctx)
 	if ctx.Written() {
 		return data
 	}
@@ -131,7 +139,7 @@ func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Co
 	}
 }
 
-func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
+func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
 	var err error
 	d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
 	if err != nil {

From 7697df9f93b0c11671cc471393f6bb590462224e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 14 Feb 2025 23:38:55 +0800
Subject: [PATCH 115/206] Use default Git timeout when checking repo health
 (#33593) (#33598)

Backport #33593 by @Zettat123

Use `git.timeout.DEFAULT` configuration instead of 60 seconds.

Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 services/cron/tasks_basic.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go
index fb5938745e61c..841981787dffd 100644
--- a/services/cron/tasks_basic.go
+++ b/services/cron/tasks_basic.go
@@ -54,7 +54,7 @@ func registerRepoHealthCheck() {
 			RunAtStart: false,
 			Schedule:   "@midnight",
 		},
-		Timeout: 60 * time.Second,
+		Timeout: time.Duration(setting.Git.Timeout.Default) * time.Second,
 		Args:    []string{},
 	}, func(ctx context.Context, _ *user_model.User, config Config) error {
 		rhcConfig := config.(*RepoHealthCheckConfig)

From 99545ae2fd376ccee376877cc49f3d586fa34d9d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 16 Feb 2025 13:17:34 +0800
Subject: [PATCH 116/206] Fix mirror bug (#33597) (#33607)

Backport #33597 by @ericLemanissier

follows-up be4e961240883778c44d9651eaaf9ab8723bbbb0

This is the same modification as
https://github.com/go-gitea/gitea/commit/be4e961240883778c44d9651eaaf9ab8723bbbb0
but for force-pushes.
It is needed, because `git fetch` reveals force pushes for github
mirrors:
```
$ git fetch --tags origin
remote: Enumerating objects: 22, done.
remote: Counting objects: 100% (22/22), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 9 (delta 5), reused 8 (delta 5), pack-reused 0 (from 0)
Unpacking objects: 100% (9/9), 1.70 KiB | 12.00 KiB/s, done.
From https://github.com/conan-io/conan-center-index
   729f0f1b8f..48184eddeb  refs/pull/26595/head  -> refs/pull/26595/head
 + 0c31ab60a3...1283cca9e7 refs/pull/26595/merge -> refs/pull/26595/merge  (forced update)
```

Fix https://github.com/go-gitea/gitea/issues/33200

PS: I did not test the modification, but it is the exact same change as
the last hunk in
https://github.com/go-gitea/gitea/pull/33224/files#diff-bb5cdb90db0f0e7f6716c0e6d0b9cbb67f08d82052b03ab3a7b5e23a1d76aed7
, just moved to the previous case of the switch

Co-authored-by: ericLemanissier <ericLemanissier@users.noreply.github.com>
---
 services/mirror/mirror_pull.go | 13 +++++++++++--
 services/mirror/mirror_test.go | 22 +++++++++++++++++++++-
 2 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 4a8bcd03a03e5..06f1fb3a3e2e2 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -136,7 +136,9 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
 		case strings.HasPrefix(lines[i], " - "): // Delete reference
 			isTag := !strings.HasPrefix(refName, remoteName+"/")
 			var refFullName git.RefName
-			if isTag {
+			if strings.HasPrefix(refName, "refs/") {
+				refFullName = git.RefName(refName)
+			} else if isTag {
 				refFullName = git.RefNameFromTag(refName)
 			} else {
 				refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
@@ -159,8 +161,15 @@ func parseRemoteUpdateOutput(output, remoteName string) []*mirrorSyncResult {
 				log.Error("Expect two SHAs but not what found: %q", lines[i])
 				continue
 			}
+			var refFullName git.RefName
+			if strings.HasPrefix(refName, "refs/") {
+				refFullName = git.RefName(refName)
+			} else {
+				refFullName = git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/"))
+			}
+
 			results = append(results, &mirrorSyncResult{
-				refName:     git.RefNameFromBranch(strings.TrimPrefix(refName, remoteName+"/")),
+				refName:     refFullName,
 				oldCommitID: shas[0],
 				newCommitID: shas[1],
 			})
diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go
index 8ad524b608de4..76632b6872ca6 100644
--- a/services/mirror/mirror_test.go
+++ b/services/mirror/mirror_test.go
@@ -17,9 +17,13 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
  - [deleted]         (none)     -> tag1
  + f895a1e...957a993 test2      -> origin/test2  (forced update)
    957a993..a87ba5f  test3      -> origin/test3
+ * [new ref]               refs/pull/26595/head  -> refs/pull/26595/head
+ * [new ref]               refs/pull/26595/merge -> refs/pull/26595/merge
+   e0639e38fb..6db2410489  refs/pull/25873/head  -> refs/pull/25873/head
+ + 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge  (forced update)
 `
 	results := parseRemoteUpdateOutput(output, "origin")
-	assert.Len(t, results, 6)
+	assert.Len(t, results, 10)
 	assert.EqualValues(t, "refs/tags/v0.1.8", results[0].refName.String())
 	assert.EqualValues(t, gitShortEmptySha, results[0].oldCommitID)
 	assert.EqualValues(t, "", results[0].newCommitID)
@@ -43,4 +47,20 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
 	assert.EqualValues(t, "refs/heads/test3", results[5].refName.String())
 	assert.EqualValues(t, "957a993", results[5].oldCommitID)
 	assert.EqualValues(t, "a87ba5f", results[5].newCommitID)
+
+	assert.EqualValues(t, "refs/pull/26595/head", results[6].refName.String())
+	assert.EqualValues(t, gitShortEmptySha, results[6].oldCommitID)
+	assert.EqualValues(t, "", results[6].newCommitID)
+
+	assert.EqualValues(t, "refs/pull/26595/merge", results[7].refName.String())
+	assert.EqualValues(t, gitShortEmptySha, results[7].oldCommitID)
+	assert.EqualValues(t, "", results[7].newCommitID)
+
+	assert.EqualValues(t, "refs/pull/25873/head", results[8].refName.String())
+	assert.EqualValues(t, "e0639e38fb", results[8].oldCommitID)
+	assert.EqualValues(t, "6db2410489", results[8].newCommitID)
+
+	assert.EqualValues(t, "refs/pull/25873/merge", results[9].refName.String())
+	assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID)
+	assert.EqualValues(t, "976d27d52f", results[9].newCommitID)
 }

From 0512b02b01e3b979f5dcf302d2c75bc16a62f973 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 18 Feb 2025 00:59:32 +0800
Subject: [PATCH 117/206] Fix project issues list and counting (#33594)
 (#33619)

Backport #33594 by lunny

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/issues/issue_project.go       |  40 ++---
 models/issues/issue_search.go        |  24 +--
 models/project/column.go             |  49 ++++---
 models/project/column_test.go        |   8 +-
 models/project/issue.go              |  43 ------
 models/project/project.go            |   3 +
 modules/indexer/issues/db/options.go |   4 +-
 routers/web/org/projects.go          |  20 ++-
 routers/web/repo/projects.go         |  11 +-
 routers/web/user/home.go             |   4 +-
 services/projects/issue.go           | 121 +++++++++++++++
 services/projects/issue_test.go      | 210 +++++++++++++++++++++++++++
 services/projects/main_test.go       |  18 +++
 templates/projects/list.tmpl         |   4 +-
 templates/projects/view.tmpl         |   2 +-
 tests/integration/project_test.go    |   2 +-
 16 files changed, 459 insertions(+), 104 deletions(-)
 create mode 100644 services/projects/issue_test.go
 create mode 100644 services/projects/main_test.go

diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index f520604321c9d..01852447834c7 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -49,6 +49,21 @@ func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
 	return ip.ProjectColumnID, nil
 }
 
+func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID int64) (map[int64]int64, error) {
+	issues := make([]project_model.ProjectIssue, 0)
+	if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&issues); err != nil {
+		return nil, err
+	}
+	result := make(map[int64]int64, len(issues))
+	for _, issue := range issues {
+		if issue.ProjectColumnID == 0 {
+			issue.ProjectColumnID = defaultColumnID
+		}
+		result[issue.IssueID] = issue.ProjectColumnID
+	}
+	return result, nil
+}
+
 // LoadIssuesFromColumn load issues assigned to this column
 func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
 	issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
@@ -61,11 +76,11 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
 	}
 
 	if b.Default {
-		issues, err := Issues(ctx, &IssuesOptions{
-			ProjectColumnID: db.NoConditionID,
-			ProjectID:       b.ProjectID,
-			SortType:        "project-column-sorting",
-		})
+		issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
+			o.ProjectColumnID = db.NoConditionID
+			o.ProjectID = b.ProjectID
+			o.SortType = "project-column-sorting"
+		}))
 		if err != nil {
 			return nil, err
 		}
@@ -79,19 +94,6 @@ func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *Is
 	return issueList, nil
 }
 
-// LoadIssuesFromColumnList load issues assigned to the columns
-func LoadIssuesFromColumnList(ctx context.Context, bs project_model.ColumnList, opts *IssuesOptions) (map[int64]IssueList, error) {
-	issuesMap := make(map[int64]IssueList, len(bs))
-	for i := range bs {
-		il, err := LoadIssuesFromColumn(ctx, bs[i], opts)
-		if err != nil {
-			return nil, err
-		}
-		issuesMap[bs[i].ID] = il
-	}
-	return issuesMap, nil
-}
-
 // IssueAssignOrRemoveProject changes the project associated with an issue
 // If newProjectID is 0, the issue is removed from the project
 func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
@@ -112,7 +114,7 @@ func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_mo
 				return util.NewPermissionDeniedErrorf("issue %d can't be accessed by project %d", issue.ID, newProject.ID)
 			}
 			if newColumnID == 0 {
-				newDefaultColumn, err := newProject.GetDefaultColumn(ctx)
+				newDefaultColumn, err := newProject.MustDefaultColumn(ctx)
 				if err != nil {
 					return err
 				}
diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go
index f1cd125d495c4..694b918755dda 100644
--- a/models/issues/issue_search.go
+++ b/models/issues/issue_search.go
@@ -49,9 +49,9 @@ type IssuesOptions struct { //nolint
 	// prioritize issues from this repo
 	PriorityRepoID int64
 	IsArchived     optional.Option[bool]
-	Org            *organization.Organization // issues permission scope
-	Team           *organization.Team         // issues permission scope
-	User           *user_model.User           // issues permission scope
+	Owner          *user_model.User   // issues permission scope, it could be an organization or a user
+	Team           *organization.Team // issues permission scope
+	Doer           *user_model.User   // issues permission scope
 }
 
 // Copy returns a copy of the options.
@@ -273,8 +273,12 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
 
 	applyLabelsCondition(sess, opts)
 
-	if opts.User != nil {
-		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
+	if opts.Owner != nil {
+		sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
+	}
+
+	if opts.Doer != nil && !opts.Doer.IsAdmin {
+		sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.Doer.ID, opts.Owner, opts.Team, opts.IsPull.Value()))
 	}
 }
 
@@ -321,20 +325,20 @@ func teamUnitsRepoCond(id string, userID, orgID, teamID int64, units ...unit.Typ
 }
 
 // issuePullAccessibleRepoCond userID must not be zero, this condition require join repository table
-func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organization.Organization, team *organization.Team, isPull bool) builder.Cond {
+func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_model.User, team *organization.Team, isPull bool) builder.Cond {
 	cond := builder.NewCond()
 	unitType := unit.TypeIssues
 	if isPull {
 		unitType = unit.TypePullRequests
 	}
-	if org != nil {
+	if owner != nil && owner.IsOrganization() {
 		if team != nil {
-			cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, org.ID, team.ID, unitType)) // special team member repos
+			cond = cond.And(teamUnitsRepoCond(repoIDstr, userID, owner.ID, team.ID, unitType)) // special team member repos
 		} else {
 			cond = cond.And(
 				builder.Or(
-					repo_model.UserOrgUnitRepoCond(repoIDstr, userID, org.ID, unitType), // team member repos
-					repo_model.UserOrgPublicUnitRepoCond(userID, org.ID),                // user org public non-member repos, TODO: check repo has issues
+					repo_model.UserOrgUnitRepoCond(repoIDstr, userID, owner.ID, unitType), // team member repos
+					repo_model.UserOrgPublicUnitRepoCond(userID, owner.ID),                // user org public non-member repos, TODO: check repo has issues
 				),
 			)
 		}
diff --git a/models/project/column.go b/models/project/column.go
index 222f44859928e..5f581b58804cb 100644
--- a/models/project/column.go
+++ b/models/project/column.go
@@ -48,6 +48,8 @@ type Column struct {
 	ProjectID int64 `xorm:"INDEX NOT NULL"`
 	CreatorID int64 `xorm:"NOT NULL"`
 
+	NumIssues int64 `xorm:"-"`
+
 	CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
 	UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
 }
@@ -57,20 +59,6 @@ func (Column) TableName() string {
 	return "project_board" // TODO: the legacy table name should be project_column
 }
 
-// NumIssues return counter of all issues assigned to the column
-func (c *Column) NumIssues(ctx context.Context) int {
-	total, err := db.GetEngine(ctx).Table("project_issue").
-		Where("project_id=?", c.ProjectID).
-		And("project_board_id=?", c.ID).
-		GroupBy("issue_id").
-		Cols("issue_id").
-		Count()
-	if err != nil {
-		return 0
-	}
-	return int(total)
-}
-
 func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) {
 	issues := make([]*ProjectIssue, 0, 5)
 	if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID).
@@ -192,7 +180,7 @@ func deleteColumnByID(ctx context.Context, columnID int64) error {
 	if err != nil {
 		return err
 	}
-	defaultColumn, err := project.GetDefaultColumn(ctx)
+	defaultColumn, err := project.MustDefaultColumn(ctx)
 	if err != nil {
 		return err
 	}
@@ -257,8 +245,8 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) {
 	return columns, nil
 }
 
-// GetDefaultColumn return default column and ensure only one exists
-func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
+// getDefaultColumn return default column and ensure only one exists
+func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) {
 	var column Column
 	has, err := db.GetEngine(ctx).
 		Where("project_id=? AND `default` = ?", p.ID, true).
@@ -270,6 +258,33 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) {
 	if has {
 		return &column, nil
 	}
+	return nil, ErrProjectColumnNotExist{ColumnID: 0}
+}
+
+// MustDefaultColumn returns the default column for a project.
+// If one exists, it is returned
+// If none exists, the first column will be elevated to the default column of this project
+func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) {
+	c, err := p.getDefaultColumn(ctx)
+	if err != nil && !IsErrProjectColumnNotExist(err) {
+		return nil, err
+	}
+	if c != nil {
+		return c, nil
+	}
+
+	var column Column
+	has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column)
+	if err != nil {
+		return nil, err
+	}
+	if has {
+		column.Default = true
+		if _, err := db.GetEngine(ctx).ID(column.ID).Cols("`default`").Update(&column); err != nil {
+			return nil, err
+		}
+		return &column, nil
+	}
 
 	// create a default column if none is found
 	column = Column{
diff --git a/models/project/column_test.go b/models/project/column_test.go
index 566667e45d133..66db23a3e429c 100644
--- a/models/project/column_test.go
+++ b/models/project/column_test.go
@@ -20,19 +20,19 @@ func TestGetDefaultColumn(t *testing.T) {
 	assert.NoError(t, err)
 
 	// check if default column was added
-	column, err := projectWithoutDefault.GetDefaultColumn(db.DefaultContext)
+	column, err := projectWithoutDefault.MustDefaultColumn(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(5), column.ProjectID)
-	assert.Equal(t, "Uncategorized", column.Title)
+	assert.Equal(t, "Done", column.Title)
 
 	projectWithMultipleDefaults, err := GetProjectByID(db.DefaultContext, 6)
 	assert.NoError(t, err)
 
 	// check if multiple defaults were removed
-	column, err = projectWithMultipleDefaults.GetDefaultColumn(db.DefaultContext)
+	column, err = projectWithMultipleDefaults.MustDefaultColumn(db.DefaultContext)
 	assert.NoError(t, err)
 	assert.Equal(t, int64(6), column.ProjectID)
-	assert.Equal(t, int64(9), column.ID)
+	assert.Equal(t, int64(9), column.ID) // there are 2 default columns in the test data, use the latest one
 
 	// set 8 as default column
 	assert.NoError(t, SetDefaultColumn(db.DefaultContext, column.ProjectID, 8))
diff --git a/models/project/issue.go b/models/project/issue.go
index b4347a9c2b4c6..98eed2a2137b5 100644
--- a/models/project/issue.go
+++ b/models/project/issue.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -34,48 +33,6 @@ func deleteProjectIssuesByProjectID(ctx context.Context, projectID int64) error
 	return err
 }
 
-// NumIssues return counter of all issues assigned to a project
-func (p *Project) NumIssues(ctx context.Context) int {
-	c, err := db.GetEngine(ctx).Table("project_issue").
-		Where("project_id=?", p.ID).
-		GroupBy("issue_id").
-		Cols("issue_id").
-		Count()
-	if err != nil {
-		log.Error("NumIssues: %v", err)
-		return 0
-	}
-	return int(c)
-}
-
-// NumClosedIssues return counter of closed issues assigned to a project
-func (p *Project) NumClosedIssues(ctx context.Context) int {
-	c, err := db.GetEngine(ctx).Table("project_issue").
-		Join("INNER", "issue", "project_issue.issue_id=issue.id").
-		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
-		Cols("issue_id").
-		Count()
-	if err != nil {
-		log.Error("NumClosedIssues: %v", err)
-		return 0
-	}
-	return int(c)
-}
-
-// NumOpenIssues return counter of open issues assigned to a project
-func (p *Project) NumOpenIssues(ctx context.Context) int {
-	c, err := db.GetEngine(ctx).Table("project_issue").
-		Join("INNER", "issue", "project_issue.issue_id=issue.id").
-		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
-		Cols("issue_id").
-		Count()
-	if err != nil {
-		log.Error("NumOpenIssues: %v", err)
-		return 0
-	}
-	return int(c)
-}
-
 func (c *Column) moveIssuesToAnotherColumn(ctx context.Context, newColumn *Column) error {
 	if c.ProjectID != newColumn.ProjectID {
 		return fmt.Errorf("columns have to be in the same project")
diff --git a/models/project/project.go b/models/project/project.go
index 7385efd39d10b..78cba8b574c13 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -97,6 +97,9 @@ type Project struct {
 	Type         Type
 
 	RenderedContent template.HTML `xorm:"-"`
+	NumOpenIssues   int64         `xorm:"-"`
+	NumClosedIssues int64         `xorm:"-"`
+	NumIssues       int64         `xorm:"-"`
 
 	CreatedUnix    timeutil.TimeStamp `xorm:"INDEX created"`
 	UpdatedUnix    timeutil.TimeStamp `xorm:"INDEX updated"`
diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go
index 42834f6e8863b..87ce398a202d0 100644
--- a/modules/indexer/issues/db/options.go
+++ b/modules/indexer/issues/db/options.go
@@ -73,9 +73,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
 		UpdatedBeforeUnix:  options.UpdatedBeforeUnix.Value(),
 		PriorityRepoID:     0,
 		IsArchived:         options.IsArchived,
-		Org:                nil,
+		Owner:              nil,
 		Team:               nil,
-		User:               nil,
+		Doer:               nil,
 	}
 
 	if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index ebfbc22db161a..8eeb67a1acbf9 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -78,6 +78,11 @@ func Projects(ctx *context.Context) {
 		return
 	}
 
+	if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
+		ctx.ServerError("LoadIssueNumbersForProjects", err)
+		return
+	}
+
 	opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
 		OwnerID:  ctx.ContextUser.ID,
 		IsClosed: optional.Some(!isShowClosed),
@@ -328,6 +333,10 @@ func ViewProject(ctx *context.Context) {
 		ctx.NotFound("", nil)
 		return
 	}
+	if err := project.LoadOwner(ctx); err != nil {
+		ctx.ServerError("LoadOwner", err)
+		return
+	}
 
 	columns, err := project.GetColumns(ctx)
 	if err != nil {
@@ -341,14 +350,21 @@ func ViewProject(ctx *context.Context) {
 	}
 	assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
 
-	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
+	opts := issues_model.IssuesOptions{
 		LabelIDs:   labelIDs,
 		AssigneeID: optional.Some(assigneeID),
-	})
+		Owner:      project.Owner,
+		Doer:       ctx.Doer,
+	}
+
+	issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
 	if err != nil {
 		ctx.ServerError("LoadIssuesOfColumns", err)
 		return
 	}
+	for _, column := range columns {
+		column.NumIssues = int64(len(issuesMap[column.ID]))
+	}
 
 	if project.CardType != project_model.CardTypeTextOnly {
 		issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index d07ff4bbbec79..4c11ffd407e5d 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -92,6 +92,11 @@ func Projects(ctx *context.Context) {
 		return
 	}
 
+	if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
+		ctx.ServerError("LoadIssueNumbersForProjects", err)
+		return
+	}
+
 	for i := range projects {
 		rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
 		projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
@@ -312,7 +317,8 @@ func ViewProject(ctx *context.Context) {
 
 	assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
 
-	issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
+	issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
+		RepoIDs:    []int64{ctx.Repo.Repository.ID},
 		LabelIDs:   labelIDs,
 		AssigneeID: optional.Some(assigneeID),
 	})
@@ -320,6 +326,9 @@ func ViewProject(ctx *context.Context) {
 		ctx.ServerError("LoadIssuesOfColumns", err)
 		return
 	}
+	for _, column := range columns {
+		column.NumIssues = int64(len(issuesMap[column.ID]))
+	}
 
 	if project.CardType != project_model.CardTypeTextOnly {
 		issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index df9a8a6bf6ad4..c14bb2f28d52d 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -419,7 +419,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 		IsPull:     optional.Some(isPullList),
 		SortType:   sortType,
 		IsArchived: optional.Some(false),
-		User:       ctx.Doer,
+		Doer:       ctx.Doer,
 	}
 	// --------------------------------------------------------------------------
 	// Build opts (IssuesOptions), which contains filter information.
@@ -431,7 +431,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
 
 	// Get repository IDs where User/Org/Team has access.
 	if ctx.Org != nil && ctx.Org.Organization != nil {
-		opts.Org = ctx.Org.Organization
+		opts.Owner = ctx.Org.Organization.AsUser()
 		opts.Team = ctx.Org.Team
 
 		issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
diff --git a/services/projects/issue.go b/services/projects/issue.go
index 6ca0f168060ad..090d19d2f4700 100644
--- a/services/projects/issue.go
+++ b/services/projects/issue.go
@@ -11,6 +11,7 @@ import (
 	issues_model "code.gitea.io/gitea/models/issues"
 	project_model "code.gitea.io/gitea/models/project"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/optional"
 )
 
 // MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
@@ -84,3 +85,123 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
 		return nil
 	})
 }
+
+// LoadIssuesFromProject load issues assigned to each project column inside the given project
+func LoadIssuesFromProject(ctx context.Context, project *project_model.Project, opts *issues_model.IssuesOptions) (map[int64]issues_model.IssueList, error) {
+	issueList, err := issues_model.Issues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
+		o.ProjectID = project.ID
+		o.SortType = "project-column-sorting"
+	}))
+	if err != nil {
+		return nil, err
+	}
+
+	if err := issueList.LoadComments(ctx); err != nil {
+		return nil, err
+	}
+
+	defaultColumn, err := project.MustDefaultColumn(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	issueColumnMap, err := issues_model.LoadProjectIssueColumnMap(ctx, project.ID, defaultColumn.ID)
+	if err != nil {
+		return nil, err
+	}
+
+	results := make(map[int64]issues_model.IssueList)
+	for _, issue := range issueList {
+		projectColumnID, ok := issueColumnMap[issue.ID]
+		if !ok {
+			continue
+		}
+		if _, ok := results[projectColumnID]; !ok {
+			results[projectColumnID] = make(issues_model.IssueList, 0)
+		}
+		results[projectColumnID] = append(results[projectColumnID], issue)
+	}
+	return results, nil
+}
+
+// NumClosedIssues return counter of closed issues assigned to a project
+func loadNumClosedIssues(ctx context.Context, p *project_model.Project) error {
+	cnt, err := db.GetEngine(ctx).Table("project_issue").
+		Join("INNER", "issue", "project_issue.issue_id=issue.id").
+		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, true).
+		Cols("issue_id").
+		Count()
+	if err != nil {
+		return err
+	}
+	p.NumClosedIssues = cnt
+	return nil
+}
+
+// NumOpenIssues return counter of open issues assigned to a project
+func loadNumOpenIssues(ctx context.Context, p *project_model.Project) error {
+	cnt, err := db.GetEngine(ctx).Table("project_issue").
+		Join("INNER", "issue", "project_issue.issue_id=issue.id").
+		Where("project_issue.project_id=? AND issue.is_closed=?", p.ID, false).
+		Cols("issue_id").
+		Count()
+	if err != nil {
+		return err
+	}
+	p.NumOpenIssues = cnt
+	return nil
+}
+
+func LoadIssueNumbersForProjects(ctx context.Context, projects []*project_model.Project, doer *user_model.User) error {
+	for _, project := range projects {
+		if err := LoadIssueNumbersForProject(ctx, project, doer); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Project, doer *user_model.User) error {
+	// for repository project, just get the numbers
+	if project.OwnerID == 0 {
+		if err := loadNumClosedIssues(ctx, project); err != nil {
+			return err
+		}
+		if err := loadNumOpenIssues(ctx, project); err != nil {
+			return err
+		}
+		project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
+		return nil
+	}
+
+	if err := project.LoadOwner(ctx); err != nil {
+		return err
+	}
+
+	// for user or org projects, we need to check access permissions
+	opts := issues_model.IssuesOptions{
+		ProjectID: project.ID,
+		Doer:      doer,
+		AllPublic: doer == nil,
+		Owner:     project.Owner,
+	}
+
+	var err error
+	project.NumOpenIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
+		o.IsClosed = optional.Some(false)
+	}))
+	if err != nil {
+		return err
+	}
+
+	project.NumClosedIssues, err = issues_model.CountIssues(ctx, opts.Copy(func(o *issues_model.IssuesOptions) {
+		o.IsClosed = optional.Some(true)
+	}))
+	if err != nil {
+		return err
+	}
+
+	project.NumIssues = project.NumClosedIssues + project.NumOpenIssues
+
+	return nil
+}
diff --git a/services/projects/issue_test.go b/services/projects/issue_test.go
new file mode 100644
index 0000000000000..b6f0b1dae11a7
--- /dev/null
+++ b/services/projects/issue_test.go
@@ -0,0 +1,210 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	org_model "code.gitea.io/gitea/models/organization"
+	project_model "code.gitea.io/gitea/models/project"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_Projects(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+	org3 := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3})
+	user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+	t.Run("User projects", func(t *testing.T) {
+		pi1 := project_model.ProjectIssue{
+			ProjectID:       4,
+			IssueID:         1,
+			ProjectColumnID: 4,
+		}
+		err := db.Insert(db.DefaultContext, &pi1)
+		assert.NoError(t, err)
+		defer func() {
+			_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi1.ID)
+			assert.NoError(t, err)
+		}()
+
+		pi2 := project_model.ProjectIssue{
+			ProjectID:       4,
+			IssueID:         4,
+			ProjectColumnID: 4,
+		}
+		err = db.Insert(db.DefaultContext, &pi2)
+		assert.NoError(t, err)
+		defer func() {
+			_, err = db.DeleteByID[project_model.ProjectIssue](db.DefaultContext, pi2.ID)
+			assert.NoError(t, err)
+		}()
+
+		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
+			OwnerID: user2.ID,
+		})
+		assert.NoError(t, err)
+		assert.Len(t, projects, 3)
+		assert.EqualValues(t, 4, projects[0].ID)
+
+		t.Run("Authenticated user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				Owner: user2,
+				Doer:  user2,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)    // 4 has 2 issues, 6 will not contains here because 0 issues
+			assert.Len(t, columnIssues[4], 2) // user2 can visit both issues, one from public repository one from private repository
+		})
+
+		t.Run("Anonymous user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				AllPublic: true,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)
+			assert.Len(t, columnIssues[4], 1) // anonymous user can only visit public repo issues
+		})
+
+		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				Owner: user2,
+				Doer:  user4,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)
+			assert.Len(t, columnIssues[4], 1) // user4 can only visit public repo issues
+		})
+	})
+
+	t.Run("Org projects", func(t *testing.T) {
+		project1 := project_model.Project{
+			Title:        "project in an org",
+			OwnerID:      org3.ID,
+			Type:         project_model.TypeOrganization,
+			TemplateType: project_model.TemplateTypeBasicKanban,
+		}
+		err := project_model.NewProject(db.DefaultContext, &project1)
+		assert.NoError(t, err)
+		defer func() {
+			err := project_model.DeleteProjectByID(db.DefaultContext, project1.ID)
+			assert.NoError(t, err)
+		}()
+
+		column1 := project_model.Column{
+			Title:     "column 1",
+			ProjectID: project1.ID,
+		}
+		err = project_model.NewColumn(db.DefaultContext, &column1)
+		assert.NoError(t, err)
+
+		column2 := project_model.Column{
+			Title:     "column 2",
+			ProjectID: project1.ID,
+		}
+		err = project_model.NewColumn(db.DefaultContext, &column2)
+		assert.NoError(t, err)
+
+		// issue 6 belongs to private repo 3 under org 3
+		issue6 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6})
+		err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue6, user2, project1.ID, column1.ID)
+		assert.NoError(t, err)
+
+		// issue 16 belongs to public repo 16 under org 3
+		issue16 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 16})
+		err = issues_model.IssueAssignOrRemoveProject(db.DefaultContext, issue16, user2, project1.ID, column1.ID)
+		assert.NoError(t, err)
+
+		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
+			OwnerID: org3.ID,
+		})
+		assert.NoError(t, err)
+		assert.Len(t, projects, 1)
+		assert.EqualValues(t, project1.ID, projects[0].ID)
+
+		t.Run("Authenticated user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				Owner: org3.AsUser(),
+				Doer:  userAdmin,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)             // column1 has 2 issues, 6 will not contains here because 0 issues
+			assert.Len(t, columnIssues[column1.ID], 2) // user2 can visit both issues, one from public repository one from private repository
+		})
+
+		t.Run("Anonymous user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				AllPublic: true,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)
+			assert.Len(t, columnIssues[column1.ID], 1) // anonymous user can only visit public repo issues
+		})
+
+		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				Owner: org3.AsUser(),
+				Doer:  user2,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 1)
+			assert.Len(t, columnIssues[column1.ID], 1) // user4 can only visit public repo issues
+		})
+	})
+
+	t.Run("Repository projects", func(t *testing.T) {
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+		projects, err := db.Find[project_model.Project](db.DefaultContext, project_model.SearchOptions{
+			RepoID: repo1.ID,
+		})
+		assert.NoError(t, err)
+		assert.Len(t, projects, 1)
+		assert.EqualValues(t, 1, projects[0].ID)
+
+		t.Run("Authenticated user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				RepoIDs: []int64{repo1.ID},
+				Doer:    userAdmin,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 3)
+			assert.Len(t, columnIssues[1], 2)
+			assert.Len(t, columnIssues[2], 1)
+			assert.Len(t, columnIssues[3], 1)
+		})
+
+		t.Run("Anonymous user", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				AllPublic: true,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 3)
+			assert.Len(t, columnIssues[1], 2)
+			assert.Len(t, columnIssues[2], 1)
+			assert.Len(t, columnIssues[3], 1)
+		})
+
+		t.Run("Authenticated user with no permission to the private repo", func(t *testing.T) {
+			columnIssues, err := LoadIssuesFromProject(db.DefaultContext, projects[0], &issues_model.IssuesOptions{
+				RepoIDs: []int64{repo1.ID},
+				Doer:    user2,
+			})
+			assert.NoError(t, err)
+			assert.Len(t, columnIssues, 3)
+			assert.Len(t, columnIssues[1], 2)
+			assert.Len(t, columnIssues[2], 1)
+			assert.Len(t, columnIssues[3], 1)
+		})
+	})
+}
diff --git a/services/projects/main_test.go b/services/projects/main_test.go
new file mode 100644
index 0000000000000..93e4887b55bc8
--- /dev/null
+++ b/services/projects/main_test.go
@@ -0,0 +1,18 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package project
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+
+	_ "code.gitea.io/gitea/models"
+	_ "code.gitea.io/gitea/models/actions"
+	_ "code.gitea.io/gitea/models/activities"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m)
+}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index f5a48f72414c1..31a31527a92c4 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -52,11 +52,11 @@
 				<div class="group">
 					<div class="flex-text-block">
 						{{svg "octicon-issue-opened" 14}}
-						{{ctx.Locale.PrettyNumber (.NumOpenIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
+						{{ctx.Locale.PrettyNumber .NumOpenIssues}}&nbsp;{{ctx.Locale.Tr "repo.issues.open_title"}}
 					</div>
 					<div class="flex-text-block">
 						{{svg "octicon-check" 14}}
-						{{ctx.Locale.PrettyNumber (.NumClosedIssues ctx)}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
+						{{ctx.Locale.PrettyNumber .NumClosedIssues}}&nbsp;{{ctx.Locale.Tr "repo.issues.closed_title"}}
 					</div>
 				</div>
 				{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 966d3bf60445b..82d744bbc29bc 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -86,7 +86,7 @@
 			<div class="project-column"{{if .Color}} style="background: {{.Color}} !important; color: {{ContrastColor .Color}} !important"{{end}} data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
 				<div class="project-column-header{{if $canWriteProject}} tw-cursor-grab{{end}}">
 					<div class="ui circular label project-column-issue-count">
-						{{.NumIssues ctx}}
+						{{.NumIssues}}
 					</div>
 					<div class="project-column-title-label gt-ellipsis">{{.Title}}</div>
 					{{if $canWriteProject}}
diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go
index cdff9aa2fdc62..111356b1da220 100644
--- a/tests/integration/project_test.go
+++ b/tests/integration/project_test.go
@@ -78,7 +78,7 @@ func TestMoveRepoProjectColumns(t *testing.T) {
 
 	columnsAfter, err := project1.GetColumns(db.DefaultContext)
 	assert.NoError(t, err)
-	assert.Len(t, columns, 3)
+	assert.Len(t, columnsAfter, 3)
 	assert.EqualValues(t, columns[1].ID, columnsAfter[0].ID)
 	assert.EqualValues(t, columns[2].ID, columnsAfter[1].ID)
 	assert.EqualValues(t, columns[0].ID, columnsAfter[2].ID)

From b2707bcd183abcf0eb64adc81c56a062d97cf518 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 18 Feb 2025 10:46:08 +0800
Subject: [PATCH 118/206] Make actions URL in commit status webhooks absolute
 (#33620) (#33632)

Backport #33620 by lunny

Fix #33603

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/webhook/notifier.go | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 52a69d53b6960..335eabedd5111 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -15,6 +15,7 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
@@ -875,6 +876,11 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod
 		return
 	}
 
+	// as a webhook url, target should be an absolute url. But for internal actions target url
+	// the target url is a url path with no host and port to make it easy to be visited
+	// from multiple hosts. So we need to convert it to an absolute url here.
+	target := httplib.MakeAbsoluteURL(ctx, status.TargetURL)
+
 	payload := api.CommitStatusPayload{
 		Context:     status.Context,
 		CreatedAt:   status.CreatedUnix.AsTime().UTC(),
@@ -882,7 +888,7 @@ func (m *webhookNotifier) CreateCommitStatus(ctx context.Context, repo *repo_mod
 		ID:          status.ID,
 		SHA:         commit.Sha1,
 		State:       status.State.String(),
-		TargetURL:   status.TargetURL,
+		TargetURL:   target,
 
 		Commit: apiCommit,
 		Repo:   convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),

From ee3f5e8fac7cd3ccb6810d61534e8a8f413b2cf8 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 18 Feb 2025 16:57:14 -0800
Subject: [PATCH 119/206] fix: add missing locale (#33641) (#33642)

---
 options/locale/locale_en-US.ini | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fd05ae7614948..9d71ccb6d5314 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1690,6 +1690,8 @@ issues.start_tracking_history = started working %s
 issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
 issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
 issues.stop_tracking_history = worked for <b>%s</b> %s
+issues.stop_tracking = Stop Timer
+issues.cancel_tracking = Discard
 issues.cancel_tracking_history = `canceled time tracking %s`
 issues.del_time = Delete this time log
 issues.add_time_history = added spent time <b>%s</b> %s

From de7026528b974ac36f8d3add7f51f82497c2d2ea Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 18 Feb 2025 17:16:26 -0800
Subject: [PATCH 120/206] Release of Gitea 1.23.4 (#33621)

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
---
 CHANGELOG.md | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e04da654a97ae..3bb447f091ec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,28 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
-## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-05
+## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
+
+* SECURITY
+  * Enhance routers for the Actions variable operations (#33547) (#33553)
+  * Enhance routers for the Actions runner operations (#33549) (#33555)
+  * Fix project issues list and counting (#33594) #33619
+* PERFORMANCES
+  * Performance optimization for pull request files loading comments attachments (#33585) (#33592)
+* BUGFIXES
+  * Add a transaction to `pickTask` (#33543) (#33563)
+  * Fix mirror bug (#33597) (#33607)
+  * Use default Git timeout when checking repo health (#33593) (#33598)
+  * Fix PR's target branch dropdown (#33589) (#33591)
+  * Fix various problems (artifact order, api empty slice, assignee check, fuzzy prompt, mirror proxy, adopt git) (#33569) (#33577)
+  * Rework suggestion backend (#33538) (#33546)
+  * Fix context usage (#33554) (#33557)
+  * Only show the latest version in the Arch index (#33262) (#33580)
+  * Skip deletion error for action artifacts (#33476) (#33568)
+  * Make actions URL in commit status webhooks absolute (#33620) #33632
+  * Add missing locale (#33641) #33642
+
+## [1.23.3](https://github.com/go-gitea/gitea/releases/tag/v1.23.3) - 2025-02-06
 
 * Security
   * Build Gitea with Golang v1.23.6 to fix security bugs

From 8844e62cb4fc4eb8be9d4a6be6da1fb10d62fa1e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 20 Feb 2025 09:11:43 +0800
Subject: [PATCH 121/206] git graph: don't show detached commits (#33645)
 (#33650)

Backport #33645 by ericLemanissier

Co-authored-by: ericLemanissier <ericLemanissier@users.noreply.github.com>
---
 modules/gitgraph/graph.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/gitgraph/graph.go b/modules/gitgraph/graph.go
index 7e12be030fb35..2628a1a55af7e 100644
--- a/modules/gitgraph/graph.go
+++ b/modules/gitgraph/graph.go
@@ -29,7 +29,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
 	}
 
 	if len(branches) == 0 {
-		graphCmd.AddArguments("--all")
+		graphCmd.AddArguments("--tags", "--branches")
 	}
 
 	graphCmd.AddArguments("-C", "-M", "--date=iso-strict").

From 9da6d4ea853a620eadbc024b1eda8e65e8e4948b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 21 Feb 2025 00:21:31 +0800
Subject: [PATCH 122/206] Fix mCaptcha bug (#33659) (#33661)

Backport #33659 by wxiaoguang

Fix #33658

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/form.css           |  2 +-
 web_src/js/features/captcha.ts | 13 +++++++++----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/web_src/css/form.css b/web_src/css/form.css
index 26f00e4d994e9..924bcb8c2f600 100644
--- a/web_src/css/form.css
+++ b/web_src/css/form.css
@@ -182,7 +182,7 @@ textarea:focus,
     height: 76px !important;
   }
   .m-captcha-style {
-    width: 50%;
+    max-width: 450px;
   }
 }
 
diff --git a/web_src/js/features/captcha.ts b/web_src/js/features/captcha.ts
index 69b4aa6852fca..df234d0e5c85e 100644
--- a/web_src/js/features/captcha.ts
+++ b/web_src/js/features/captcha.ts
@@ -34,13 +34,18 @@ export async function initCaptcha() {
       break;
     }
     case 'm-captcha': {
-      const {default: mCaptcha} = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
-      // @ts-expect-error
+      const mCaptcha = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue');
+
+      // FIXME: the mCaptcha code is not right, it's a miracle that the wrong code could run
+      // * the "vanilla-glue" has some problems with es6 module.
+      // * the INPUT_NAME is a "const", it should not be changed.
+      // * the "mCaptcha.default" is actually the "Widget".
+
+      // @ts-expect-error TS2540: Cannot assign to 'INPUT_NAME' because it is a read-only property.
       mCaptcha.INPUT_NAME = 'm-captcha-response';
       const instanceURL = captchaEl.getAttribute('data-instance-url');
 
-      // @ts-expect-error
-      mCaptcha.default({
+      new mCaptcha.default({
         siteKey: {
           instanceUrl: new URL(instanceURL),
           key: siteKey,

From 37e99d9b34aa9cece308c3cf484a5c13d4c1a83d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 21 Feb 2025 16:08:05 +0800
Subject: [PATCH 123/206] Fix omitempty bug (#33663) (#33670)

Backport #33663 by @lunny

Fix #33660

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/admin/task.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/models/admin/task.go b/models/admin/task.go
index 10f8e6d57046d..0541a8ec78ebd 100644
--- a/models/admin/task.go
+++ b/models/admin/task.go
@@ -44,7 +44,7 @@ func init() {
 // TranslatableMessage represents JSON struct that can be translated with a Locale
 type TranslatableMessage struct {
 	Format string
-	Args   []any `json:"omitempty"`
+	Args   []any `json:",omitempty"`
 }
 
 // LoadRepo loads repository of the task

From aeeccc9642b248e27e6802b14de220e942336e1b Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 21 Feb 2025 16:38:15 +0800
Subject: [PATCH 124/206] Deleting repository should unlink all related
 packages (#33653) (#33673)

Backport #33653 by @lunny

Fix #33634

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/repository/delete.go     | 6 ++++++
 services/repository/repository.go | 7 +------
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/services/repository/delete.go b/services/repository/delete.go
index aa1bbf884c684..96b18ac811def 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -16,6 +16,7 @@ import (
 	git_model "code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
+	packages_model "code.gitea.io/gitea/models/packages"
 	access_model "code.gitea.io/gitea/models/perm/access"
 	project_model "code.gitea.io/gitea/models/project"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -273,6 +274,11 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 		return err
 	}
 
+	// unlink packages linked to this repository
+	if err = packages_model.UnlinkRepositoryFromAllPackages(ctx, repoID); err != nil {
+		return err
+	}
+
 	if err = committer.Commit(); err != nil {
 		return err
 	}
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 59b4491132da9..fcc617979ef29 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -11,7 +11,6 @@ import (
 	"code.gitea.io/gitea/models/git"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
-	packages_model "code.gitea.io/gitea/models/packages"
 	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/models/unit"
@@ -63,11 +62,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod
 		notify_service.DeleteRepository(ctx, doer, repo)
 	}
 
-	if err := DeleteRepositoryDirectly(ctx, doer, repo.ID); err != nil {
-		return err
-	}
-
-	return packages_model.UnlinkRepositoryFromAllPackages(ctx, repo.ID)
+	return DeleteRepositoryDirectly(ctx, doer, repo.ID)
 }
 
 // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace

From ae595aa91382cb998dc43a3f565337c75ffc6297 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 22 Feb 2025 01:57:17 +0800
Subject: [PATCH 125/206] Improve Open-with URL encoding (#33666) (#33680)

Backport #33666 by wxiaoguang

Fix #33665

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/features/repo-common.test.ts |  7 +++++++
 web_src/js/features/repo-common.ts      | 10 +++++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 web_src/js/features/repo-common.test.ts

diff --git a/web_src/js/features/repo-common.test.ts b/web_src/js/features/repo-common.test.ts
new file mode 100644
index 0000000000000..009dfc86b1016
--- /dev/null
+++ b/web_src/js/features/repo-common.test.ts
@@ -0,0 +1,7 @@
+import {substituteRepoOpenWithUrl} from './repo-common.ts';
+
+test('substituteRepoOpenWithUrl', () => {
+  // For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea"
+  expect(substituteRepoOpenWithUrl('proto://a/{url}', 'https://gitea')).toEqual('proto://a/https://gitea');
+  expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea');
+});
diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts
index 86e14585c7620..2fe8270793a39 100644
--- a/web_src/js/features/repo-common.ts
+++ b/web_src/js/features/repo-common.ts
@@ -42,6 +42,14 @@ export function initRepoActivityTopAuthorsChart() {
   }
 }
 
+export function substituteRepoOpenWithUrl(tmpl: string, url: string): string {
+  const pos = tmpl.indexOf('{url}');
+  if (pos === -1) return tmpl;
+  const posQuestionMark = tmpl.indexOf('?');
+  const needEncode = posQuestionMark >= 0 && posQuestionMark < pos;
+  return tmpl.replace('{url}', needEncode ? encodeURIComponent(url) : url);
+}
+
 function initCloneSchemeUrlSelection(parent: Element) {
   const elCloneUrlInput = parent.querySelector<HTMLInputElement>('.repo-clone-url');
 
@@ -70,7 +78,7 @@ function initCloneSchemeUrlSelection(parent: Element) {
       }
     }
     for (const el of parent.querySelectorAll<HTMLAnchorElement>('.js-clone-url-editor')) {
-      el.href = el.getAttribute('data-href-template').replace('{url}', encodeURIComponent(link));
+      el.href = substituteRepoOpenWithUrl(el.getAttribute('data-href-template'), link);
     }
   };
 

From b5007c61547518406f6f580336805bb1a6f38d33 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 22 Feb 2025 03:37:42 +0800
Subject: [PATCH 126/206] Fix for Maven Package Naming Convention Handling
 (#33678) (#33679)

Backport #33678 by dianaStr7

Co-authored-by: Diana <80010947+dianaStr7@users.noreply.github.com>
Co-authored-by: diana.strebkova@t-systems.com <diana.strebkova@t-systems.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/api/packages/maven/api.go            |  9 +++------
 routers/api/packages/maven/maven.go          | 17 ++++++++---------
 tests/integration/api_packages_maven_test.go | 14 ++++++++++++++
 3 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/routers/api/packages/maven/api.go b/routers/api/packages/maven/api.go
index 167fe42b56d51..ec6b9cfb0e05b 100644
--- a/routers/api/packages/maven/api.go
+++ b/routers/api/packages/maven/api.go
@@ -8,7 +8,6 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
-	maven_module "code.gitea.io/gitea/modules/packages/maven"
 )
 
 // MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html
@@ -22,7 +21,7 @@ type MetadataResponse struct {
 }
 
 // pds is expected to be sorted ascending by CreatedUnix
-func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse {
+func createMetadataResponse(pds []*packages_model.PackageDescriptor, groupID, artifactID string) *MetadataResponse {
 	var release *packages_model.PackageDescriptor
 
 	versions := make([]string, 0, len(pds))
@@ -35,11 +34,9 @@ func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataRe
 
 	latest := pds[len(pds)-1]
 
-	metadata := latest.Metadata.(*maven_module.Metadata)
-
 	resp := &MetadataResponse{
-		GroupID:    metadata.GroupID,
-		ArtifactID: metadata.ArtifactID,
+		GroupID:    groupID,
+		ArtifactID: artifactID,
 		Latest:     latest.Version.Version,
 		Version:    versions,
 	}
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 4d04d4d1e9f32..4f9ced25b4bba 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -84,20 +84,19 @@ func handlePackageFile(ctx *context.Context, serveContent bool) {
 }
 
 func serveMavenMetadata(ctx *context.Context, params parameters) {
-	// /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
-
-	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName())
-	if errors.Is(err, util.ErrNotExist) {
-		pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
-	}
+	// path pattern: /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512]
+	// in case there are legacy package names ("GroupID-ArtifactID") we need to check both, new packages always use ":" as separator("GroupID:ArtifactID")
+	pvsLegacy, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy())
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
 		return
 	}
-	if len(pvs) == 0 {
-		apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
+	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName())
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
 		return
 	}
+	pvs = append(pvsLegacy, pvs...)
 
 	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 	if err != nil {
@@ -110,7 +109,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 		return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix
 	})
 
-	xmlMetadata, err := xml.Marshal(createMetadataResponse(pds))
+	xmlMetadata, err := xml.Marshal(createMetadataResponse(pds, params.GroupID, params.ArtifactID))
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
 		return
diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go
index 486a5af93e17b..408c8805c2412 100644
--- a/tests/integration/api_packages_maven_test.go
+++ b/tests/integration/api_packages_maven_test.go
@@ -80,6 +80,7 @@ func TestPackageMaven(t *testing.T) {
 	t.Run("UploadLegacy", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
+		// try to upload a package with legacy package name (will be saved as "GroupID-ArtifactID")
 		legacyRootLink := "/api/packages/user2/maven/com/gitea/legacy-project"
 		req := NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.2/any-file-name?use_legacy_package_name=1", strings.NewReader("test-content")).AddBasicAuth(user.Name)
 		MakeRequest(t, req, http.StatusCreated)
@@ -97,6 +98,13 @@ func TestPackageMaven(t *testing.T) {
 		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
 		MakeRequest(t, req, http.StatusNotFound)
 
+		// legacy package names should also be able to be listed
+		req = NewRequest(t, "GET", legacyRootLink+"/maven-metadata.xml").AddBasicAuth(user.Name)
+		resp := MakeRequest(t, req, http.StatusOK)
+		respBody := resp.Body.String()
+		assert.Contains(t, respBody, "<version>1.0.2</version>")
+
+		// then upload a package with correct package name (will be saved as "GroupID:ArtifactID")
 		req = NewRequestWithBody(t, "PUT", legacyRootLink+"/1.0.3/any-file-name", strings.NewReader("test-content")).AddBasicAuth(user.Name)
 		MakeRequest(t, req, http.StatusCreated)
 		_, err = packages.GetPackageByName(db.DefaultContext, user.ID, packages.TypeMaven, "com.gitea-legacy-project")
@@ -114,6 +122,12 @@ func TestPackageMaven(t *testing.T) {
 		req = NewRequest(t, "GET", "/user2/-/packages/maven/com.gitea%3Alegacy-project/1.0.2")
 		MakeRequest(t, req, http.StatusOK)
 
+		// now 2 packages should be listed
+		req = NewRequest(t, "GET", legacyRootLink+"/maven-metadata.xml").AddBasicAuth(user.Name)
+		resp = MakeRequest(t, req, http.StatusOK)
+		respBody = resp.Body.String()
+		assert.Contains(t, respBody, "<version>1.0.2</version>")
+		assert.Contains(t, respBody, "<version>1.0.3</version>")
 		require.NoError(t, packages.DeletePackageByID(db.DefaultContext, p.ID))
 	})
 

From 039924aa2a9f64f44a827d9dae7f6001edeb7701 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 23 Feb 2025 20:58:10 +0800
Subject: [PATCH 127/206] Try to fix ACME path when renew (#33668) (#33693)

Backport #33668 by wxiaoguang

Try to fix #32191

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 cmd/web_acme.go           | 16 ++++++++++------
 modules/setting/server.go | 25 ++++++++++++++++---------
 2 files changed, 26 insertions(+), 15 deletions(-)

diff --git a/cmd/web_acme.go b/cmd/web_acme.go
index 5daf0f55f243f..bca4ae021217b 100644
--- a/cmd/web_acme.go
+++ b/cmd/web_acme.go
@@ -54,10 +54,6 @@ func runACME(listenAddr string, m http.Handler) error {
 		altTLSALPNPort = p
 	}
 
-	// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
-	// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
-	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
-	magic := certmagic.NewDefault()
 	// Try to use private CA root if provided, otherwise defaults to system's trust
 	var certPool *x509.CertPool
 	if setting.AcmeCARoot != "" {
@@ -67,7 +63,13 @@ func runACME(listenAddr string, m http.Handler) error {
 			log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
 		}
 	}
-	myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
+	// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
+	// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
+	// And one more thing, no idea why we should set the global default variables here
+	// But it seems that the current ACME code needs these global variables to make renew work.
+	// Otherwise, "renew" will use incorrect storage path
+	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
+	certmagic.DefaultACME = certmagic.ACMEIssuer{
 		CA:                      setting.AcmeURL,
 		TrustedRoots:            certPool,
 		Email:                   setting.AcmeEmail,
@@ -77,8 +79,10 @@ func runACME(listenAddr string, m http.Handler) error {
 		ListenHost:              setting.HTTPAddr,
 		AltTLSALPNPort:          altTLSALPNPort,
 		AltHTTPPort:             altHTTPPort,
-	})
+	}
 
+	magic := certmagic.NewDefault()
+	myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
 	magic.Issuers = []certmagic.Issuer{myACME}
 
 	// this obtains certificates or renews them if necessary
diff --git a/modules/setting/server.go b/modules/setting/server.go
index d7a71578d4ab6..e15b790906738 100644
--- a/modules/setting/server.go
+++ b/modules/setting/server.go
@@ -169,20 +169,24 @@ func loadServerFrom(rootCfg ConfigProvider) {
 	HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
 	HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
 
+	// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
+	// if these are removed, the warning will not be shown
+	if sec.HasKey("ENABLE_ACME") {
+		EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
+	} else {
+		deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
+		EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
+	}
+
 	Protocol = HTTP
 	protocolCfg := sec.Key("PROTOCOL").String()
+	if protocolCfg != "https" && EnableAcme {
+		log.Fatal("ACME could only be used with HTTPS protocol")
+	}
+
 	switch protocolCfg {
 	case "https":
 		Protocol = HTTPS
-
-		// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
-		// if these are removed, the warning will not be shown
-		if sec.HasKey("ENABLE_ACME") {
-			EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
-		} else {
-			deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
-			EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
-		}
 		if EnableAcme {
 			AcmeURL = sec.Key("ACME_URL").MustString("")
 			AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
@@ -210,6 +214,9 @@ func loadServerFrom(rootCfg ConfigProvider) {
 				deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
 				AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
 			}
+			if AcmeEmail == "" {
+				log.Fatal("ACME Email is not set (ACME_EMAIL).")
+			}
 		} else {
 			CertFile = sec.Key("CERT_FILE").String()
 			KeyFile = sec.Key("KEY_FILE").String()

From 1c7339e385764f4e53be0cbda5043d2915b49277 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 24 Feb 2025 22:13:45 +0800
Subject: [PATCH 128/206] Fix OCI image.version annotation for releases to use
 full semver (#33698) (#33701)

---
 .github/workflows/release-tag-version.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml
index f67b76f40873b..08bb9baecfebb 100644
--- a/.github/workflows/release-tag-version.yml
+++ b/.github/workflows/release-tag-version.yml
@@ -88,9 +88,9 @@ jobs:
           # 1.2
           # 1.2.3
           tags: |
+            type=semver,pattern={{version}}
             type=semver,pattern={{major}}
             type=semver,pattern={{major}}.{{minor}}
-            type=semver,pattern={{version}}
       - name: Login to Docker Hub
         uses: docker/login-action@v3
         with:
@@ -126,9 +126,9 @@ jobs:
           # 1.2
           # 1.2.3
           tags: |
+            type=semver,pattern={{version}}
             type=semver,pattern={{major}}
             type=semver,pattern={{major}}.{{minor}}
-            type=semver,pattern={{version}}
       - name: Login to Docker Hub
         uses: docker/login-action@v3
         with:

From 81126daf53428a447d69b7311710ee330e4ed3c7 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 25 Feb 2025 12:34:03 +0800
Subject: [PATCH 129/206] Optimize user dashboard loading (#33686) (#33708)

Backport #33686 by @lunny

Fix #33582
Fix #31698

When a user login, the dashboard should load all feed belongs to him
with no any conditions. The complicated conditions should be applied
only for another user view this user's profile.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/activities/action.go      | 30 +++++++++++++++++++-----------
 models/activities/action_list.go | 28 +++++++++++++++++++++++++---
 2 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/models/activities/action.go b/models/activities/action.go
index 4c71810c61777..7432e073b929d 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -454,6 +454,24 @@ func ActivityReadable(user, doer *user_model.User) bool {
 		doer != nil && (doer.IsAdmin || user.ID == doer.ID)
 }
 
+func FeedDateCond(opts GetFeedsOptions) builder.Cond {
+	cond := builder.NewCond()
+	if opts.Date == "" {
+		return cond
+	}
+
+	dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
+	if err != nil {
+		log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
+	} else {
+		dateHigh := dateLow.Add(86399000000000) // 23h59m59s
+
+		cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
+		cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
+	}
+	return cond
+}
+
 func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
 	cond := builder.NewCond()
 
@@ -534,17 +552,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
 		cond = cond.And(builder.Eq{"is_deleted": false})
 	}
 
-	if opts.Date != "" {
-		dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
-		if err != nil {
-			log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
-		} else {
-			dateHigh := dateLow.Add(86399000000000) // 23h59m59s
-
-			cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
-			cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
-		}
-	}
+	cond = cond.And(FeedDateCond(opts))
 
 	return cond, nil
 }
diff --git a/models/activities/action_list.go b/models/activities/action_list.go
index 5f9acb8f2aa46..f7ea48f03e7ab 100644
--- a/models/activities/action_list.go
+++ b/models/activities/action_list.go
@@ -208,9 +208,31 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
 		return nil, 0, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
 	}
 
-	cond, err := ActivityQueryCondition(ctx, opts)
-	if err != nil {
-		return nil, 0, err
+	var err error
+	var cond builder.Cond
+	// if the actor is the requested user or is an administrator, we can skip the ActivityQueryCondition
+	if opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) {
+		cond = builder.Eq{
+			"user_id": opts.RequestedUser.ID,
+		}.And(
+			FeedDateCond(opts),
+		)
+
+		if !opts.IncludeDeleted {
+			cond = cond.And(builder.Eq{"is_deleted": false})
+		}
+
+		if !opts.IncludePrivate {
+			cond = cond.And(builder.Eq{"is_private": false})
+		}
+		if opts.OnlyPerformedBy {
+			cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
+		}
+	} else {
+		cond, err = ActivityQueryCondition(ctx, opts)
+		if err != nil {
+			return nil, 0, err
+		}
 	}
 
 	actions := make([]*Action, 0, opts.PageSize)

From e3021fae79f31907a3b5be512397ede547b74c02 Mon Sep 17 00:00:00 2001
From: Jimmy Praet <jimmy.praet@telenet.be>
Date: Tue, 25 Feb 2025 21:20:54 +0100
Subject: [PATCH 130/206] Use MatchPhraseQuery for bleve code search (#33628)

Fix regression from #32210 which unintentionally changed the search mode
for bleve from MaatchPhraseQuery to MatchQuery.

On the main branch, meanwhile with #33590 a "literal code search" mode
(by using quotes) was implemented as workaround for this unexpected code
search behavior. Maybe that feature needs some redesign as it turns out
to have been caused by a regression.

But this PR at least already fixes the regression for 1.23.x
---
 modules/indexer/code/bleve/bleve.go  |  2 +-
 modules/indexer/code/indexer_test.go | 70 ++++++++++++++++------------
 2 files changed, 42 insertions(+), 30 deletions(-)

diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 772317fa594ba..981fe75c3d596 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -266,7 +266,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
 	pathQuery.FieldVal = "Filename"
 	pathQuery.SetBoost(10)
 
-	contentQuery := bleve.NewMatchQuery(opts.Keyword)
+	contentQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
 	contentQuery.FieldVal = "Content"
 
 	if opts.IsKeywordFuzzy {
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index f358bbe785110..48afdd1a71197 100644
--- a/modules/indexer/code/indexer_test.go
+++ b/modules/indexer/code/indexer_test.go
@@ -165,35 +165,6 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 					},
 				},
 			},
-			// Search for matches on the contents of files within the repo '62'.
-			// This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one)
-			{
-				RepoIDs: []int64{62},
-				Keyword: "This is not cheese",
-				Langs:   1,
-				Results: []codeSearchResult{
-					{
-						Filename: "potato/ham.md",
-						Content:  "This is not cheese",
-					},
-					{
-						Filename: "ham.md",
-						Content:  "This is also not cheese",
-					},
-				},
-			},
-			// Search for matches on the contents of files regardless of case.
-			{
-				RepoIDs: nil,
-				Keyword: "dESCRIPTION",
-				Langs:   1,
-				Results: []codeSearchResult{
-					{
-						Filename: "README.md",
-						Content:  "# repo1\n\nDescription for repo1",
-					},
-				},
-			},
 			// Search for an exact match on the filename within the repo '62' (case insenstive).
 			// This scenario yields a single result (the file avocado.md on the repo '62')
 			{
@@ -233,6 +204,47 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
 			},
 		}
 
+		if name == "elastic_search" {
+			// Additional scenarios for elastic_search only
+			additional := []struct {
+				RepoIDs []int64
+				Keyword string
+				Langs   int
+				Results []codeSearchResult
+			}{
+				// Search for matches on the contents of files within the repo '62'.
+				// This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one)
+				{
+					RepoIDs: []int64{62},
+					Keyword: "This is not cheese",
+					Langs:   1,
+					Results: []codeSearchResult{
+						{
+							Filename: "potato/ham.md",
+							Content:  "This is not cheese",
+						},
+						{
+							Filename: "ham.md",
+							Content:  "This is also not cheese",
+						},
+					},
+				},
+				// Search for matches on the contents of files regardless of case.
+				{
+					RepoIDs: nil,
+					Keyword: "dESCRIPTION",
+					Langs:   1,
+					Results: []codeSearchResult{
+						{
+							Filename: "README.md",
+							Content:  "# repo1\n\nDescription for repo1",
+						},
+					},
+				},
+			}
+			keywords = append(keywords, additional...)
+		}
+
 		for _, kw := range keywords {
 			t.Run(kw.Keyword, func(t *testing.T) {
 				total, res, langs, err := indexer.Search(context.TODO(), &internal.SearchOptions{

From 27a60fd91b4046b52f330719eafbdd1f2fbc319d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 26 Feb 2025 09:29:07 +0800
Subject: [PATCH 131/206] Fix inconsistent closed issue list icon (#33722)
 (#33728)

Backport #33722 by @lunny

Fixe #33718

Before


![image](https://github.com/user-attachments/assets/2c77e249-a118-4471-8c63-ead4fe0f6336)


After


![image](https://github.com/user-attachments/assets/c082eba8-5b21-4814-b091-c725ca46ccf3)

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 templates/repo/issue/openclose.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index b9dd04a7db226..00a31b5fad91a 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -17,7 +17,7 @@
 		{{ctx.Locale.PrettyNumber .OpenCount}} {{ctx.Locale.Tr "repo.issues.open_title"}}
 	</a>
 	<a class="{{if eq .State "closed"}}active {{end}}item flex-text-inline" href="{{if eq .State "closed"}}{{$allStatesLink}}{{else}}{{$closedLink}}{{end}}">
-		{{svg "octicon-check"}}
+		{{svg "octicon-issue-closed"}}
 		{{ctx.Locale.PrettyNumber .ClosedCount}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
 	</a>
 </div>

From 7ded86f5afe2715fb630c94b589e93b2c8a44cd8 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 1 Mar 2025 01:38:27 +0800
Subject: [PATCH 132/206] Remove superflous tw-content-center (#33741) (#33743)

Backport #33741 by @silverwind

Co-authored-by: silverwind <me@silverwind.io>
---
 templates/base/paginate.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/base/paginate.tmpl b/templates/base/paginate.tmpl
index 9a7a6322f7924..253892c009231 100644
--- a/templates/base/paginate.tmpl
+++ b/templates/base/paginate.tmpl
@@ -17,7 +17,7 @@
 					{{if eq .Num -1}}
 						<a class="disabled item">...</a>
 					{{else}}
-						<a class="{{if .IsCurrent}}active {{end}}item tw-content-center" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
+						<a class="{{if .IsCurrent}}active {{end}}item" {{if not .IsCurrent}}href="{{$paginationLink}}?page={{.Num}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>{{.Num}}</a>
 					{{end}}
 				{{end}}
 				<a class="{{if not .HasNext}}disabled{{end}} item navigation" {{if .HasNext}}href="{{$paginationLink}}?page={{.Next}}{{if $paginationParams}}&{{$paginationParams}}{{end}}"{{end}}>

From d634e7576fced4a9ca6e69efd5e7950f488542ed Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Fri, 28 Feb 2025 12:46:54 -0500
Subject: [PATCH 133/206] bump x/oauth2 & x/crypto (#33704) (#33727)

Backport dep bump

---------

Co-authored-by: silverwind <me@silverwind.io>
---
 go.mod | 14 ++++++++------
 go.sum | 24 ++++++++++++------------
 2 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/go.mod b/go.mod
index 422952af9e2db..f7b16ce9aed33 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,8 @@
 module code.gitea.io/gitea
 
-go 1.23
+go 1.23.0
+
+toolchain go1.23.6
 
 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
 // But some CAs use negative serial number, just relax the check. related:
@@ -118,13 +120,13 @@ require (
 	github.com/yuin/goldmark v1.7.8
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.32.0
+	golang.org/x/crypto v0.35.0
 	golang.org/x/image v0.21.0
 	golang.org/x/net v0.34.0
-	golang.org/x/oauth2 v0.23.0
-	golang.org/x/sync v0.10.0
-	golang.org/x/sys v0.29.0
-	golang.org/x/text v0.21.0
+	golang.org/x/oauth2 v0.27.0
+	golang.org/x/sync v0.11.0
+	golang.org/x/sys v0.30.0
+	golang.org/x/text v0.22.0
 	golang.org/x/tools v0.29.0
 	google.golang.org/grpc v1.67.1
 	google.golang.org/protobuf v1.35.1
diff --git a/go.sum b/go.sum
index e7f6b7c6d9993..7ff8d437db475 100644
--- a/go.sum
+++ b/go.sum
@@ -833,8 +833,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
@@ -871,8 +871,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
+golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -884,8 +884,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -919,8 +919,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -932,8 +932,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -944,8 +944,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

From 8939c3845a5f4207481d2dfae4fd2b04f62d62dc Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 1 Mar 2025 09:13:19 +0800
Subject: [PATCH 134/206] Disable go license generation as part of `make tidy`
 (#33747) (#33751)

Backport #33747 by @silverwind

It seems something broken `google/go-licenses` (maybe related to go
1.24), and my findings are in
https://github.com/google/go-licenses/issues/128#issuecomment-2689753365.
I think it's best we disable this generation for now until a better
solution is found.

Also, enable showing stderr output so we can actually debug this thing.
For reference, these are the errors that currently apparently break the
tool:

```
E0228 05:15:27.005759   13158 library.go:117] Package text/tabwriter does not have module info. Non go modules projects are no longer supported. For feedback, refer to https://github.com/google/go-licenses/issues/128.
E0228 05:15:27.005776   13158 library.go:117] Package net/http/fcgi does not have module info. Non go modules projects are no longer supported. For feedback, refer to https://github.com/google/go-licenses/issues/128.
F0228 05:15:27.028122   13158 main.go:77] some errors occurred when loading direct and transitive dependency packages
```

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
---
 Makefile | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index 5524cfb08cdf7..169e2a21757da 100644
--- a/Makefile
+++ b/Makefile
@@ -508,7 +508,8 @@ unit-test-coverage:
 tidy:
 	$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
 	$(GO) mod tidy -compat=$(MIN_GO_VERSION)
-	@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
+# disabled because of issues related to https://github.com/google/go-licenses/issues/128
+# 	@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
 
 vendor: go.mod go.sum
 	$(GO) mod vendor
@@ -527,7 +528,7 @@ tidy-check: tidy
 go-licenses: $(GO_LICENSE_FILE)
 
 $(GO_LICENSE_FILE): go.mod go.sum
-	-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
+	-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR)
 	$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
 	@rm -rf $(GO_LICENSE_TMP_DIR)
 

From 730742230f576d213a0b23ce3ed454f15b691f7f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sat, 1 Mar 2025 06:23:55 -0800
Subject: [PATCH 135/206] upgrade go-crypto from 1.1.4 to 1.1.6 (#33745)
 (#33754)

---
 Makefile | 5 ++---
 go.mod   | 6 ++----
 go.sum   | 4 ++--
 3 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/Makefile b/Makefile
index 169e2a21757da..ba98f72550cdf 100644
--- a/Makefile
+++ b/Makefile
@@ -508,8 +508,7 @@ unit-test-coverage:
 tidy:
 	$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
 	$(GO) mod tidy -compat=$(MIN_GO_VERSION)
-# disabled because of issues related to https://github.com/google/go-licenses/issues/128
-# 	@$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
+	$(MAKE) --no-print-directory $(GO_LICENSE_FILE)
 
 vendor: go.mod go.sum
 	$(GO) mod vendor
@@ -528,7 +527,7 @@ tidy-check: tidy
 go-licenses: $(GO_LICENSE_FILE)
 
 $(GO_LICENSE_FILE): go.mod go.sum
-	-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR)
+	-$(GO) run $(GO_LICENSES_PACKAGE) save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
 	$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
 	@rm -rf $(GO_LICENSE_TMP_DIR)
 
diff --git a/go.mod b/go.mod
index f7b16ce9aed33..1f20e162cd836 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,6 @@
 module code.gitea.io/gitea
 
-go 1.23.0
-
-toolchain go1.23.6
+go 1.23.6
 
 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
 // But some CAs use negative serial number, just relax the check. related:
@@ -26,7 +24,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
-	github.com/ProtonMail/go-crypto v1.1.4
+	github.com/ProtonMail/go-crypto v1.1.6
 	github.com/PuerkitoBio/goquery v1.10.0
 	github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.3
 	github.com/alecthomas/chroma/v2 v2.15.0
diff --git a/go.sum b/go.sum
index 7ff8d437db475..c9b188b9c0b68 100644
--- a/go.sum
+++ b/go.sum
@@ -71,8 +71,8 @@ github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSC
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProtonMail/go-crypto v1.1.4 h1:G5U5asvD5N/6/36oIw3k2bOfBn5XVcZrb7PBjzzKKoE=
-github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
+github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
 github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
 github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=

From 2cc76009dc689f9d2b50e72790b2d5541008607f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 4 Mar 2025 16:52:54 +0800
Subject: [PATCH 136/206] Fix navbar dropdown item align (#33782)

1.23 only

Fix #33781
---
 web_src/css/modules/navbar.css | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index 73914d9a21590..1137e46c7fd3e 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -47,10 +47,6 @@
   display: flex;
 }
 
-#navbar .dropdown .item {
-  justify-content: stretch;
-}
-
 #navbar a.item:hover,
 #navbar button.item:hover {
   background: var(--color-nav-hover-bg);

From b8a7c2047483c95f9a043d28f235dcfb278e73bf Mon Sep 17 00:00:00 2001
From: techknowlogick <techknowlogick@gitea.com>
Date: Tue, 4 Mar 2025 15:09:07 -0500
Subject: [PATCH 137/206] Update minimum version of mssql tested

---
 .github/workflows/pull-db-tests.yml | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index 0b23de0a66c01..255552e3aaab1 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -202,12 +202,11 @@ jobs:
   test-mssql:
     if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
     needs: files-changed
-    # specifying the version of ubuntu in use as mssql fails on newer kernels
-    # pending resolution from vendor
-    runs-on: ubuntu-20.04
+    # NOTE: mssql-2017 docker image will panic when run on hosts that have Ubuntu newer than 20.04
+    runs-on: ubuntu-latest
     services:
       mssql:
-        image: mcr.microsoft.com/mssql/server:2017-latest
+        image: mcr.microsoft.com/mssql/server:2019-latest
         env:
           ACCEPT_EULA: Y
           MSSQL_PID: Standard

From 69d35ee91177dfc5e96a00aa9eeeee40fe85143e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 5 Mar 2025 04:47:05 +0800
Subject: [PATCH 138/206] Adjust appearence of commit status webhook (#33778)
 (#33789)

Backport #33778 by @denyskon

Some visual improvement for the commit status webhook message introduced
by #33320

- use short commit SHA as already done in e. g. commit webhook
- fix spacing, link text
- do not set user link for internal gitea-actions user

Before:

![grafik](https://github.com/user-attachments/assets/9c460846-c350-444c-89b5-8a0d5e26cb86)

After:

![grafik](https://github.com/user-attachments/assets/05519cd8-6d8f-432b-bd9d-082de558a55a)

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
---
 services/webhook/general.go | 12 +++++++++---
 services/webhook/matrix.go  |  5 +++--
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/services/webhook/general.go b/services/webhook/general.go
index 91bf68600fcc5..c3e2ded2043e5 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -9,7 +9,9 @@ import (
 	"net/url"
 	"strings"
 
+	user_model "code.gitea.io/gitea/models/user"
 	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
@@ -308,12 +310,16 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
 }
 
 func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
-	refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+	refLink := linkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
 
-	text = fmt.Sprintf("Commit Status changed: %s", refLink)
+	text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
 	color = greenColor
 	if withSender {
-		text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+		if user_model.IsGiteaActionsUserName(p.Sender.UserName) {
+			text += fmt.Sprintf(" by %s", p.Sender.FullName)
+		} else {
+			text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+		}
 	}
 
 	return text, color
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index ec21712837b93..f43354726c569 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -15,6 +15,7 @@ import (
 	"strings"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -245,8 +246,8 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
 }
 
 func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
-	refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
-	text := fmt.Sprintf("Commit Status changed: %s", refLink)
+	refLink := htmlLinkFormatter(p.TargetURL, fmt.Sprintf("%s [%s]", p.Context, base.ShortSha(p.SHA)))
+	text := fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
 
 	return m.newPayload(text)
 }

From 9c3511f0b1aeb9d7674794dde31962878522a467 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 4 Mar 2025 12:49:46 -0800
Subject: [PATCH 139/206] Add changelog for 1.23.5 (#33780)

Wait tomorrow's Golang version.
Maybe wait backport of #33764 and #33744, #33789

---------

Co-authored-by: metiftikci <metiftikci@hotmail.com>
---
 CHANGELOG.md | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bb447f091ec8..a51e31a3dd902 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-03
+
+* SECURITY
+  * Bump x/oauth2 & x/crypto (#33704) (#33727)
+* PERFORMANCE
+  * Optimize user dashboard loading (#33686) (#33708)
+* BUGFIXES
+  * Fix navbar dropdown item align (#33782)
+  * Fix inconsistent closed issue list icon (#33722) (#33728)
+  * Fix for Maven Package Naming Convention Handling (#33678) (#33679)
+  * Improve Open-with URL encoding (#33666) (#33680)
+  * Deleting repository should unlink all related packages (#33653) (#33673)
+  * Fix omitempty bug (#33663) (#33670)
+  * Upgrade go-crypto from 1.1.4 to 1.1.6 (#33745) (#33754)
+  * Fix OCI image.version annotation for releases to use full semver (#33698) (#33701)
+  * Try to fix ACME path when renew (#33668) (#33693)
+  * Fix mCaptcha bug (#33659) (#33661)
+  * Git graph: don't show detached commits (#33645) (#33650)
+  * Use MatchPhraseQuery for bleve code search (#33628)
+  * Adjust appearence of commit status webhook (#33778) #33789
+
 ## [1.23.4](https://github.com/go-gitea/gitea/releases/tag/v1.23.4) - 2025-02-16
 
 * SECURITY

From 92f2d904f0d33d6a6a7f1a53dbe00ad38b135cce Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 4 Mar 2025 15:39:08 -0800
Subject: [PATCH 140/206] Upgrade golang net from 0.35.0 -> 0.36.0 #33795
 (#33796)

Backport #33795
---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index 1f20e162cd836..8a7c430f19263 100644
--- a/go.mod
+++ b/go.mod
@@ -120,7 +120,7 @@ require (
 	github.com/yuin/goldmark-meta v1.1.0
 	golang.org/x/crypto v0.35.0
 	golang.org/x/image v0.21.0
-	golang.org/x/net v0.34.0
+	golang.org/x/net v0.36.0
 	golang.org/x/oauth2 v0.27.0
 	golang.org/x/sync v0.11.0
 	golang.org/x/sys v0.30.0
diff --git a/go.sum b/go.sum
index c9b188b9c0b68..34252c50f432b 100644
--- a/go.sum
+++ b/go.sum
@@ -869,8 +869,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
+golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
 golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
 golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

From d03e7fd65eb47f5df267ff657c7ed0e9971b06c8 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 8 Mar 2025 03:31:25 +0800
Subject: [PATCH 141/206] Support disable passkey auth (#33348) (#33819)

* Backport #33348
* Backport #33820

---------

Co-authored-by: yp05327 <576951401@qq.com>
---
 custom/conf/app.example.ini               |  5 ++++-
 modules/setting/service.go                |  2 ++
 routers/web/auth/auth.go                  |  1 +
 routers/web/auth/linkaccount.go           |  3 +++
 routers/web/auth/webauthn.go              | 10 ++++++++++
 templates/user/auth/signin_inner.tmpl     |  7 ++++---
 tests/integration/signin_test.go          | 20 +++++++++++++++++++-
 web_src/js/features/user-auth-webauthn.ts | 11 ++++++++++-
 8 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 6896b073e1f51..b417baae8b357 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -784,10 +784,13 @@ LEVEL = Info
 ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
 ;ENABLE_BASIC_AUTHENTICATION = true
 ;;
-;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods.
+;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled.
 ;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication.
 ;ENABLE_PASSWORD_SIGNIN_FORM = true
 ;;
+;; Allow users to sign-in with a passkey
+;ENABLE_PASSKEY_AUTHENTICATION = true
+;;
 ;; More detail: https://github.com/gogits/gogs/issues/165
 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
 ; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 526ad64eb4001..8c1843eeb7548 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -46,6 +46,7 @@ var Service = struct {
 	RequireSignInView                       bool
 	EnableNotifyMail                        bool
 	EnableBasicAuth                         bool
+	EnablePasskeyAuth                       bool
 	EnableReverseProxyAuth                  bool
 	EnableReverseProxyAuthAPI               bool
 	EnableReverseProxyAutoRegister          bool
@@ -161,6 +162,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
 	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
 	Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
 	Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
+	Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
 	Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
 	Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
 	Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 8ddb9730c3ac3..ee70889a53af1 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -169,6 +169,7 @@ func prepareSignInPageData(ctx *context.Context) {
 	ctx.Data["PageIsLogin"] = true
 	ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
 	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
+	ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
 
 	if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
 		context.SetCaptchaData(ctx)
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index ba61a1ad1a2f3..a476536f36f44 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -46,6 +46,7 @@ func LinkAccount(ctx *context.Context) {
 	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
 	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
+	ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
@@ -145,6 +146,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
 	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
+	ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
@@ -235,6 +237,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
 	ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
 	ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
 	ctx.Data["ShowRegistrationButton"] = false
+	ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
 
 	// use this to set the right link into the signIn and signUp templates in the link_account template
 	ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index ba25d4507052e..d03bcaaf03d20 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -50,6 +50,11 @@ func WebAuthn(ctx *context.Context) {
 
 // WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser
 func WebAuthnPasskeyAssertion(ctx *context.Context) {
+	if !setting.Service.EnablePasskeyAuth {
+		ctx.Error(http.StatusForbidden)
+		return
+	}
+
 	assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
 	if err != nil {
 		ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
@@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
 
 // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
 func WebAuthnPasskeyLogin(ctx *context.Context) {
+	if !setting.Service.EnablePasskeyAuth {
+		ctx.Error(http.StatusForbidden)
+		return
+	}
+
 	sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData)
 	if !okData || sessionData == nil {
 		ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session"))
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index dd608e5aa1e53..1e90b3e924b83 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -60,10 +60,11 @@
 </div>
 
 <div class="ui container fluid">
-	{{template "user/auth/webauthn_error" .}}
-
 	<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
-		<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
+		{{if .EnablePasskeyAuth}}
+			{{template "user/auth/webauthn_error" .}}
+			<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
+		{{end}}
 
 		{{if .ShowRegistrationButton}}
 			<div class="field">
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index 7282a11b35e7c..25b66bd28b577 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -96,7 +96,7 @@ func TestSigninWithRememberMe(t *testing.T) {
 	session.MakeRequest(t, req, http.StatusOK)
 }
 
-func TestEnablePasswordSignInForm(t *testing.T) {
+func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	mockLinkAccount := func(ctx *context.Context) {
@@ -139,4 +139,22 @@ func TestEnablePasswordSignInForm(t *testing.T) {
 		resp = MakeRequest(t, req, http.StatusOK)
 		NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true)
 	})
+
+	t.Run("EnablePasskeyAuth=false", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, false)()
+
+		req := NewRequest(t, "GET", "/user/login")
+		resp := MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", false)
+	})
+
+	t.Run("EnablePasskeyAuth=true", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, true)()
+
+		req := NewRequest(t, "GET", "/user/login")
+		resp := MakeRequest(t, req, http.StatusOK)
+		NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true)
+	})
 }
diff --git a/web_src/js/features/user-auth-webauthn.ts b/web_src/js/features/user-auth-webauthn.ts
index 70516c280d897..743b39a11e3b1 100644
--- a/web_src/js/features/user-auth-webauthn.ts
+++ b/web_src/js/features/user-auth-webauthn.ts
@@ -1,5 +1,5 @@
 import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.ts';
-import {showElem} from '../utils/dom.ts';
+import {hideElem, showElem} from '../utils/dom.ts';
 import {GET, POST} from '../modules/fetch.ts';
 
 const {appSubUrl} = window.config;
@@ -11,6 +11,15 @@ export async function initUserAuthWebAuthn() {
     return;
   }
 
+  if (window.location.protocol === 'http:') {
+    // webauthn is only supported on secure contexts
+    const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
+    if (!isLocalhost) {
+      hideElem(elSignInPasskeyBtn);
+      return;
+    }
+  }
+
   if (!detectWebAuthnSupport()) {
     return;
   }

From 16a332464d7bc6f096f4bfb6678239e4e4a2eee6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 8 Mar 2025 15:16:54 +0800
Subject: [PATCH 142/206]  Try to fix ACME (3rd) (#33807) (#33808)

Backport #33807
---
 cmd/web_acme.go | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/cmd/web_acme.go b/cmd/web_acme.go
index bca4ae021217b..172dde913b6c6 100644
--- a/cmd/web_acme.go
+++ b/cmd/web_acme.go
@@ -16,6 +16,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/caddyserver/certmagic"
 )
@@ -68,9 +69,15 @@ func runACME(listenAddr string, m http.Handler) error {
 	// And one more thing, no idea why we should set the global default variables here
 	// But it seems that the current ACME code needs these global variables to make renew work.
 	// Otherwise, "renew" will use incorrect storage path
+	oldDefaultACME := certmagic.DefaultACME
 	certmagic.Default.Storage = &certmagic.FileStorage{Path: setting.AcmeLiveDirectory}
 	certmagic.DefaultACME = certmagic.ACMEIssuer{
-		CA:                      setting.AcmeURL,
+		// try to use the default values provided by DefaultACME
+		CA:        util.IfZero(setting.AcmeURL, oldDefaultACME.CA),
+		TestCA:    oldDefaultACME.TestCA,
+		Logger:    oldDefaultACME.Logger,
+		HTTPProxy: oldDefaultACME.HTTPProxy,
+
 		TrustedRoots:            certPool,
 		Email:                   setting.AcmeEmail,
 		Agreed:                  setting.AcmeTOS,

From 869ee4fc38346cc0a4db0aa2ec79657e82dd1328 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 8 Mar 2025 15:41:51 +0800
Subject: [PATCH 143/206] Do not call "git diff" when listing PRs (#33817)

Fix  #31492
---
 modules/structs/pull.go                |  7 +++--
 services/convert/pull.go               | 20 ++-----------
 tests/integration/api_pull_test.go     | 40 ++++++++++++--------------
 tests/integration/repo_webhook_test.go |  7 +++--
 4 files changed, 31 insertions(+), 43 deletions(-)

diff --git a/modules/structs/pull.go b/modules/structs/pull.go
index 55831e642c1a0..d0f4eabc9e746 100644
--- a/modules/structs/pull.go
+++ b/modules/structs/pull.go
@@ -27,9 +27,10 @@ type PullRequest struct {
 	Comments                int        `json:"comments"`
 	// number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
 	ReviewComments int `json:"review_comments"`
-	Additions      int `json:"additions"`
-	Deletions      int `json:"deletions"`
-	ChangedFiles   int `json:"changed_files"`
+
+	Additions    *int `json:"additions,omitempty"`
+	Deletions    *int `json:"deletions,omitempty"`
+	ChangedFiles *int `json:"changed_files,omitempty"`
 
 	HTMLURL  string `json:"html_url"`
 	DiffURL  string `json:"diff_url"`
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 209d2bd79d2e1..e0548eb6353f5 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -239,9 +239,11 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
 		// Calculate diff
 		startCommitID = pr.MergeBase
 
-		apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
+		diffChangedFiles, diffAdditions, diffDeletions, err := gitRepo.GetDiffShortStat(startCommitID, endCommitID)
 		if err != nil {
 			log.Error("GetDiffShortStat: %v", err)
+		} else {
+			apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions = &diffChangedFiles, &diffAdditions, &diffDeletions
 		}
 	}
 
@@ -459,12 +461,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
 				return nil, err
 			}
 
-			// Outer scope variables to be used in diff calculation
-			var (
-				startCommitID string
-				endCommitID   string
-			)
-
 			if git.IsErrBranchNotExist(err) {
 				headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
 				if err != nil && !git.IsErrNotExist(err) {
@@ -473,7 +469,6 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
 				}
 				if err == nil {
 					apiPullRequest.Head.Sha = headCommitID
-					endCommitID = headCommitID
 				}
 			} else {
 				commit, err := headBranch.GetCommit()
@@ -484,17 +479,8 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs
 				if err == nil {
 					apiPullRequest.Head.Ref = pr.HeadBranch
 					apiPullRequest.Head.Sha = commit.ID.String()
-					endCommitID = commit.ID.String()
 				}
 			}
-
-			// Calculate diff
-			startCommitID = pr.MergeBase
-
-			apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
-			if err != nil {
-				log.Error("GetDiffShortStat: %v", err)
-			}
 		}
 
 		if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index 969e1108951fd..4dfc8a332a89a 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -51,7 +51,6 @@ func TestAPIViewPulls(t *testing.T) {
 	assert.Empty(t, pull.RequestedReviewersTeams)
 	assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
 	assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID)
-	assert.EqualValues(t, 1, pull.ChangedFiles)
 
 	if assert.EqualValues(t, 5, pull.ID) {
 		resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
@@ -59,22 +58,23 @@ func TestAPIViewPulls(t *testing.T) {
 		assert.NoError(t, err)
 		patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
 		assert.NoError(t, err)
-		if assert.Len(t, patch.Files, pull.ChangedFiles) {
+		if assert.Len(t, patch.Files, 1) {
 			assert.Equal(t, "File-WoW", patch.Files[0].Name)
 			// FIXME: The old name should be empty if it's a file add type
 			assert.Equal(t, "File-WoW", patch.Files[0].OldName)
-			assert.EqualValues(t, pull.Additions, patch.Files[0].Addition)
-			assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion)
+			assert.EqualValues(t, 1, patch.Files[0].Addition)
+			assert.EqualValues(t, 0, patch.Files[0].Deletion)
 			assert.Equal(t, gitdiff.DiffFileAdd, patch.Files[0].Type)
 		}
 
 		t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
 			doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
-				if assert.Len(t, files, pull.ChangedFiles) {
+				if assert.Len(t, files, 1) {
 					assert.Equal(t, "File-WoW", files[0].Filename)
 					assert.Empty(t, files[0].PreviousFilename)
-					assert.EqualValues(t, pull.Additions, files[0].Additions)
-					assert.EqualValues(t, pull.Deletions, files[0].Deletions)
+					assert.EqualValues(t, 1, files[0].Additions)
+					assert.EqualValues(t, 1, files[0].Changes)
+					assert.EqualValues(t, 0, files[0].Deletions)
 					assert.Equal(t, "added", files[0].Status)
 				}
 			}))
@@ -88,7 +88,6 @@ func TestAPIViewPulls(t *testing.T) {
 	assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID)
 	assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID)
 	assert.EqualValues(t, 5, pull.RequestedReviewers[3].ID)
-	assert.EqualValues(t, 1, pull.ChangedFiles)
 
 	if assert.EqualValues(t, 2, pull.ID) {
 		resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
@@ -96,45 +95,44 @@ func TestAPIViewPulls(t *testing.T) {
 		assert.NoError(t, err)
 		patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
 		assert.NoError(t, err)
-		if assert.Len(t, patch.Files, pull.ChangedFiles) {
+		if assert.Len(t, patch.Files, 1) {
 			assert.Equal(t, "README.md", patch.Files[0].Name)
 			assert.Equal(t, "README.md", patch.Files[0].OldName)
-			assert.EqualValues(t, pull.Additions, patch.Files[0].Addition)
-			assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion)
+			assert.EqualValues(t, 4, patch.Files[0].Addition)
+			assert.EqualValues(t, 1, patch.Files[0].Deletion)
 			assert.Equal(t, gitdiff.DiffFileChange, patch.Files[0].Type)
 		}
 
 		t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
 			doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
-				if assert.Len(t, files, pull.ChangedFiles) {
+				if assert.Len(t, files, 1) {
 					assert.Equal(t, "README.md", files[0].Filename)
 					// FIXME: The PreviousFilename name should be the same as Filename if it's a file change
 					assert.Equal(t, "", files[0].PreviousFilename)
-					assert.EqualValues(t, pull.Additions, files[0].Additions)
-					assert.EqualValues(t, pull.Deletions, files[0].Deletions)
+					assert.EqualValues(t, 4, files[0].Additions)
+					assert.EqualValues(t, 1, files[0].Deletions)
 					assert.Equal(t, "changed", files[0].Status)
 				}
 			}))
 	}
 
-	pull = pulls[2]
+	pull = pulls[0]
 	assert.EqualValues(t, 1, pull.Poster.ID)
-	assert.Len(t, pull.RequestedReviewers, 1)
+	assert.Len(t, pull.RequestedReviewers, 2)
 	assert.Empty(t, pull.RequestedReviewersTeams)
-	assert.EqualValues(t, 1, pull.RequestedReviewers[0].ID)
-	assert.EqualValues(t, 0, pull.ChangedFiles)
+	assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
 
-	if assert.EqualValues(t, 1, pull.ID) {
+	if assert.EqualValues(t, 5, pull.ID) {
 		resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
 		bs, err := io.ReadAll(resp.Body)
 		assert.NoError(t, err)
 		patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
 		assert.NoError(t, err)
-		assert.EqualValues(t, pull.ChangedFiles, patch.NumFiles)
+		assert.EqualValues(t, 1, patch.NumFiles)
 
 		t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
 			doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
-				assert.Len(t, files, pull.ChangedFiles)
+				assert.Len(t, files, 1)
 			}))
 	}
 }
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index 5a50d7d4914f1..09522639682ce 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/PuerkitoBio/goquery"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestNewWebHookLink(t *testing.T) {
@@ -378,12 +379,14 @@ func Test_WebhookPullRequest(t *testing.T) {
 
 		// 3. validate the webhook is triggered
 		assert.EqualValues(t, "pull_request", triggeredEvent)
-		assert.Len(t, payloads, 1)
+		require.Len(t, payloads, 1)
 		assert.EqualValues(t, "repo1", payloads[0].PullRequest.Base.Repository.Name)
 		assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Base.Repository.FullName)
 		assert.EqualValues(t, "repo1", payloads[0].PullRequest.Head.Repository.Name)
 		assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Head.Repository.FullName)
-		assert.EqualValues(t, 0, payloads[0].PullRequest.Additions)
+		assert.EqualValues(t, 0, *payloads[0].PullRequest.Additions)
+		assert.EqualValues(t, 0, *payloads[0].PullRequest.ChangedFiles)
+		assert.EqualValues(t, 0, *payloads[0].PullRequest.Deletions)
 	})
 }
 

From 523a84e5d0290de9abbff8b5f5fb216dfa64e750 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 10 Mar 2025 01:02:45 +0800
Subject: [PATCH 144/206] Removing unwanted ui container (#33833) (#33835)

Backport #33833 by Vinoth-kumar-Ganesan

when the passkey auth and register was disabled
the unwanted ui container was show

Co-authored-by: Vinoth Kumar <103478407+Vinoth-kumar-Ganesan@users.noreply.github.com>
Co-authored-by: Vinoth414 <103478407+Vinoth414@users.noreply.github.com>
---
 templates/user/auth/signin_inner.tmpl | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 1e90b3e924b83..28a85dd4c2054 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -59,6 +59,7 @@
 	</div>
 </div>
 
+{{if or .EnablePasskeyAuth .ShowRegistrationButton}}
 <div class="ui container fluid">
 	<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
 		{{if .EnablePasskeyAuth}}
@@ -74,3 +75,4 @@
 		{{end}}
 	</div>
 </div>
+{{end}}

From a2c6ecc09366760ff8b07d3b0287a62ca3d2d25a Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 10 Mar 2025 18:26:31 +0800
Subject: [PATCH 145/206] Fix LFS URL (#33840) (#33843)

Backport #33840 by wxiaoguang

Fix #33839

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/httplib/request.go               |  7 +++
 modules/lfstransfer/backend/backend.go   | 12 ++----
 modules/lfstransfer/backend/lock.go      | 13 +++---
 modules/lfstransfer/backend/util.go      | 52 +++++++++++++++++++++--
 modules/lfstransfer/backend/util_test.go | 54 ++++++++++++++++++++++++
 modules/private/internal.go              |  4 ++
 tests/integration/git_lfs_ssh_test.go    |  7 ++-
 tests/mssql.ini.tmpl                     |  1 +
 tests/mysql.ini.tmpl                     |  1 +
 tests/pgsql.ini.tmpl                     |  1 +
 tests/sqlite.ini.tmpl                    |  1 +
 11 files changed, 132 insertions(+), 21 deletions(-)
 create mode 100644 modules/lfstransfer/backend/util_test.go

diff --git a/modules/httplib/request.go b/modules/httplib/request.go
index 267e276df344d..5e409228961c9 100644
--- a/modules/httplib/request.go
+++ b/modules/httplib/request.go
@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
+	"errors"
 	"fmt"
 	"io"
 	"net"
@@ -101,6 +102,9 @@ func (r *Request) Param(key, value string) *Request {
 
 // Body adds request raw body. It supports string, []byte and io.Reader as body.
 func (r *Request) Body(data any) *Request {
+	if r == nil {
+		return nil
+	}
 	switch t := data.(type) {
 	case nil: // do nothing
 	case string:
@@ -193,6 +197,9 @@ func (r *Request) getResponse() (*http.Response, error) {
 // Response executes request client gets response manually.
 // Caller MUST close the response body if no error occurs
 func (r *Request) Response() (*http.Response, error) {
+	if r == nil {
+		return nil, errors.New("invalid request")
+	}
 	return r.getResponse()
 }
 
diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go
index 540932b9302de..1328d93a48fef 100644
--- a/modules/lfstransfer/backend/backend.go
+++ b/modules/lfstransfer/backend/backend.go
@@ -70,14 +70,13 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
 		g.logger.Log("json marshal error", err)
 		return nil, err
 	}
-	url := g.server.JoinPath("objects/batch").String()
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, g.server.JoinPath("objects/batch").String(), http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
@@ -179,13 +178,12 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
 		g.logger.Log("argument id incorrect")
 		return nil, 0, transfer.ErrCorruptData
 	}
-	url := action.Href
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeOctetStream,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
+	req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodGet, headers, nil)
 	resp, err := req.Response()
 	if err != nil {
 		return nil, 0, fmt.Errorf("failed to get response: %w", err)
@@ -225,7 +223,6 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
 		g.logger.Log("argument id incorrect")
 		return transfer.ErrCorruptData
 	}
-	url := action.Href
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
@@ -233,7 +230,7 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
 		headerContentLength:     strconv.FormatInt(size, 10),
 	}
 
-	req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil)
+	req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPut, headers, nil)
 	req.Body(r)
 	resp, err := req.Response()
 	if err != nil {
@@ -274,14 +271,13 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
 		// the server sent no verify action
 		return transfer.SuccessStatus(), nil
 	}
-	url := action.Href
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, toInternalLFSURL(action.Href), http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		return transfer.NewStatus(transfer.StatusInternalServerError), err
diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go
index 4b45658611463..639f8b184e442 100644
--- a/modules/lfstransfer/backend/lock.go
+++ b/modules/lfstransfer/backend/lock.go
@@ -43,14 +43,13 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
 		g.logger.Log("json marshal error", err)
 		return nil, err
 	}
-	url := g.server.String()
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, g.server.String(), http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
@@ -95,14 +94,13 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
 		g.logger.Log("json marshal error", err)
 		return err
 	}
-	url := g.server.JoinPath(lock.ID(), "unlock").String()
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
+	req := newInternalRequestLFS(g.ctx, g.server.JoinPath(lock.ID(), "unlock").String(), http.MethodPost, headers, bodyBytes)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
@@ -176,16 +174,15 @@ func (g *giteaLockBackend) Range(cursor string, limit int, iter func(transfer.Lo
 }
 
 func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, error) {
-	urlq := g.server.JoinPath() // get a copy
-	urlq.RawQuery = v.Encode()
-	url := urlq.String()
+	serverURLWithQuery := g.server.JoinPath() // get a copy
+	serverURLWithQuery.RawQuery = v.Encode()
 	headers := map[string]string{
 		headerAuthorization:     g.authToken,
 		headerGiteaInternalAuth: g.internalAuth,
 		headerAccept:            mimeGitLFS,
 		headerContentType:       mimeGitLFS,
 	}
-	req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
+	req := newInternalRequestLFS(g.ctx, serverURLWithQuery.String(), http.MethodGet, headers, nil)
 	resp, err := req.Response()
 	if err != nil {
 		g.logger.Log("http request error", err)
diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go
index f322d542575d8..98ce0b1e62ad8 100644
--- a/modules/lfstransfer/backend/util.go
+++ b/modules/lfstransfer/backend/util.go
@@ -8,9 +8,13 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/private"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/charmbracelet/git-lfs-transfer/transfer"
 )
@@ -57,8 +61,7 @@ const (
 
 // Operations enum
 const (
-	opNone = iota
-	opDownload
+	opDownload = iota + 1
 	opUpload
 )
 
@@ -86,8 +89,49 @@ func statusCodeToErr(code int) error {
 	}
 }
 
-func newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request {
-	req := private.NewInternalRequest(ctx, url, method)
+func toInternalLFSURL(s string) string {
+	pos1 := strings.Index(s, "://")
+	if pos1 == -1 {
+		return ""
+	}
+	appSubURLWithSlash := setting.AppSubURL + "/"
+	pos2 := strings.Index(s[pos1+3:], appSubURLWithSlash)
+	if pos2 == -1 {
+		return ""
+	}
+	routePath := s[pos1+3+pos2+len(appSubURLWithSlash):]
+	fields := strings.SplitN(routePath, "/", 3)
+	if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
+		return ""
+	}
+	return setting.LocalURL + "api/internal/repo/" + routePath
+}
+
+func isInternalLFSURL(s string) bool {
+	if !strings.HasPrefix(s, setting.LocalURL) {
+		return false
+	}
+	u, err := url.Parse(s)
+	if err != nil {
+		return false
+	}
+	routePath := util.PathJoinRelX(u.Path)
+	subRoutePath, cut := strings.CutPrefix(routePath, "api/internal/repo/")
+	if !cut {
+		return false
+	}
+	fields := strings.SplitN(subRoutePath, "/", 3)
+	if len(fields) < 3 || !strings.HasPrefix(fields[2], "info/lfs") {
+		return false
+	}
+	return true
+}
+
+func newInternalRequestLFS(ctx context.Context, internalURL, method string, headers map[string]string, body any) *httplib.Request {
+	if !isInternalLFSURL(internalURL) {
+		return nil
+	}
+	req := private.NewInternalRequest(ctx, internalURL, method)
 	for k, v := range headers {
 		req.Header(k, v)
 	}
diff --git a/modules/lfstransfer/backend/util_test.go b/modules/lfstransfer/backend/util_test.go
new file mode 100644
index 0000000000000..0f6d7af803cc8
--- /dev/null
+++ b/modules/lfstransfer/backend/util_test.go
@@ -0,0 +1,54 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package backend
+
+import (
+	"context"
+	"testing"
+
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestToInternalLFSURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
+	defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+	cases := []struct {
+		url      string
+		expected string
+	}{
+		{"http://appurl/any", ""},
+		{"http://appurl/sub/any", ""},
+		{"http://appurl/sub/owner/repo/any", ""},
+		{"http://appurl/sub/owner/repo/info/any", ""},
+		{"http://appurl/sub/owner/repo/info/lfs/any", "http://localurl/api/internal/repo/owner/repo/info/lfs/any"},
+	}
+	for _, c := range cases {
+		assert.Equal(t, c.expected, toInternalLFSURL(c.url), c.url)
+	}
+}
+
+func TestIsInternalLFSURL(t *testing.T) {
+	defer test.MockVariableValue(&setting.LocalURL, "http://localurl/")()
+	defer test.MockVariableValue(&setting.InternalToken, "mock-token")()
+	cases := []struct {
+		url      string
+		expected bool
+	}{
+		{"", false},
+		{"http://otherurl/api/internal/repo/owner/repo/info/lfs/any", false},
+		{"http://localurl/api/internal/repo/owner/repo/info/lfs/any", true},
+		{"http://localurl/api/internal/repo/owner/repo/info", false},
+		{"http://localurl/api/internal/misc/owner/repo/info/lfs/any", false},
+		{"http://localurl/api/internal/owner/repo/info/lfs/any", false},
+		{"http://localurl/api/internal/foo/bar", false},
+	}
+	for _, c := range cases {
+		req := newInternalRequestLFS(context.Background(), c.url, "GET", nil, nil)
+		assert.Equal(t, c.expected, req != nil, c.url)
+		assert.Equal(t, c.expected, isInternalLFSURL(c.url), c.url)
+	}
+}
diff --git a/modules/private/internal.go b/modules/private/internal.go
index 3bd4eb06b1954..35eed1d6087ef 100644
--- a/modules/private/internal.go
+++ b/modules/private/internal.go
@@ -40,6 +40,10 @@ func NewInternalRequest(ctx context.Context, url, method string) *httplib.Reques
 Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
 	}
 
+	if !strings.HasPrefix(url, setting.LocalURL) {
+		log.Fatal("Invalid internal request URL: %q", url)
+	}
+
 	req := httplib.NewRequest(url, method).
 		SetContext(ctx).
 		Header("X-Real-IP", getClientIP()).
diff --git a/tests/integration/git_lfs_ssh_test.go b/tests/integration/git_lfs_ssh_test.go
index 9cb7fd089bdf9..c6011523a80c5 100644
--- a/tests/integration/git_lfs_ssh_test.go
+++ b/tests/integration/git_lfs_ssh_test.go
@@ -55,9 +55,14 @@ func TestGitLFSSSH(t *testing.T) {
 			return strings.Contains(s, "POST /api/internal/repo/user2/repo1.git/info/lfs/objects/batch")
 		})
 		countUpload := slices.ContainsFunc(routerCalls, func(s string) bool {
-			return strings.Contains(s, "PUT /user2/repo1.git/info/lfs/objects/")
+			return strings.Contains(s, "PUT /api/internal/repo/user2/repo1.git/info/lfs/objects/")
+		})
+		nonAPIRequests := slices.ContainsFunc(routerCalls, func(s string) bool {
+			fields := strings.Fields(s)
+			return !strings.HasPrefix(fields[1], "/api/")
 		})
 		assert.NotZero(t, countBatch)
 		assert.NotZero(t, countUpload)
+		assert.Zero(t, nonAPIRequests)
 	})
 }
diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl
index 8b1593a396f9e..0c5bcd49772db 100644
--- a/tests/mssql.ini.tmpl
+++ b/tests/mssql.ini.tmpl
@@ -45,6 +45,7 @@ SIGNING_KEY = none
 SSH_DOMAIN       = localhost
 HTTP_PORT        = 3003
 ROOT_URL         = http://localhost:3003/
+LOCAL_ROOT_URL   = http://127.0.0.1:3003/
 DISABLE_SSH      = false
 SSH_LISTEN_HOST  = localhost
 SSH_PORT         = 2201
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index 8cafba9591382..5778cc5867207 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -47,6 +47,7 @@ SIGNING_KEY = none
 SSH_DOMAIN       = localhost
 HTTP_PORT        = 3001
 ROOT_URL         = http://localhost:3001/
+LOCAL_ROOT_URL   = http://127.0.0.1:3001/
 DISABLE_SSH      = false
 SSH_LISTEN_HOST  = localhost
 SSH_PORT         = 2201
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index fb881d411c7ad..1febdf930ec4c 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -46,6 +46,7 @@ SIGNING_KEY = none
 SSH_DOMAIN       = localhost
 HTTP_PORT        = 3002
 ROOT_URL         = http://localhost:3002/
+LOCAL_ROOT_URL   = http://127.0.0.1:3002/
 DISABLE_SSH      = false
 SSH_LISTEN_HOST  = localhost
 SSH_PORT         = 2202
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
index c0b3e1c85979c..ce1b65d02c6f6 100644
--- a/tests/sqlite.ini.tmpl
+++ b/tests/sqlite.ini.tmpl
@@ -41,6 +41,7 @@ SIGNING_KEY = none
 SSH_DOMAIN       = localhost
 HTTP_PORT        = 3003
 ROOT_URL         = http://localhost:3003/
+LOCAL_ROOT_URL   = http://127.0.0.1:3003/
 DISABLE_SSH      = false
 SSH_LISTEN_HOST  = localhost
 SSH_PORT         = 2203

From 6f8e62fa9c8782d9ae39c93704134095fa8c98fa Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 11 Mar 2025 23:16:33 +0800
Subject: [PATCH 146/206] Fix some UI problems for 1.23 (#33856)

Partially backport #32927 #33851
---
 web_src/css/modules/navbar.css   | 4 ++++
 web_src/js/features/repo-diff.ts | 5 ++++-
 web_src/js/globals.d.ts          | 1 +
 3 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css
index 1137e46c7fd3e..545ebf5c039af 100644
--- a/web_src/css/modules/navbar.css
+++ b/web_src/css/modules/navbar.css
@@ -57,6 +57,10 @@
   margin-right: 0;
 }
 
+#navbar .item.active {
+  background: var(--color-active);
+}
+
 @media (max-width: 767.98px) {
   #navbar {
     align-items: stretch;
diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts
index 58e0d880922dd..89bf7f4cdf61c 100644
--- a/web_src/js/features/repo-diff.ts
+++ b/web_src/js/features/repo-diff.ts
@@ -221,7 +221,10 @@ function initRepoDiffShowMore() {
       if (!resp) {
         return;
       }
-      $target.parent().replaceWith($(resp).find('#diff-file-boxes .diff-file-body .file-body').children());
+      const $respFileBody = $(resp).find('#diff-file-boxes .diff-file-body .file-body');
+      const respFileBodyChildren = Array.from($respFileBody.children());
+      $target.parent().replaceWith($respFileBody.children());
+      for (const el of respFileBodyChildren) window.htmx.process(el);
       onShowMoreFiles();
     } catch (error) {
       console.error('Error:', error);
diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts
index a5ec29a83f666..bddabd54ee115 100644
--- a/web_src/js/globals.d.ts
+++ b/web_src/js/globals.d.ts
@@ -63,6 +63,7 @@ interface Window {
   jQuery: typeof import('@types/jquery'),
   htmx: Omit<typeof import('htmx.org/dist/htmx.esm.js').default, 'config'> & {
     config?: Writable<typeof import('htmx.org').default.config>,
+    process?: (elt: Element | string) => void,
   },
   ui?: any,
   _globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & {

From fc82204fca91eee57d3a732c0b56d155a80c3d60 Mon Sep 17 00:00:00 2001
From: ChristopherHX <christopher.homberger@web.de>
Date: Tue, 11 Mar 2025 17:51:58 +0100
Subject: [PATCH 147/206] Fix auto concurrency cancellation skips commit status
 updates (#33764) (#33849)

Backport #33764
* add missing commit status
* conflicts with concurrency support
---
 models/actions/run.go               | 20 ++++++++++++--------
 models/actions/schedule.go          | 13 +++++++------
 routers/api/v1/repo/repo.go         |  3 +--
 routers/web/repo/actions/view.go    |  2 +-
 routers/web/repo/setting/setting.go |  3 +--
 services/actions/clear_tasks.go     | 22 +++++++++++++++++++++-
 services/actions/notifier_helper.go |  6 +++---
 services/actions/schedule_tasks.go  |  2 +-
 services/repository/branch.go       |  5 +++--
 services/repository/setting.go      |  3 +--
 10 files changed, 51 insertions(+), 28 deletions(-)

diff --git a/models/actions/run.go b/models/actions/run.go
index f40bc1eb3db1a..a268f760db66c 100644
--- a/models/actions/run.go
+++ b/models/actions/run.go
@@ -194,7 +194,7 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
 
 // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
 // It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
-func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
 	// Find all runs in the specified repository, reference, and workflow with non-final status
 	runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
 		RepoID:       repoID,
@@ -204,14 +204,16 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
 		Status:       []Status{StatusRunning, StatusWaiting, StatusBlocked},
 	})
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// If there are no runs found, there's no need to proceed with cancellation, so return nil.
 	if total == 0 {
-		return nil
+		return nil, nil
 	}
 
+	cancelledJobs := make([]*ActionRunJob, 0, total)
+
 	// Iterate over each found run and cancel its associated jobs.
 	for _, run := range runs {
 		// Find all jobs associated with the current run.
@@ -219,7 +221,7 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
 			RunID: run.ID,
 		})
 		if err != nil {
-			return err
+			return cancelledJobs, err
 		}
 
 		// Iterate over each job and attempt to cancel it.
@@ -238,27 +240,29 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
 				// Update the job's status and stopped time in the database.
 				n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
 				if err != nil {
-					return err
+					return cancelledJobs, err
 				}
 
 				// If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
 				if n == 0 {
-					return fmt.Errorf("job has changed, try again")
+					return cancelledJobs, fmt.Errorf("job has changed, try again")
 				}
 
+				cancelledJobs = append(cancelledJobs, job)
 				// Continue with the next job.
 				continue
 			}
 
 			// If the job has an associated task, try to stop the task, effectively cancelling the job.
 			if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil {
-				return err
+				return cancelledJobs, err
 			}
+			cancelledJobs = append(cancelledJobs, job)
 		}
 	}
 
 	// Return nil to indicate successful cancellation of all running and waiting jobs.
-	return nil
+	return cancelledJobs, nil
 }
 
 // InsertRun inserts a run
diff --git a/models/actions/schedule.go b/models/actions/schedule.go
index 961ffd0851c95..cb381dfd435bd 100644
--- a/models/actions/schedule.go
+++ b/models/actions/schedule.go
@@ -120,21 +120,22 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
 	return committer.Commit()
 }
 
-func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
+func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) ([]*ActionRunJob, error) {
 	// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
 	// There is no other place we can do this because the app.ini will be changed manually
 	if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
-		return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
+		return nil, fmt.Errorf("DeleteCronTaskByRepo: %v", err)
 	}
 	// cancel running cron jobs of this repository and delete old schedules
-	if err := CancelPreviousJobs(
+	jobs, err := CancelPreviousJobs(
 		ctx,
 		repo.ID,
 		repo.DefaultBranch,
 		"",
 		webhook_module.HookEventSchedule,
-	); err != nil {
-		return fmt.Errorf("CancelPreviousJobs: %v", err)
+	)
+	if err != nil {
+		return jobs, fmt.Errorf("CancelPreviousJobs: %v", err)
 	}
-	return nil
+	return jobs, nil
 }
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 40990a28cbdee..c1bfa5bcbbff9 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -12,7 +12,6 @@ import (
 	"strings"
 	"time"
 
-	actions_model "code.gitea.io/gitea/models/actions"
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
@@ -1050,7 +1049,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
 				ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err)
 				return err
 			}
-			if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+			if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
 				log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
 			}
 			log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index fe490ac717b6e..1e829caa8c823 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -903,7 +903,7 @@ func Run(ctx *context_module.Context) {
 	}
 
 	// cancel running jobs of the same workflow
-	if err := actions_model.CancelPreviousJobs(
+	if err := actions_service.CancelPreviousJobs(
 		ctx,
 		run.RepoID,
 		run.Ref,
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 717d7cbce1dc3..d750700ed10dc 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -12,7 +12,6 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
-	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
 	"code.gitea.io/gitea/models/perm"
@@ -902,7 +901,7 @@ func SettingsPost(ctx *context.Context) {
 			return
 		}
 
-		if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+		if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
 			log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
 		}
 
diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go
index 67373782d5c91..9d613b68a5560 100644
--- a/services/actions/clear_tasks.go
+++ b/services/actions/clear_tasks.go
@@ -10,10 +10,12 @@ import (
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/actions"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
+	webhook_module "code.gitea.io/gitea/modules/webhook"
 )
 
 // StopZombieTasks stops the task which have running status, but haven't been updated for a long time
@@ -32,6 +34,24 @@ func StopEndlessTasks(ctx context.Context) error {
 	})
 }
 
+func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.ActionRunJob) {
+	if len(jobs) > 0 {
+		CreateCommitStatus(ctx, jobs...)
+	}
+}
+
+func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
+	jobs, err := actions_model.CancelPreviousJobs(ctx, repoID, ref, workflowID, event)
+	notifyWorkflowJobStatusUpdate(ctx, jobs)
+	return err
+}
+
+func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
+	jobs, err := actions_model.CleanRepoScheduleTasks(ctx, repo)
+	notifyWorkflowJobStatusUpdate(ctx, jobs)
+	return err
+}
+
 func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
 	tasks, err := db.Find[actions_model.ActionTask](ctx, opts)
 	if err != nil {
@@ -67,7 +87,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
 		remove()
 	}
 
-	CreateCommitStatus(ctx, jobs...)
+	notifyWorkflowJobStatusUpdate(ctx, jobs)
 
 	return nil
 }
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index 2d8885dc323ab..87ea1a37f591c 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -136,7 +136,7 @@ func notify(ctx context.Context, input *notifyInput) error {
 		return nil
 	}
 	if unit_model.TypeActions.UnitGlobalDisabled() {
-		if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
+		if err := CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
 			log.Error("CleanRepoScheduleTasks: %v", err)
 		}
 		return nil
@@ -341,7 +341,7 @@ func handleWorkflows(
 		// cancel running jobs if the event is push or pull_request_sync
 		if run.Event == webhook_module.HookEventPush ||
 			run.Event == webhook_module.HookEventPullRequestSync {
-			if err := actions_model.CancelPreviousJobs(
+			if err := CancelPreviousJobs(
 				ctx,
 				run.RepoID,
 				run.Ref,
@@ -472,7 +472,7 @@ func handleSchedules(
 		log.Error("CountSchedules: %v", err)
 		return err
 	} else if count > 0 {
-		if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
+		if err := CleanRepoScheduleTasks(ctx, input.Repo); err != nil {
 			log.Error("CleanRepoScheduleTasks: %v", err)
 		}
 	}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 18f3324fd2c26..ad1158313b220 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -55,7 +55,7 @@ func startTasks(ctx context.Context) error {
 			// cancel running jobs if the event is push
 			if row.Schedule.Event == webhook_module.HookEventPush {
 				// cancel running jobs of the same workflow
-				if err := actions_model.CancelPreviousJobs(
+				if err := CancelPreviousJobs(
 					ctx,
 					row.RepoID,
 					row.Schedule.Ref,
diff --git a/services/repository/branch.go b/services/repository/branch.go
index ee06d89cf2371..13a5265d081f2 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -30,6 +30,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	webhook_module "code.gitea.io/gitea/modules/webhook"
+	actions_service "code.gitea.io/gitea/services/actions"
 	notify_service "code.gitea.io/gitea/services/notify"
 	files_service "code.gitea.io/gitea/services/repository/files"
 
@@ -428,7 +429,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
 				log.Error("DeleteCronTaskByRepo: %v", err)
 			}
 			// cancel running cron jobs of this repository and delete old schedules
-			if err := actions_model.CancelPreviousJobs(
+			if err := actions_service.CancelPreviousJobs(
 				ctx,
 				repo.ID,
 				from,
@@ -609,7 +610,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
 			log.Error("DeleteCronTaskByRepo: %v", err)
 		}
 		// cancel running cron jobs of this repository and delete old schedules
-		if err := actions_model.CancelPreviousJobs(
+		if err := actions_service.CancelPreviousJobs(
 			ctx,
 			repo.ID,
 			oldDefaultBranchName,
diff --git a/services/repository/setting.go b/services/repository/setting.go
index b82f24271ea34..e0c787dd2d887 100644
--- a/services/repository/setting.go
+++ b/services/repository/setting.go
@@ -7,7 +7,6 @@ import (
 	"context"
 	"slices"
 
-	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -29,7 +28,7 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni
 	}
 
 	if slices.Contains(deleteUnitTypes, unit.TypeActions) {
-		if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
+		if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
 			log.Error("CleanRepoScheduleTasks: %v", err)
 		}
 	}

From 9054a6670c8b80a9f0f558163f100143be3e2334 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 14 Mar 2025 08:28:10 +0800
Subject: [PATCH 148/206] Fix markdown render (#33870) (#33875)

Backport #33870 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/markdown/markdown.go           | 22 +++++++++----------
 .../markdown/markdown_attention_test.go       | 14 +++++++-----
 .../markup/markdown/transform_blockquote.go   |  3 +++
 modules/markup/renderer_test.go               |  4 ----
 4 files changed, 22 insertions(+), 21 deletions(-)
 delete mode 100644 modules/markup/renderer_test.go

diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index b5fffccdb989a..ace31eb540778 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -159,6 +159,14 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 		limit: setting.UI.MaxDisplayFileSize * 3,
 	}
 
+	// FIXME: Don't read all to memory, but goldmark doesn't support
+	buf, err := io.ReadAll(input)
+	if err != nil {
+		log.Error("Unable to ReadAll: %v", err)
+		return err
+	}
+	buf = giteautil.NormalizeEOL(buf)
+
 	// FIXME: should we include a timeout to abort the renderer if it takes too long?
 	defer func() {
 		err := recover()
@@ -166,20 +174,12 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 			return
 		}
 
-		log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
-		if (!setting.IsProd && !setting.IsInTesting) || log.IsDebug() {
-			log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
-		}
+		log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
+		escapedHTML := template.HTMLEscapeString(giteautil.UnsafeBytesToString(buf))
+		_, _ = output.Write(giteautil.UnsafeStringToBytes(escapedHTML))
 	}()
 
-	// FIXME: Don't read all to memory, but goldmark doesn't support
 	pc := newParserContext(ctx)
-	buf, err := io.ReadAll(input)
-	if err != nil {
-		log.Error("Unable to ReadAll: %v", err)
-		return err
-	}
-	buf = giteautil.NormalizeEOL(buf)
 
 	// Preserve original length.
 	bufWithMetadataLength := len(buf)
diff --git a/modules/markup/markdown/markdown_attention_test.go b/modules/markup/markdown/markdown_attention_test.go
index f6ec775b2c929..7b54653ec0ed1 100644
--- a/modules/markup/markdown/markdown_attention_test.go
+++ b/modules/markup/markdown/markdown_attention_test.go
@@ -23,6 +23,11 @@ func TestAttention(t *testing.T) {
 	defer svg.MockIcon("octicon-alert")()
 	defer svg.MockIcon("octicon-stop")()
 
+	test := func(input, expected string) {
+		result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
+		assert.NoError(t, err)
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
+	}
 	renderAttention := func(attention, icon string) string {
 		tmpl := `<blockquote class="attention-header attention-{attention}"><p><svg class="attention-icon attention-{attention} svg {icon}" width="16" height="16"></svg><strong class="attention-{attention}">{Attention}</strong></p>`
 		tmpl = strings.ReplaceAll(tmpl, "{attention}", attention)
@@ -31,12 +36,6 @@ func TestAttention(t *testing.T) {
 		return tmpl
 	}
 
-	test := func(input, expected string) {
-		result, err := markdown.RenderString(markup.NewTestRenderContext(), input)
-		assert.NoError(t, err)
-		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(result)))
-	}
-
 	test(`
 > [!NOTE]
 > text
@@ -53,4 +52,7 @@ func TestAttention(t *testing.T) {
 
 	// legacy GitHub style
 	test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
+
+	// edge case (it used to cause panic)
+	test(">\ntext", "<blockquote>\n</blockquote>\n<p>text</p>")
 }
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 2651d44a69ff9..3a8c6fa01869c 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -115,6 +115,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
 
 	// grab these nodes and make sure we adhere to the attention blockquote structure
 	firstParagraph := v.FirstChild()
+	if firstParagraph == nil {
+		return ast.WalkContinue, nil
+	}
 	g.applyElementDir(firstParagraph)
 
 	attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader)
diff --git a/modules/markup/renderer_test.go b/modules/markup/renderer_test.go
deleted file mode 100644
index 0791081f94e8f..0000000000000
--- a/modules/markup/renderer_test.go
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package markup_test

From be94f7bc07d3bda4d23f245bb1af4b5090f9e9f9 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 14 Mar 2025 23:37:00 +0800
Subject: [PATCH 149/206] Ignore trivial errors when updating push data
 (#33864) (#33887)

Backport #33864 by wxiaoguang

Fix #23213

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/gitdiff/gitdiff.go |  2 --
 services/repository/push.go | 36 ++++++++++++++++++++----------------
 2 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index eb7c273d49940..8d6e9f38e04a7 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -1377,10 +1377,8 @@ func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs {
 		"ignore-eol":    {"--ignore-space-at-eol"},
 		"show-all":      nil,
 	}
-
 	if flag, ok := whitespaceFlags[whitespaceBehavior]; ok {
 		return flag
 	}
-	log.Warn("unknown whitespace behavior: %q, default to 'show-all'", whitespaceBehavior)
 	return nil
 }
diff --git a/services/repository/push.go b/services/repository/push.go
index 06ad65e48fb6b..9f20563014aa2 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -23,6 +23,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
+	"code.gitea.io/gitea/modules/util"
 	issue_service "code.gitea.io/gitea/services/issue"
 	notify_service "code.gitea.io/gitea/services/notify"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -133,23 +134,26 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 			} else { // is new tag
 				newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
 				if err != nil {
-					return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err)
+					// in case there is dirty data, for example, the "github.com/git/git" repository has tags pointing to non-existing commits
+					if !errors.Is(err, util.ErrNotExist) {
+						log.Error("Unable to get tag commit: gitRepo.GetCommit(%s) in %s/%s[%d]: %v", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err)
+					}
+				} else {
+					commits := repo_module.NewPushCommits()
+					commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
+					commits.CompareURL = repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), opts.NewCommitID)
+
+					notify_service.PushCommits(
+						ctx, pusher, repo,
+						&repo_module.PushUpdateOptions{
+							RefFullName: opts.RefFullName,
+							OldCommitID: objectFormat.EmptyObjectID().String(),
+							NewCommitID: opts.NewCommitID,
+						}, commits)
+
+					addTags = append(addTags, tagName)
+					notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
 				}
-
-				commits := repo_module.NewPushCommits()
-				commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
-				commits.CompareURL = repo.ComposeCompareURL(objectFormat.EmptyObjectID().String(), opts.NewCommitID)
-
-				notify_service.PushCommits(
-					ctx, pusher, repo,
-					&repo_module.PushUpdateOptions{
-						RefFullName: opts.RefFullName,
-						OldCommitID: objectFormat.EmptyObjectID().String(),
-						NewCommitID: opts.NewCommitID,
-					}, commits)
-
-				addTags = append(addTags, tagName)
-				notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
 			}
 		} else if opts.RefFullName.IsBranch() {
 			if pusher == nil || pusher.ID != opts.PusherID {

From 5fadcf997e956d711dd3dc3fe7e7af2aeeac4cd1 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 15 Mar 2025 02:11:41 +0800
Subject: [PATCH 150/206] Fix maven panic when no package exists (#33888)
 (#33889)

Backport #33888 by @wxiaoguang

Fix #33886

Restore the old logic from #16510, which was incorrectly removed by
#33678

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/api/packages/maven/maven.go | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 4f9ced25b4bba..9089c2eccfa8f 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -98,6 +98,11 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
 	}
 	pvs = append(pvsLegacy, pvs...)
 
+	if len(pvs) == 0 {
+		apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist)
+		return
+	}
+
 	pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)

From 92280637a4c1bd7b1cd8735f4aaf061cc2aa9f7e Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 18 Mar 2025 02:59:51 +0800
Subject: [PATCH 151/206] Try to figure out attribute checker problem (#33901)
 (#33902)

Backport #33901
---
 modules/git/command.go             |  2 +
 modules/git/repo_attribute.go      | 47 +++++++++++++++--------
 modules/git/repo_attribute_test.go | 60 ++++++++++++++++++++++++++++++
 services/gitdiff/gitdiff.go        |  2 +
 4 files changed, 96 insertions(+), 15 deletions(-)

diff --git a/modules/git/command.go b/modules/git/command.go
index 22cb275ab2dbd..fd781ffef4fd9 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -46,6 +46,7 @@ type Command struct {
 	desc             string
 	globalArgsLength int
 	brokenArgs       []string
+	cmd              *exec.Cmd // for debug purpose only
 }
 
 func (c *Command) String() string {
@@ -314,6 +315,7 @@ func (c *Command) Run(opts *RunOpts) error {
 	startTime := time.Now()
 
 	cmd := exec.CommandContext(ctx, c.prog, c.args...)
+	c.cmd = cmd // for debug purpose only
 	if opts.Env == nil {
 		cmd.Env = os.Environ()
 	} else {
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 90eb783fe8761..99073a84774e4 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -9,6 +9,8 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path/filepath"
+	"time"
 
 	"code.gitea.io/gitea/modules/log"
 )
@@ -102,7 +104,7 @@ type CheckAttributeReader struct {
 
 	stdinReader io.ReadCloser
 	stdinWriter *os.File
-	stdOut      attributeWriter
+	stdOut      *nulSeparatedAttributeWriter
 	cmd         *Command
 	env         []string
 	ctx         context.Context
@@ -152,7 +154,6 @@ func (c *CheckAttributeReader) Init(ctx context.Context) error {
 	return nil
 }
 
-// Run run cmd
 func (c *CheckAttributeReader) Run() error {
 	defer func() {
 		_ = c.stdinReader.Close()
@@ -176,7 +177,7 @@ func (c *CheckAttributeReader) Run() error {
 func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
 	defer func() {
 		if err != nil && err != c.ctx.Err() {
-			log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err)
+			log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.Repo.Path), err)
 		}
 	}()
 
@@ -191,9 +192,31 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
 		return nil, err
 	}
 
+	reportTimeout := func() error {
+		stdOutClosed := false
+		select {
+		case <-c.stdOut.closed:
+			stdOutClosed = true
+		default:
+		}
+		debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.Repo.Path))
+		debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
+		if c.cmd.cmd != nil {
+			debugMsg += fmt.Sprintf(", process state: %q", c.cmd.cmd.ProcessState.String())
+		}
+		_ = c.Close()
+		return fmt.Errorf("CheckPath timeout: %s", debugMsg)
+	}
+
 	rs = make(map[string]string)
 	for range c.Attributes {
 		select {
+		case <-time.After(5 * time.Second):
+			// There is a strange "hang" problem in gitdiff.GetDiff -> CheckPath
+			// So add a timeout here to mitigate the problem, and output more logs for debug purpose
+			// In real world, if CheckPath runs long than seconds, it blocks the end user's operation,
+			// and at the moment the CheckPath result is not so important, so we can just ignore it.
+			return nil, reportTimeout()
 		case attr, ok := <-c.stdOut.ReadAttribute():
 			if !ok {
 				return nil, c.ctx.Err()
@@ -206,18 +229,12 @@ func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err
 	return rs, nil
 }
 
-// Close close pip after use
 func (c *CheckAttributeReader) Close() error {
 	c.cancel()
 	err := c.stdinWriter.Close()
 	return err
 }
 
-type attributeWriter interface {
-	io.WriteCloser
-	ReadAttribute() <-chan attributeTriple
-}
-
 type attributeTriple struct {
 	Filename  string
 	Attribute string
@@ -281,7 +298,7 @@ func (wr *nulSeparatedAttributeWriter) Close() error {
 	return nil
 }
 
-// Create a check attribute reader for the current repository and provided commit ID
+// CheckAttributeReader creates a check attribute reader for the current repository and provided commit ID
 func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
 	indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
 	if err != nil {
@@ -303,21 +320,21 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
 	}
 	ctx, cancel := context.WithCancel(repo.Ctx)
 	if err := checker.Init(ctx); err != nil {
-		log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+		log.Error("Unable to open attribute checker for commit %s, error: %v", commitID, err)
 	} else {
 		go func() {
 			err := checker.Run()
-			if err != nil && err != ctx.Err() {
-				log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+			if err != nil && !IsErrCanceledOrKilled(err) {
+				log.Error("Attribute checker for commit %s exits with error: %v", commitID, err)
 			}
 			cancel()
 		}()
 	}
-	deferable := func() {
+	deferrable := func() {
 		_ = checker.Close()
 		cancel()
 		deleteTemporaryFile()
 	}
 
-	return checker, deferable
+	return checker, deferrable
 }
diff --git a/modules/git/repo_attribute_test.go b/modules/git/repo_attribute_test.go
index 0fcd94b4c794e..2e1abe17a9a03 100644
--- a/modules/git/repo_attribute_test.go
+++ b/modules/git/repo_attribute_test.go
@@ -4,10 +4,16 @@
 package git
 
 import (
+	"context"
+	mathRand "math/rand/v2"
+	"path/filepath"
+	"slices"
+	"sync"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
@@ -95,3 +101,57 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
 		Value:     "unspecified",
 	}, attr)
 }
+
+func TestAttributeReader(t *testing.T) {
+	t.Skip() // for debug purpose only, do not run in CI
+
+	ctx := context.Background()
+
+	timeout := 1 * time.Second
+	repoPath := filepath.Join(testReposDir, "language_stats_repo")
+	commitRef := "HEAD"
+
+	oneRound := func(t *testing.T, roundIdx int) {
+		ctx, cancel := context.WithTimeout(ctx, timeout)
+		_ = cancel
+		gitRepo, err := OpenRepository(ctx, repoPath)
+		require.NoError(t, err)
+		defer gitRepo.Close()
+
+		commit, err := gitRepo.GetCommit(commitRef)
+		require.NoError(t, err)
+
+		files, err := gitRepo.LsFiles()
+		require.NoError(t, err)
+
+		randomFiles := slices.Clone(files)
+		randomFiles = append(randomFiles, "any-file-1", "any-file-2")
+
+		t.Logf("Round %v with %d files", roundIdx, len(randomFiles))
+
+		attrReader, deferrable := gitRepo.CheckAttributeReader(commit.ID.String())
+		defer deferrable()
+
+		wg := sync.WaitGroup{}
+		wg.Add(1)
+
+		go func() {
+			for {
+				file := randomFiles[mathRand.IntN(len(randomFiles))]
+				_, err := attrReader.CheckPath(file)
+				if err != nil {
+					for i := 0; i < 10; i++ {
+						_, _ = attrReader.CheckPath(file)
+					}
+					break
+				}
+			}
+			wg.Done()
+		}()
+		wg.Wait()
+	}
+
+	for i := 0; i < 100; i++ {
+		oneRound(t, i)
+	}
+}
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 8d6e9f38e04a7..6f7890c448f74 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -1193,6 +1193,8 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 				if language.Has() {
 					diffFile.Language = language.Value()
 				}
+			} else {
+				checker = nil // CheckPath fails, it's not impossible to "check" anymore
 			}
 		}
 

From 987219ab3ccdbf993a82ff6524c9a94cbcfed868 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 18 Mar 2025 23:13:14 +0800
Subject: [PATCH 152/206] Fix incorrect 1.23 translations (#33932)

Fix #33931
---
 options/locale/locale_zh-CN.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 003aa103e522a..c02d4d64b653d 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -1943,8 +1943,8 @@ pulls.delete.title=删除此合并请求？
 pulls.delete.text=你真的要删除这个合并请求吗？ (这将永久删除所有内容。如果你打算将内容存档，请考虑关闭它)
 
 pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong>
-pulls.upstream_diverging_prompt_behind_1=该分支落后于 %s %d 个提交
-pulls.upstream_diverging_prompt_behind_n=该分支落后于 %s %d 个提交
+pulls.upstream_diverging_prompt_behind_1=该分支落后于 %[2]s %[1]d 个提交
+pulls.upstream_diverging_prompt_behind_n=该分支落后于 %[2]s %[1]d 个提交
 pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改
 pulls.upstream_diverging_merge=同步派生
 

From d6cee7c59640b29fe798fa80a49340ef5711d89f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 21 Mar 2025 20:50:44 +0800
Subject: [PATCH 153/206]  Fix oauth2 auth (#33961) (#33962)

Backport #33961

UI fix is not needed.
---
 routers/web/auth/oauth2_provider.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go
index 1aebc047bd3d7..5dbb529e0aad0 100644
--- a/routers/web/auth/oauth2_provider.go
+++ b/routers/web/auth/oauth2_provider.go
@@ -248,7 +248,7 @@ func AuthorizeOAuth(ctx *context.Context) {
 			}, form.RedirectURI)
 			return
 		}
-		if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
+		if err := ctx.Session.Set("CodeChallenge", form.CodeChallenge); err != nil {
 			handleAuthorizeError(ctx, AuthorizeError{
 				ErrorCode:        ErrorCodeServerError,
 				ErrorDescription: "cannot set code challenge",

From b5f8c4a51052b846013f464c0376c406ff41c588 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 22 Mar 2025 08:46:54 +0800
Subject: [PATCH 154/206] Drop timeout for requests made to the internal hook
 api (#33947) (#33970)

Backport #33947 by Mik4sa

Co-authored-by: Kai Leonhardt <8343141+Mik4sa@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/private/hook.go | 25 ++++++++++++++-----------
 1 file changed, 14 insertions(+), 11 deletions(-)

diff --git a/modules/private/hook.go b/modules/private/hook.go
index 87d6549f9c503..215996b9b9936 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -7,9 +7,9 @@ import (
 	"context"
 	"fmt"
 	"net/url"
-	"time"
 
 	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 )
@@ -82,29 +82,32 @@ type HookProcReceiveRefResult struct {
 	HeadBranch        string
 }
 
+func newInternalRequestAPIForHooks(ctx context.Context, hookName, ownerName, repoName string, opts HookOptions) *httplib.Request {
+	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/%s/%s/%s", hookName, url.PathEscape(ownerName), url.PathEscape(repoName))
+	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
+	// This "timeout" applies to http.Client's timeout: A Timeout of zero means no timeout.
+	// This "timeout" was previously set to `time.Duration(60+len(opts.OldCommitIDs))` seconds, but it caused unnecessary timeout failures.
+	// It should be good enough to remove the client side timeout, only respect the "ctx" and server side timeout.
+	req.SetReadWriteTimeout(0)
+	return req
+}
+
 // HookPreReceive check whether the provided commits are allowed
 func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra {
-	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
-	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
-	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
+	req := newInternalRequestAPIForHooks(ctx, "pre-receive", ownerName, repoName, opts)
 	_, extra := requestJSONResp(req, &ResponseText{})
 	return extra
 }
 
 // HookPostReceive updates services and users
 func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) {
-	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
-	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
-	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
+	req := newInternalRequestAPIForHooks(ctx, "post-receive", ownerName, repoName, opts)
 	return requestJSONResp(req, &HookPostReceiveResult{})
 }
 
 // HookProcReceive proc-receive hook
 func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) {
-	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
-
-	req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
-	req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
+	req := newInternalRequestAPIForHooks(ctx, "proc-receive", ownerName, repoName, opts)
 	return requestJSONResp(req, &HookProcReceiveResult{})
 }
 

From 347101f2a88ce0468f50d09e4f629b33e490d8cc Mon Sep 17 00:00:00 2001
From: TheFox0x7 <thefox0x7@gmail.com>
Date: Sun, 23 Mar 2025 16:35:27 +0100
Subject: [PATCH 155/206] update jwt and redis packages (#33984) (#33987)

---
 go.mod |  6 +++---
 go.sum | 11 ++++++-----
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/go.mod b/go.mod
index 8a7c430f19263..3f4877392ffd9 100644
--- a/go.mod
+++ b/go.mod
@@ -64,7 +64,7 @@ require (
 	github.com/gobwas/glob v0.2.3
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
 	github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
-	github.com/golang-jwt/jwt/v5 v5.2.1
+	github.com/golang-jwt/jwt/v5 v5.2.2
 	github.com/google/go-github/v61 v61.0.0
 	github.com/google/licenseclassifier/v2 v2.0.0
 	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
@@ -100,7 +100,7 @@ require (
 	github.com/pquerna/otp v1.4.0
 	github.com/prometheus/client_golang v1.20.5
 	github.com/quasoft/websspi v1.1.2
-	github.com/redis/go-redis/v9 v9.7.0
+	github.com/redis/go-redis/v9 v9.7.3
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
 	github.com/sassoftware/go-rpmutils v0.4.0
@@ -215,7 +215,7 @@ require (
 	github.com/go-openapi/validate v0.24.0 // indirect
 	github.com/go-webauthn/x v0.1.15 // indirect
 	github.com/goccy/go-json v0.10.3 // indirect
-	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
diff --git a/go.sum b/go.sum
index 34252c50f432b..7b8caddce2c9f 100644
--- a/go.sum
+++ b/go.sum
@@ -373,10 +373,11 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w
 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0=
 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
-github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
 github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
 github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -658,8 +659,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
 github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
 github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
-github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
+github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
+github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
 github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
 github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=

From f514b2651e8bb44696f7857dd265837cb985f797 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Mon, 24 Mar 2025 01:18:29 +0800
Subject: [PATCH 156/206] Update golang crypto and net for 1.23 (#33989)

---
 go.mod | 10 +++++-----
 go.sum | 24 ++++++++++++------------
 2 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/go.mod b/go.mod
index 3f4877392ffd9..1ca481e073ef7 100644
--- a/go.mod
+++ b/go.mod
@@ -118,13 +118,13 @@ require (
 	github.com/yuin/goldmark v1.7.8
 	github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
 	github.com/yuin/goldmark-meta v1.1.0
-	golang.org/x/crypto v0.35.0
+	golang.org/x/crypto v0.36.0
 	golang.org/x/image v0.21.0
-	golang.org/x/net v0.36.0
+	golang.org/x/net v0.37.0
 	golang.org/x/oauth2 v0.27.0
-	golang.org/x/sync v0.11.0
-	golang.org/x/sys v0.30.0
-	golang.org/x/text v0.22.0
+	golang.org/x/sync v0.12.0
+	golang.org/x/sys v0.31.0
+	golang.org/x/text v0.23.0
 	golang.org/x/tools v0.29.0
 	google.golang.org/grpc v1.67.1
 	google.golang.org/protobuf v1.35.1
diff --git a/go.sum b/go.sum
index 7b8caddce2c9f..ebf40cb62849a 100644
--- a/go.sum
+++ b/go.sum
@@ -834,8 +834,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
@@ -870,8 +870,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
+golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
 golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -885,8 +885,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -920,8 +920,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -933,8 +933,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -945,8 +945,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

From f994f3cac6cda36b2be009c3ad64a39cd3933fe8 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 25 Mar 2025 01:02:05 +0800
Subject: [PATCH 157/206] Fix incorrect code search indexer options (#33992)
 (#33999)

Backport #33992 by @wxiaoguang

Fix #33798

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/indexer/code/bleve/bleve.go | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go
index 981fe75c3d596..395c7a0d314db 100644
--- a/modules/indexer/code/bleve/bleve.go
+++ b/modules/indexer/code/bleve/bleve.go
@@ -28,7 +28,6 @@ import (
 	"github.com/blevesearch/bleve/v2"
 	analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
 	analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
-	"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
 	"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
 	"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
 	"github.com/blevesearch/bleve/v2/analysis/tokenizer/letter"
@@ -70,7 +69,7 @@ const (
 	filenameIndexerAnalyzer  = "filenameIndexerAnalyzer"
 	filenameIndexerTokenizer = "filenameIndexerTokenizer"
 	repoIndexerDocType       = "repoIndexerDocType"
-	repoIndexerLatestVersion = 8
+	repoIndexerLatestVersion = 9
 )
 
 // generateBleveIndexMapping generates a bleve index mapping for the repo indexer
@@ -107,7 +106,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
 		"type":          analyzer_custom.Name,
 		"char_filters":  []string{},
 		"tokenizer":     letter.Name,
-		"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
+		"token_filters": []string{unicodeNormalizeName, lowercase.Name},
 	}); err != nil {
 		return nil, err
 	}

From 7758df426409c73a645e135ed95a827948b20bd5 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 24 Mar 2025 13:08:00 -0700
Subject: [PATCH 158/206] Add changelog for 1.23.6 (#33975) (#34000)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
---
 CHANGELOG.md | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a51e31a3dd902..2c112483325ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
+
+* SECURITY
+  * Fix LFS URL (#33840) (#33843)
+  * Update jwt and redis packages (#33984) (#33987)
+  * Update golang crypto and net (#33989)
+* BUGFIXES
+  * Drop timeout for requests made to the internal hook api (#33947) (#33970)
+  * Fix maven panic when no package exists (#33888) (#33889)
+  * Fix markdown render (#33870) (#33875)
+  * Fix auto concurrency cancellation skips commit status updates (#33764) (#33849)
+  * Fix oauth2 auth (#33961) (#33962)
+  * Fix incorrect 1.23 translations (#33932)
+  * Try to figure out attribute checker problem (#33901) (#33902)
+  * Ignore trivial errors when updating push data (#33864) (#33887)
+  * Fix some UI problems for 1.23 (#33856)
+  * Removing unwanted ui container (#33833) (#33835)
+  * Support disable passkey auth (#33348) (#33819)
+  * Do not call "git diff" when listing PRs (#33817)
+  * Try to fix ACME (3rd) (#33807) (#33808)
+  * Fix incorrect code search indexer options (#33992) #33999
+
 ## [1.23.5](https://github.com/go-gitea/gitea/releases/tag/v1.23.5) - 2025-03-03
 
 * SECURITY

From 4b3400bd9c77347664cc3b968626991ac9fd5f6d Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 25 Mar 2025 15:56:44 +0800
Subject: [PATCH 159/206] Git client will follow 301 but 307 (#34005) (#34010)

Backport #34005 by lunny

Fix #28460

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 services/context/repo.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/services/context/repo.go b/services/context/repo.go
index 879b3fcb865c7..da6731df5a211 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -356,7 +356,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
 	if ctx.Req.URL.RawQuery != "" {
 		redirectPath += "?" + ctx.Req.URL.RawQuery
 	}
-	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
+	// Git client needs a 301 redirect by default to follow the new location
+	// It's not documentated in git documentation, but it's the behavior of git client
+	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
 }
 
 func repoAssignment(ctx *Context, repo *repo_model.Repository) {

From 35983ac0a82413be1594d786a37532d3819320fe Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 27 Mar 2025 00:13:22 +0800
Subject: [PATCH 160/206] Polyfill WeakRef (#34025) (#34028)

Backport #34025 by wxiaoguang

Fix #33407

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/js/webcomponents/polyfill.test.ts |  7 +++++++
 web_src/js/webcomponents/polyfills.ts     | 16 ++++++++++++++++
 2 files changed, 23 insertions(+)
 create mode 100644 web_src/js/webcomponents/polyfill.test.ts

diff --git a/web_src/js/webcomponents/polyfill.test.ts b/web_src/js/webcomponents/polyfill.test.ts
new file mode 100644
index 0000000000000..4fb4621547ed5
--- /dev/null
+++ b/web_src/js/webcomponents/polyfill.test.ts
@@ -0,0 +1,7 @@
+import {weakRefClass} from './polyfills.ts';
+
+test('polyfillWeakRef', () => {
+  const WeakRef = weakRefClass();
+  const r = new WeakRef(123);
+  expect(r.deref()).toEqual(123);
+});
diff --git a/web_src/js/webcomponents/polyfills.ts b/web_src/js/webcomponents/polyfills.ts
index 4a84ee9562b93..9575324b5abf1 100644
--- a/web_src/js/webcomponents/polyfills.ts
+++ b/web_src/js/webcomponents/polyfills.ts
@@ -16,3 +16,19 @@ try {
     return intlNumberFormat(locales, options);
   };
 }
+
+export function weakRefClass() {
+  const weakMap = new WeakMap();
+  return class {
+    constructor(target: any) {
+      weakMap.set(this, target);
+    }
+    deref() {
+      return weakMap.get(this);
+    }
+  };
+}
+
+if (!window.WeakRef) {
+  window.WeakRef = weakRefClass() as any;
+}

From 690e810bccc544a134c9012b0dd9d7db73f4759e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 27 Mar 2025 13:36:07 +0800
Subject: [PATCH 161/206] Try to fix check-attr bug (#34029) (#34033)

Backport #34029 by wxiaoguang

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/git/repo_attribute.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 99073a84774e4..e7c6c1a47fa08 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -280,7 +280,7 @@ func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
 		}
 	}
 	wr.tmp = append(wr.tmp, p...)
-	return len(p), nil
+	return l, nil
 }
 
 func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {

From e46f9ff5348974d3d9c5e364dbd58e6447bd10b5 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 27 Mar 2025 17:50:57 +0800
Subject: [PATCH 162/206] Fix the issue with error message logging for the
 `check-attr` command on Windows OS. (#34035) (#34036)

Backport #34035 by charles7668

Close #34022 , #33550

This error message always appears when using the `check-attr` command,
even though it works correctly.
The issue occurs when the stdin writer is closed, so I added a special
case to handle and check the error message when the exit code is 1.

Co-authored-by: charles <30816317+charles7668@users.noreply.github.com>
---
 modules/git/command.go | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/modules/git/command.go b/modules/git/command.go
index fd781ffef4fd9..dc75adfacefca 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -350,9 +350,10 @@ func (c *Command) Run(opts *RunOpts) error {
 	// We need to check if the context is canceled by the program on Windows.
 	// This is because Windows does not have signal checking when terminating the process.
 	// It always returns exit code 1, unlike Linux, which has many exit codes for signals.
+	// `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled.
 	if runtime.GOOS == "windows" &&
 		err != nil &&
-		err.Error() == "" &&
+		(err.Error() == "" || err.Error() == "exit status 1") &&
 		cmd.ProcessState.ExitCode() == 1 &&
 		ctx.Err() == context.Canceled {
 		return ctx.Err()

From 8d1be2a9c58b540399a4b637c84915e0c6d17306 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 28 Mar 2025 14:59:46 +0800
Subject: [PATCH 163/206] Fix git client accessing renamed repo (#34034)
 (#34043)

Backport #34034
---
 routers/web/githttp.go                   | 16 +--------------
 tests/integration/git_smart_http_test.go | 26 +++++++++++++++++++++++-
 2 files changed, 26 insertions(+), 16 deletions(-)

diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index 5831e6f52366d..06de811f16e11 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -4,26 +4,12 @@
 package web
 
 import (
-	"net/http"
-
-	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/web/repo"
 	"code.gitea.io/gitea/services/context"
 )
 
 func addOwnerRepoGitHTTPRouters(m *web.Router) {
-	reqGitSignIn := func(ctx *context.Context) {
-		if !setting.Service.RequireSignInView {
-			return
-		}
-		// rely on the results of Contexter
-		if !ctx.IsSigned {
-			// TODO: support digit auth - which would be Authorization header with digit
-			ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
-			ctx.Error(http.StatusUnauthorized)
-		}
-	}
 	m.Group("/{username}/{reponame}", func() {
 		m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
 		m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
 		m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
 		m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
-	}, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
+	}, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
 }
diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go
index 15336b9b81f39..132171cde4e56 100644
--- a/tests/integration/git_smart_http_test.go
+++ b/tests/integration/git_smart_http_test.go
@@ -9,11 +9,17 @@ import (
 	"net/url"
 	"testing"
 
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
+
 	"github.com/stretchr/testify/assert"
 )
 
 func TestGitSmartHTTP(t *testing.T) {
-	onGiteaRun(t, testGitSmartHTTP)
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		testGitSmartHTTP(t, u)
+		testRenamedRepoRedirect(t)
+	})
 }
 
 func testGitSmartHTTP(t *testing.T, u *url.URL) {
@@ -66,3 +72,21 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
 		})
 	}
 }
+
+func testRenamedRepoRedirect(t *testing.T) {
+	defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
+
+	// git client requires to get a 301 redirect response before 401 unauthorized response
+	req := NewRequest(t, "GET", "/user2/oldrepo1/info/refs")
+	resp := MakeRequest(t, req, http.StatusMovedPermanently)
+	redirect := resp.Header().Get("Location")
+	assert.Equal(t, "/user2/repo1/info/refs", redirect)
+
+	req = NewRequest(t, "GET", redirect)
+	resp = MakeRequest(t, req, http.StatusUnauthorized)
+	assert.Equal(t, "Unauthorized\n", resp.Body.String())
+
+	req = NewRequest(t, "GET", redirect).AddBasicAuth("user2")
+	resp = MakeRequest(t, req, http.StatusOK)
+	assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1")
+}

From b3516767fbeb44be24fcbb458cdac17fb880ba7c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 28 Mar 2025 15:59:46 +0800
Subject: [PATCH 164/206] fix org repo creation being limited by user limits
 (#34030) (#34044)

Backport #34030
---
 templates/repo/create.tmpl           | 15 +++++++--------
 web_src/js/features/repo-template.ts | 16 +++++++++++++++-
 2 files changed, 22 insertions(+), 9 deletions(-)

diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl
index 2e1de244ea19c..07d581b2e801a 100644
--- a/templates/repo/create.tmpl
+++ b/templates/repo/create.tmpl
@@ -10,12 +10,7 @@
 				<div class="ui attached segment">
 					{{template "base/alert" .}}
 					{{template "repo/create_helper" .}}
-
-					{{if not .CanCreateRepo}}
-						<div class="ui negative message">
-							<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
-						</div>
-					{{end}}
+					<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
 					<div class="inline required field {{if .Err_Owner}}error{{end}}">
 						<label>{{ctx.Locale.Tr "repo.owner"}}</label>
 						<div class="ui selection owner dropdown">
@@ -26,7 +21,11 @@
 							</span>
 							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 							<div class="menu">
-								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
+								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"
+								{{if not .CanCreateRepo}}
+									data-create-repo-disallowed-prompt="{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}"
+								{{end}}
+								>
 									{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
 									<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
 								</div>
@@ -209,7 +208,7 @@
 					<br>
 					<div class="inline field">
 						<label></label>
-						<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
+						<button class="ui primary button">
 							{{ctx.Locale.Tr "repo.create_repo"}}
 						</button>
 					</div>
diff --git a/web_src/js/features/repo-template.ts b/web_src/js/features/repo-template.ts
index fbd7b656ed941..0a689e803f40d 100644
--- a/web_src/js/features/repo-template.ts
+++ b/web_src/js/features/repo-template.ts
@@ -1,6 +1,6 @@
 import $ from 'jquery';
 import {htmlEscape} from 'escape-goat';
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
 
 const {appSubUrl} = window.config;
 
@@ -21,6 +21,20 @@ export function initRepoTemplateSearch() {
   checkTemplate();
 
   const changeOwner = function () {
+    const elUid = document.querySelector<HTMLInputElement>('#uid');
+    const elForm = elUid.closest('form');
+    const elSubmitButton = querySingleVisibleElem<HTMLInputElement>(elForm, '.ui.primary.button');
+    const elCreateRepoErrorMessage = elForm.querySelector('#create-repo-error-message');
+    const elOwnerItem = document.querySelector(`.ui.selection.owner.dropdown .menu > .item[data-value="${CSS.escape(elUid.value)}"]`);
+    hideElem(elCreateRepoErrorMessage);
+    elSubmitButton.disabled = false;
+    if (elOwnerItem) {
+      elCreateRepoErrorMessage.textContent = elOwnerItem.getAttribute('data-create-repo-disallowed-prompt') ?? '';
+      const hasError = Boolean(elCreateRepoErrorMessage.textContent);
+      toggleElem(elCreateRepoErrorMessage, hasError);
+      elSubmitButton.disabled = hasError;
+    }
+
     $('#repo_template_search')
       .dropdown({
         apiSettings: {

From eee4a752a55a5b0021f14420c60948223ebe8fd2 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 28 Mar 2025 18:30:29 +0800
Subject: [PATCH 165/206] Simplify emoji rendering (#34048) (#34049)

Backport #34048 by silverwind

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/base.css           | 9 +--------
 web_src/css/markup/content.css | 5 -----
 2 files changed, 1 insertion(+), 13 deletions(-)

diff --git a/web_src/css/base.css b/web_src/css/base.css
index 1a354c96633fe..01eff4ac07029 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1225,14 +1225,7 @@ table th[data-sortt-desc] .svg {
   box-shadow: 0 0 0 1px var(--color-secondary) inset;
 }
 
-.emoji {
-  font-size: 1.25em;
-  line-height: var(--line-height-default);
-  font-style: normal !important;
-  font-weight: var(--font-weight-normal) !important;
-  vertical-align: -0.075em;
-}
-
+/* for "image" emojis like ":git:"  ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */
 .emoji img {
   border-width: 0 !important;
   margin: 0 !important;
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index d2dcf2ec6ec38..4c6940e027784 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -337,11 +337,6 @@
   padding-right: 28px;
 }
 
-.markup .emoji {
-  max-width: none;
-  vertical-align: text-top;
-}
-
 .markup span.frame {
   display: block;
   overflow: hidden;

From 5d81f6d73f2382e829ed0cb24e6b37f4bd5bdb3d Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 28 Mar 2025 20:37:27 +0800
Subject: [PATCH 166/206] Adjust the layout of the toolbar on the
 Issues/Projects page (#33667) (#34047)

And fix layout for mobile

Backport #33667

Fix #33880

---------

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
---
 templates/projects/view.tmpl      |  9 ++++-----
 templates/repo/issue/list.tmpl    |  5 +++--
 templates/repo/projects/view.tmpl |  5 +++--
 web_src/css/base.css              |  5 +++++
 web_src/css/features/projects.css | 12 ------------
 5 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 82d744bbc29bc..217ffe635589b 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -1,10 +1,10 @@
 {{$canWriteProject := and .CanWriteProjects (or (not .Repository) (not .Repository.IsArchived))}}
 
 <div class="ui container tw-max-w-full">
-	<div class="tw-flex tw-justify-between tw-items-center tw-mb-4 tw-gap-3">
-		<h2 class="tw-mb-0 tw-flex-1 tw-break-anywhere">{{.Project.Title}}</h2>
-			<div class="project-toolbar-right">
-				<div class="ui secondary filter menu labels">
+	<div class="flex-text-block tw-flex-wrap tw-mb-4">
+		<h2 class="tw-mb-0">{{.Project.Title}}</h2>
+		<div class="tw-flex-1"></div>
+				<div class="ui secondary menu tw-m-0">
 					{{$queryLink := QueryBuild "?" "labels" .SelectLabels "assignee" $.AssigneeID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
 
 					{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
@@ -19,7 +19,6 @@
 						"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
 					}}
 				</div>
-			</div>
 		{{if $canWriteProject}}
 			<div class="ui compact mini menu">
 				<a class="item" href="{{.Link}}/edit?redirect=project">
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 01b610b39db0b..ee783b420ae9b 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -14,9 +14,10 @@
 		</div>
 	{{end}}
 
-		<div class="list-header">
-			{{template "repo/issue/navbar" .}}
+		<div class="list-header flex-text-block">
 			{{template "repo/issue/search" .}}
+			<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
+			<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 			{{if not .Repository.IsArchived}}
 				{{if .PageIsIssueList}}
 					<a class="ui small primary button issue-list-new" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
diff --git a/templates/repo/projects/view.tmpl b/templates/repo/projects/view.tmpl
index 05ad7264bf8fa..7267a99b1db0f 100644
--- a/templates/repo/projects/view.tmpl
+++ b/templates/repo/projects/view.tmpl
@@ -2,8 +2,9 @@
 <div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
 	{{template "repo/header" .}}
 	<div class="ui container padded">
-		<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
-			{{template "repo/issue/navbar" .}}
+		<div class="flex-text-block tw-justify-end tw-mb-4">
+			<a class="ui small button" href="{{.RepoLink}}/labels">{{ctx.Locale.Tr "repo.labels"}}</a>
+			<a class="ui small button" href="{{.RepoLink}}/milestones">{{ctx.Locale.Tr "repo.milestones"}}</a>
 			<a class="ui small primary button" href="{{.RepoLink}}/issues/new/choose?project={{.Project.ID}}">{{ctx.Locale.Tr "repo.issues.new"}}</a>
 		</div>
 	</div>
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 01eff4ac07029..28bb6ce7ad9d6 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1384,6 +1384,11 @@ table th[data-sortt-desc] .svg {
   min-width: 0;
 }
 
+.flex-text-block > .ui.button,
+.flex-text-inline > .ui.button {
+  margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */
+}
+
 /* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content
 the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */
 .ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) {
diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css
index 9d462c4c4abb9..ef3b036217a1c 100644
--- a/web_src/css/features/projects.css
+++ b/web_src/css/features/projects.css
@@ -8,18 +8,6 @@
   margin: 0 0.5em;
 }
 
-.project-toolbar-right .filter.menu {
-  flex-direction: row;
-  flex-wrap: wrap;
-}
-
-@media (max-width: 767.98px) {
-  .project-toolbar-right .dropdown .menu {
-    left: auto !important;
-    right: auto !important;
-  }
-}
-
 .project-column {
   background-color: var(--color-project-column-bg) !important;
   border: 1px solid var(--color-secondary) !important;

From 7f962a16c9d8c02e822a2fdea4cf2e0c584fad21 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 28 Mar 2025 10:24:31 -0700
Subject: [PATCH 167/206] Pull request updates will also trigger code owners
 review requests (#33744) (#34045)

Fix #33490
Backport #33744

It will only read the changed file on the pushed commits but not all the
files of this PR.

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/repo/view_file.go         |  3 +-
 services/issue/issue.go               |  6 ++-
 services/issue/pull.go                | 42 +++++++++++++-----
 services/pull/merge.go                | 10 ++++-
 services/pull/pull.go                 | 62 +++++++++++++++++++--------
 services/pull/update.go               | 20 ++++++++-
 services/repository/push.go           | 12 +++++-
 tests/integration/pull_review_test.go | 44 +++++++++++++++++--
 8 files changed, 159 insertions(+), 40 deletions(-)

diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index 17c2821824339..63a117b60c5ca 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -9,7 +9,6 @@ import (
 	"image"
 	"io"
 	"path"
-	"slices"
 	"strings"
 
 	git_model "code.gitea.io/gitea/models/git"
@@ -79,7 +78,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
 		if workFlowErr != nil {
 			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
 		}
-	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
+	} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
 		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
 			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
 			if len(warnings) > 0 {
diff --git a/services/issue/issue.go b/services/issue/issue.go
index c6a52cc0fe512..b24ca3001965b 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -92,8 +92,12 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
 
 	var reviewNotifiers []*ReviewRequestNotifier
 	if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
+		if err := issue.LoadPullRequest(ctx); err != nil {
+			return err
+		}
+
 		var err error
-		reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
+		reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue.PullRequest)
 		if err != nil {
 			log.Error("PullRequestCodeOwnersReview: %v", err)
 		}
diff --git a/services/issue/pull.go b/services/issue/pull.go
index 896802108d6ca..bd19c254362b9 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -6,6 +6,7 @@ package issue
 import (
 	"context"
 	"fmt"
+	"slices"
 	"time"
 
 	issues_model "code.gitea.io/gitea/models/issues"
@@ -40,20 +41,31 @@ type ReviewRequestNotifier struct {
 	ReviewTeam *org_model.Team
 }
 
-func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
-	files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
+var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
 
+func IsCodeOwnerFile(f string) bool {
+	return slices.Contains(codeOwnerFiles, f)
+}
+
+func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
+	return PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, "", "") // no commit is provided, then it uses PR's base&head branch
+}
+
+func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_model.PullRequest, startCommitID, endCommitID string) ([]*ReviewRequestNotifier, error) {
+	if err := pr.LoadIssue(ctx); err != nil {
+		return nil, err
+	}
+	issue := pr.Issue
 	if pr.IsWorkInProgress(ctx) {
 		return nil, nil
 	}
-
 	if err := pr.LoadHeadRepo(ctx); err != nil {
 		return nil, err
 	}
-
 	if err := pr.LoadBaseRepo(ctx); err != nil {
 		return nil, err
 	}
+	pr.Issue.Repo = pr.BaseRepo
 
 	if pr.BaseRepo.IsFork {
 		return nil, nil
@@ -71,7 +83,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 	}
 
 	var data string
-	for _, file := range files {
+	for _, file := range codeOwnerFiles {
 		if blob, err := commit.GetBlobByPath(file); err == nil {
 			data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
 			if err == nil {
@@ -79,18 +91,28 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
 			}
 		}
 	}
+	if data == "" {
+		return nil, nil
+	}
 
 	rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
+	if len(rules) == 0 {
+		return nil, nil
+	}
 
-	// get the mergebase
-	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
-	if err != nil {
-		return nil, err
+	if startCommitID == "" && endCommitID == "" {
+		// get the mergebase
+		mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
+		if err != nil {
+			return nil, err
+		}
+		startCommitID = mergeBase
+		endCommitID = pr.GetGitRefName()
 	}
 
 	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
 	// between the merge base and the head commit but not the base branch and the head commit
-	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
+	changedFiles, err := repo.GetFilesChangedBetween(startCommitID, endCommitID)
 	if err != nil {
 		return nil, err
 	}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index a3fbe4f627b00..63eb91b4a9879 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -189,7 +189,15 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
 	}
 	defer releaser()
 	defer func() {
-		go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
+		go AddTestPullRequestTask(TestPullRequestOptions{
+			RepoID:      pr.BaseRepo.ID,
+			Doer:        doer,
+			Branch:      pr.BaseBranch,
+			IsSync:      false,
+			IsForcePush: false,
+			OldCommitID: "",
+			NewCommitID: "",
+		})
 	}()
 
 	_, err = doMergeAndPush(ctx, pr, doer, mergeStyle, expectedHeadCommitID, message, repo_module.PushTriggerPRMergeToBase)
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 98bd28a4a5441..11718c2f877ef 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -176,7 +176,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
 		}
 
 		if !pr.IsWorkInProgress(ctx) {
-			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
+			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
 			if err != nil {
 				return err
 			}
@@ -349,19 +349,29 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
 	return nil
 }
 
+type TestPullRequestOptions struct {
+	RepoID      int64
+	Doer        *user_model.User
+	Branch      string
+	IsSync      bool // True means it's a pull request synchronization, false means it's triggered for pull request merging or updating
+	IsForcePush bool
+	OldCommitID string
+	NewCommitID string
+}
+
 // AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
 // and generate new patch for testing as needed.
-func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string, isSync bool, oldCommitID, newCommitID string) {
-	log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
+func AddTestPullRequestTask(opts TestPullRequestOptions) {
+	log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
 	graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
 		// There is no sensible way to shut this down ":-("
 		// If you don't let it run all the way then you will lose data
 		// TODO: graceful: AddTestPullRequestTask needs to become a queue!
 
 		// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
-		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repoID, branch)
+		prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
 		if err != nil {
-			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
+			log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
 			return
 		}
 
@@ -377,25 +387,24 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 			}
 
 			AddToTaskQueue(ctx, pr)
-			comment, err := CreatePushPullComment(ctx, doer, pr, oldCommitID, newCommitID)
+			comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
 			if err == nil && comment != nil {
-				notify_service.PullRequestPushCommits(ctx, doer, pr, comment)
+				notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
 			}
 		}
 
-		if isSync {
-			requests := issues_model.PullRequestList(prs)
-			if err = requests.LoadAttributes(ctx); err != nil {
+		if opts.IsSync {
+			if err = issues_model.PullRequestList(prs).LoadAttributes(ctx); err != nil {
 				log.Error("PullRequestList.LoadAttributes: %v", err)
 			}
-			if invalidationErr := checkForInvalidation(ctx, requests, repoID, doer, branch); invalidationErr != nil {
+			if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
 				log.Error("checkForInvalidation: %v", invalidationErr)
 			}
 			if err == nil {
 				for _, pr := range prs {
 					objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
-					if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
-						changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
+					if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
+						changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
 						if err != nil {
 							log.Error("checkIfPRContentChanged: %v", err)
 						}
@@ -411,12 +420,12 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 								log.Error("GetFirstMatchProtectedBranchRule: %v", err)
 							}
 							if pb != nil && pb.DismissStaleApprovals {
-								if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
+								if err := DismissApprovalReviews(ctx, opts.Doer, pr); err != nil {
 									log.Error("DismissApprovalReviews: %v", err)
 								}
 							}
 						}
-						if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
+						if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
 							log.Error("MarkReviewsAsNotStale: %v", err)
 						}
 						divergence, err := GetDiverging(ctx, pr)
@@ -430,15 +439,30 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
 						}
 					}
 
-					notify_service.PullRequestSynchronized(ctx, doer, pr)
+					if !pr.IsWorkInProgress(ctx) {
+						var reviewNotifiers []*issue_service.ReviewRequestNotifier
+						if opts.IsForcePush {
+							reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
+						} else {
+							reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, opts.OldCommitID, opts.NewCommitID)
+						}
+						if err != nil {
+							log.Error("PullRequestCodeOwnersReview: %v", err)
+						}
+						if len(reviewNotifiers) > 0 {
+							issue_service.ReviewRequestNotify(ctx, pr.Issue, opts.Doer, reviewNotifiers)
+						}
+					}
+
+					notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
 				}
 			}
 		}
 
-		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
-		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repoID, branch)
+		log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
+		prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
 		if err != nil {
-			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
+			log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
 			return
 		}
 		for _, pr := range prs {
diff --git a/services/pull/update.go b/services/pull/update.go
index abf7ad45091ae..3e00dd4e65f72 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -42,7 +42,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
 
 	if rebase {
 		defer func() {
-			go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
+			go AddTestPullRequestTask(TestPullRequestOptions{
+				RepoID:      pr.BaseRepo.ID,
+				Doer:        doer,
+				Branch:      pr.BaseBranch,
+				IsSync:      false,
+				IsForcePush: false,
+				OldCommitID: "",
+				NewCommitID: "",
+			})
 		}()
 
 		return updateHeadByRebaseOnToBase(ctx, pr, doer)
@@ -83,7 +91,15 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
 	_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
 
 	defer func() {
-		go AddTestPullRequestTask(doer, reversePR.HeadRepo.ID, reversePR.HeadBranch, false, "", "")
+		go AddTestPullRequestTask(TestPullRequestOptions{
+			RepoID:      reversePR.HeadRepo.ID,
+			Doer:        doer,
+			Branch:      reversePR.HeadBranch,
+			IsSync:      false,
+			IsForcePush: false,
+			OldCommitID: "",
+			NewCommitID: "",
+		})
 	}()
 
 	return err
diff --git a/services/repository/push.go b/services/repository/push.go
index 9f20563014aa2..211bfddde28a1 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -170,7 +170,6 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 			branch := opts.RefFullName.BranchName()
 			if !opts.IsDelRef() {
 				log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
-				go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true, opts.OldCommitID, opts.NewCommitID)
 
 				newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
 				if err != nil {
@@ -212,6 +211,17 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
 						log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
 					}
 
+					// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
+					go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
+						RepoID:      repo.ID,
+						Doer:        pusher,
+						Branch:      branch,
+						IsSync:      true,
+						IsForcePush: isForcePush,
+						OldCommitID: opts.OldCommitID,
+						NewCommitID: opts.NewCommitID,
+					})
+
 					if isForcePush {
 						log.Trace("Push %s is a force push", opts.NewCommitID)
 
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 5ecf3ef46918b..878020625b48b 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"path"
+	"sort"
 	"strings"
 	"testing"
 
@@ -67,7 +68,7 @@ func TestPullView_CodeOwner(t *testing.T) {
 				{
 					Operation:     "create",
 					TreePath:      "CODEOWNERS",
-					ContentReader: strings.NewReader("README.md @user5\n"),
+					ContentReader: strings.NewReader("README.md @user5\nuser8-file.md @user8\n"),
 				},
 			},
 		})
@@ -75,7 +76,7 @@ func TestPullView_CodeOwner(t *testing.T) {
 
 		t.Run("First Pull Request", func(t *testing.T) {
 			// create a new branch to prepare for pull request
-			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			resp1, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
 				NewBranch: "codeowner-basebranch",
 				Files: []*files_service.ChangeRepoFile{
 					{
@@ -95,7 +96,37 @@ func TestPullView_CodeOwner(t *testing.T) {
 			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
 			assert.NoError(t, pr.LoadIssue(db.DefaultContext))
 
-			err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
+			reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
+			assert.NoError(t, err)
+			assert.Len(t, reviewNotifiers, 1)
+			assert.EqualValues(t, 5, reviewNotifiers[0].Reviewer.ID)
+
+			// update the file on the pr branch
+			resp2, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+				OldBranch: "codeowner-basebranch",
+				Files: []*files_service.ChangeRepoFile{
+					{
+						Operation:     "create",
+						TreePath:      "user8-file.md",
+						ContentReader: strings.NewReader("# This is a new project2\n"),
+					},
+				},
+			})
+			assert.NoError(t, err)
+
+			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
+			assert.NoError(t, err)
+			assert.Len(t, reviewNotifiers, 2)
+			reviewerIDs := []int64{reviewNotifiers[0].Reviewer.ID, reviewNotifiers[1].Reviewer.ID}
+			sort.Slice(reviewerIDs, func(i, j int) bool { return reviewerIDs[i] < reviewerIDs[j] })
+			assert.EqualValues(t, []int64{5, 8}, reviewerIDs)
+
+			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(db.DefaultContext, pr, resp1.Commit.SHA, resp2.Commit.SHA)
+			assert.NoError(t, err)
+			assert.Len(t, reviewNotifiers, 1)
+			assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
+
+			err = issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
 			assert.NoError(t, err)
 			prUpdated1 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
 			assert.NoError(t, prUpdated1.LoadIssue(db.DefaultContext))
@@ -139,7 +170,12 @@ func TestPullView_CodeOwner(t *testing.T) {
 			testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2")
 
 			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
-			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+			unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
+
+			reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
+			assert.NoError(t, err)
+			assert.Len(t, reviewNotifiers, 1)
+			assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
 		})
 
 		t.Run("Forked Repo Pull Request", func(t *testing.T) {

From dd901983c0a3a6087f6a83f95453f4f484bc0ff0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 30 Mar 2025 03:30:27 +0800
Subject: [PATCH 168/206] Fix repo-template.ts error in 1.23 (#34060)

Fix #34059
---
 web_src/js/features/repo-template.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/web_src/js/features/repo-template.ts b/web_src/js/features/repo-template.ts
index 0a689e803f40d..f3d79eba80af7 100644
--- a/web_src/js/features/repo-template.ts
+++ b/web_src/js/features/repo-template.ts
@@ -6,6 +6,8 @@ const {appSubUrl} = window.config;
 
 export function initRepoTemplateSearch() {
   const $repoTemplate = $('#repo_template');
+  if (!$repoTemplate.length) return; // make sure the current page is "new repo" page
+
   const checkTemplate = function () {
     const $templateUnits = $('#template_units');
     const $nonTemplate = $('#non_template');

From 5a9b3bfa50ff45c65d8d1b17ad960fb05f38ef84 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sun, 30 Mar 2025 13:51:08 +0800
Subject: [PATCH 169/206] add additional ReplaceAll in pathsep to cater for
 different pathsep (#34061) (#34070)

Backport #34061 by eeyrjmr

The doctor storage check reconstructs the lfs oid by producing a string
where the path separator is stripped
ab/dc/efg -> abdcefg. Windows however uses a backslash and thus the
ReplaceAll call doesn't produce the correct oid resulting in all lfs
objects being classed as orphaned.
This PR allows this to be more OS agnostic.

Closes #34039

Co-authored-by: JonRB <4564448+eeyrjmr@users.noreply.github.com>
---
 services/doctor/storage.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/doctor/storage.go b/services/doctor/storage.go
index 3f3b562c370c7..77fc6d65dfafc 100644
--- a/services/doctor/storage.go
+++ b/services/doctor/storage.go
@@ -121,7 +121,7 @@ func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger lo
 					storer: storage.LFS,
 					isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
 						// The oid of an LFS stored object is the name but with all the path.Separators removed
-						oid := strings.ReplaceAll(path, "/", "")
+						oid := strings.ReplaceAll(strings.ReplaceAll(path, "\\", ""), "/", "")
 						exists, err := git.ExistsLFSObject(ctx, oid)
 						return !exists, err
 					},

From 15e93a751cb455d2bafb4127ce6a350f4e3a1bc6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 30 Mar 2025 14:16:32 +0800
Subject: [PATCH 170/206] Add a config option to block "expensive" pages for
 anonymous users (#34024) (#34071)

Backport #34024 since there are too many AI crawlers. The new code is
covered by tests and it does nothing if users don't set it.
---
 custom/conf/app.example.ini                   |  3 +
 modules/setting/config_provider.go            |  1 +
 modules/setting/service.go                    | 16 +++-
 modules/setting/service_test.go               | 41 +++++++--
 routers/api/packages/cargo/cargo.go           |  2 +-
 routers/api/packages/container/container.go   |  4 +-
 routers/api/v1/api.go                         |  4 +-
 routers/common/blockexpensive.go              | 91 +++++++++++++++++++
 routers/common/blockexpensive_test.go         | 30 ++++++
 routers/install/install.go                    |  2 +-
 routers/private/serv.go                       |  2 +-
 routers/web/repo/githttp.go                   |  2 +-
 routers/web/web.go                            | 16 ++--
 services/context/package.go                   |  2 +-
 services/packages/cargo/index.go              |  2 +-
 templates/admin/config.tmpl                   |  2 +-
 tests/integration/api_org_test.go             |  4 +-
 .../api_packages_container_test.go            |  2 +-
 .../integration/api_packages_generic_test.go  |  7 +-
 tests/integration/git_smart_http_test.go      |  2 +-
 tests/integration/signin_test.go              | 31 +++++++
 21 files changed, 228 insertions(+), 38 deletions(-)
 create mode 100644 routers/common/blockexpensive.go
 create mode 100644 routers/common/blockexpensive_test.go

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index b417baae8b357..cb85082839957 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -774,6 +774,9 @@ LEVEL = Info
 ;ALLOW_ONLY_EXTERNAL_REGISTRATION = false
 ;;
 ;; User must sign in to view anything.
+;; After 1.23.7, it could be set to "expensive" to block anonymous users accessing some pages which consume a lot of resources,
+;; for example: block anonymous AI crawlers from accessing repo code pages.
+;; The "expensive" mode is experimental and subject to change.
 ;REQUIRE_SIGNIN_VIEW = false
 ;;
 ;; Mail notification
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index 3138f8a63eeda..b34751e9593c2 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -26,6 +26,7 @@ type ConfigKey interface {
 	In(defaultVal string, candidates []string) string
 	String() string
 	Strings(delim string) []string
+	Bool() (bool, error)
 
 	MustString(defaultVal string) string
 	MustBool(defaultVal ...bool) bool
diff --git a/modules/setting/service.go b/modules/setting/service.go
index 8c1843eeb7548..6f0bcb48bbbfa 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -43,7 +43,8 @@ var Service = struct {
 	ShowRegistrationButton                  bool
 	EnablePasswordSignInForm                bool
 	ShowMilestonesDashboardPage             bool
-	RequireSignInView                       bool
+	RequireSignInViewStrict                 bool
+	BlockAnonymousAccessExpensive           bool
 	EnableNotifyMail                        bool
 	EnableBasicAuth                         bool
 	EnablePasskeyAuth                       bool
@@ -159,7 +160,18 @@ func loadServiceFrom(rootCfg ConfigProvider) {
 	Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
 	Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
 	Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
-	Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
+
+	// boolean values are considered as "strict"
+	var err error
+	Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
+	if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
+		// non-boolean value only supports "expensive" at the moment
+		Service.BlockAnonymousAccessExpensive = s == "expensive"
+		if !Service.BlockAnonymousAccessExpensive {
+			log.Error("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
+		}
+	}
+
 	Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
 	Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
 	Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go
index 1647bcec160a1..73736b793a8db 100644
--- a/modules/setting/service_test.go
+++ b/modules/setting/service_test.go
@@ -7,16 +7,14 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/test"
 
 	"github.com/gobwas/glob"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestLoadServices(t *testing.T) {
-	oldService := Service
-	defer func() {
-		Service = oldService
-	}()
+	defer test.MockVariableValue(&Service)()
 
 	cfg, err := NewConfigProviderFromData(`
 [service]
@@ -48,10 +46,7 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
 }
 
 func TestLoadServiceVisibilityModes(t *testing.T) {
-	oldService := Service
-	defer func() {
-		Service = oldService
-	}()
+	defer test.MockVariableValue(&Service)()
 
 	kases := map[string]func(){
 		`
@@ -130,3 +125,33 @@ ALLOWED_USER_VISIBILITY_MODES = public, limit, privated
 		})
 	}
 }
+
+func TestLoadServiceRequireSignInView(t *testing.T) {
+	defer test.MockVariableValue(&Service)()
+
+	cfg, err := NewConfigProviderFromData(`
+[service]
+`)
+	assert.NoError(t, err)
+	loadServiceFrom(cfg)
+	assert.False(t, Service.RequireSignInViewStrict)
+	assert.False(t, Service.BlockAnonymousAccessExpensive)
+
+	cfg, err = NewConfigProviderFromData(`
+[service]
+REQUIRE_SIGNIN_VIEW = true
+`)
+	assert.NoError(t, err)
+	loadServiceFrom(cfg)
+	assert.True(t, Service.RequireSignInViewStrict)
+	assert.False(t, Service.BlockAnonymousAccessExpensive)
+
+	cfg, err = NewConfigProviderFromData(`
+[service]
+REQUIRE_SIGNIN_VIEW = expensive
+`)
+	assert.NoError(t, err)
+	loadServiceFrom(cfg)
+	assert.False(t, Service.RequireSignInViewStrict)
+	assert.True(t, Service.BlockAnonymousAccessExpensive)
+}
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index 3d8407e6b6000..c1755dc1e84d5 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) {
 
 // https://rust-lang.github.io/rfcs/2789-sparse-index.html
 func RepositoryConfig(ctx *context.Context) {
-	ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
+	ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
 }
 
 func EnumeratePackageVersions(ctx *context.Context) {
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index bb14db9db79d2..6ef1655235617 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -126,7 +126,7 @@ func apiUnauthorizedError(ctx *context.Context) {
 
 // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
 func ReqContainerAccess(ctx *context.Context) {
-	if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
+	if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
 		apiUnauthorizedError(ctx)
 	}
 }
@@ -152,7 +152,7 @@ func Authenticate(ctx *context.Context) {
 	u := ctx.Doer
 	packageScope := auth_service.GetAccessScope(ctx.Data)
 	if u == nil {
-		if setting.Service.RequireSignInView {
+		if setting.Service.RequireSignInViewStrict {
 			apiUnauthorizedError(ctx)
 			return
 		}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index d0a2bd8a2786e..f937a475b361a 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -356,7 +356,7 @@ func reqToken() func(ctx *context.APIContext) {
 
 func reqExploreSignIn() func(ctx *context.APIContext) {
 	return func(ctx *context.APIContext) {
-		if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
+		if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned {
 			ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users")
 		}
 	}
@@ -874,7 +874,7 @@ func Routes() *web.Router {
 	m.Use(apiAuth(buildAuthGroup()))
 
 	m.Use(verifyAuthWithOptions(&common.VerifyOptions{
-		SignInRequired: setting.Service.RequireSignInView,
+		SignInRequired: setting.Service.RequireSignInViewStrict,
 	}))
 
 	addActionsRoutes := func(
diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go
new file mode 100644
index 0000000000000..1aa0f48d2f013
--- /dev/null
+++ b/routers/common/blockexpensive.go
@@ -0,0 +1,91 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+	"context"
+	"net/http"
+	"strings"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/web/middleware"
+
+	"github.com/go-chi/chi/v5"
+)
+
+func BlockExpensive() func(next http.Handler) http.Handler {
+	if !setting.Service.BlockAnonymousAccessExpensive {
+		return nil
+	}
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			ret := determineRequestPriority(req.Context())
+			if !ret.SignedIn {
+				if ret.Expensive || ret.LongPolling {
+					http.Redirect(w, req, setting.AppSubURL+"/user/login", http.StatusSeeOther)
+					return
+				}
+			}
+			next.ServeHTTP(w, req)
+		})
+	}
+}
+
+func isRoutePathExpensive(routePattern string) bool {
+	if strings.HasPrefix(routePattern, "/user/") || strings.HasPrefix(routePattern, "/login/") {
+		return false
+	}
+
+	expensivePaths := []string{
+		// code related
+		"/{username}/{reponame}/archive/",
+		"/{username}/{reponame}/blame/",
+		"/{username}/{reponame}/commit/",
+		"/{username}/{reponame}/commits/",
+		"/{username}/{reponame}/graph",
+		"/{username}/{reponame}/media/",
+		"/{username}/{reponame}/raw/",
+		"/{username}/{reponame}/src/",
+
+		// issue & PR related (no trailing slash)
+		"/{username}/{reponame}/issues",
+		"/{username}/{reponame}/{type:issues}",
+		"/{username}/{reponame}/pulls",
+		"/{username}/{reponame}/{type:pulls}",
+
+		// wiki
+		"/{username}/{reponame}/wiki/",
+
+		// activity
+		"/{username}/{reponame}/activity/",
+	}
+	for _, path := range expensivePaths {
+		if strings.HasPrefix(routePattern, path) {
+			return true
+		}
+	}
+	return false
+}
+
+func isRoutePathForLongPolling(routePattern string) bool {
+	return routePattern == "/user/events"
+}
+
+func determineRequestPriority(ctx context.Context) (ret struct {
+	SignedIn    bool
+	Expensive   bool
+	LongPolling bool
+},
+) {
+	dataStore := middleware.GetContextData(ctx)
+	chiRoutePath := chi.RouteContext(ctx).RoutePattern()
+	if _, ok := dataStore[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
+		ret.SignedIn = true
+	} else {
+		ret.Expensive = isRoutePathExpensive(chiRoutePath)
+		ret.LongPolling = isRoutePathForLongPolling(chiRoutePath)
+	}
+	return ret
+}
diff --git a/routers/common/blockexpensive_test.go b/routers/common/blockexpensive_test.go
new file mode 100644
index 0000000000000..db5c0db7ddaa6
--- /dev/null
+++ b/routers/common/blockexpensive_test.go
@@ -0,0 +1,30 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package common
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBlockExpensive(t *testing.T) {
+	cases := []struct {
+		expensive bool
+		routePath string
+	}{
+		{false, "/user/xxx"},
+		{false, "/login/xxx"},
+		{true, "/{username}/{reponame}/archive/xxx"},
+		{true, "/{username}/{reponame}/graph"},
+		{true, "/{username}/{reponame}/src/xxx"},
+		{true, "/{username}/{reponame}/wiki/xxx"},
+		{true, "/{username}/{reponame}/activity/xxx"},
+	}
+	for _, c := range cases {
+		assert.Equal(t, c.expensive, isRoutePathExpensive(c.routePath), "routePath: %s", c.routePath)
+	}
+
+	assert.True(t, isRoutePathForLongPolling("/user/events"))
+}
diff --git a/routers/install/install.go b/routers/install/install.go
index e420d36da5a1b..73ce97a334551 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -156,7 +156,7 @@ func Install(ctx *context.Context) {
 	form.DisableRegistration = setting.Service.DisableRegistration
 	form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
 	form.EnableCaptcha = setting.Service.EnableCaptcha
-	form.RequireSignInView = setting.Service.RequireSignInView
+	form.RequireSignInView = setting.Service.RequireSignInViewStrict
 	form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
 	form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
 	form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
diff --git a/routers/private/serv.go b/routers/private/serv.go
index 4dd7d06fb36e2..12ea01a7e6089 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -286,7 +286,7 @@ func ServCommand(ctx *context.PrivateContext) {
 			repo.IsPrivate ||
 			owner.Visibility.IsPrivate() ||
 			(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
-			setting.Service.RequireSignInView) {
+			setting.Service.RequireSignInViewStrict) {
 		if key.Type == asymkey_model.KeyTypeDeploy {
 			if deployKey.Mode < mode {
 				ctx.JSON(http.StatusUnauthorized, private.Response{
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 58a2bdbab1c34..2c2f59b7be698 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
 	// Only public pull don't need auth.
 	isPublicPull := repoExist && !repo.IsPrivate && isPull
 	var (
-		askAuth = !isPublicPull || setting.Service.RequireSignInView
+		askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
 		environ []string
 	)
 
diff --git a/routers/web/web.go b/routers/web/web.go
index ae5f51d40385c..9f3fb7aa5f1b4 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -285,23 +285,23 @@ func Routes() *web.Router {
 	mid = append(mid, repo.GetActiveStopwatch)
 	mid = append(mid, goGet)
 
-	others := web.NewRouter()
-	others.Use(mid...)
-	registerRoutes(others)
-	routes.Mount("", others)
+	webRoutes := web.NewRouter()
+	webRoutes.Use(mid...)
+	webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive())
+	routes.Mount("", webRoutes)
 	return routes
 }
 
 var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
 
-// registerRoutes register routes
-func registerRoutes(m *web.Router) {
+// registerWebRoutes register routes
+func registerWebRoutes(m *web.Router) {
 	// required to be signed in or signed out
 	reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
 	reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
 	// optional sign in (if signed in, use the user as doer, if not, no doer)
-	optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
-	optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
+	optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
+	optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
 
 	validation.AddBindingRules()
 
diff --git a/services/context/package.go b/services/context/package.go
index 271b61e99c323..33855b1101292 100644
--- a/services/context/package.go
+++ b/services/context/package.go
@@ -93,7 +93,7 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string, any))
 }
 
 func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
-	if setting.Service.RequireSignInView && (doer == nil || doer.IsGhost()) {
+	if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
 		return perm.AccessModeNone, nil
 	}
 
diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go
index e8a8313625dcb..ae4b96702922d 100644
--- a/services/packages/cargo/index.go
+++ b/services/packages/cargo/index.go
@@ -248,7 +248,7 @@ func createOrUpdateConfigFile(ctx context.Context, repo *repo_model.Repository,
 		"Initialize Cargo Config",
 		func(t *files_service.TemporaryUploadRepository) error {
 			var b bytes.Buffer
-			err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInView || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
+			err := json.NewEncoder(&b).Encode(BuildConfig(owner, setting.Service.RequireSignInViewStrict || owner.Visibility != structs.VisibleTypePublic || repo.IsPrivate))
 			if err != nil {
 				return err
 			}
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 29a5e1b473d3b..88dadeb3ee5db 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -148,7 +148,7 @@
 				<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
 				<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
-				<dd>{{svg (Iif .Service.RequireSignInView "octicon-check" "octicon-x")}}</dd>
+				<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>
 				<dd>{{svg (Iif .Service.EnableNotifyMail "octicon-check" "octicon-x")}}</dd>
 				<dt>{{ctx.Locale.Tr "admin.config.enable_captcha"}}</dt>
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index fff121490c9ca..9c1082c48e3ea 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -148,9 +148,9 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
 
 func TestAPIOrgDeny(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
-		setting.Service.RequireSignInView = true
+		setting.Service.RequireSignInViewStrict = true
 		defer func() {
-			setting.Service.RequireSignInView = false
+			setting.Service.RequireSignInViewStrict = false
 		}()
 
 		orgName := "user1_org"
diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go
index 3905ad1b70368..cc9bf11f13033 100644
--- a/tests/integration/api_packages_container_test.go
+++ b/tests/integration/api_packages_container_test.go
@@ -111,7 +111,7 @@ func TestPackageContainer(t *testing.T) {
 				AddTokenAuth(anonymousToken)
 			MakeRequest(t, req, http.StatusOK)
 
-			defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
+			defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
 
 			req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
 			MakeRequest(t, req, http.StatusUnauthorized)
diff --git a/tests/integration/api_packages_generic_test.go b/tests/integration/api_packages_generic_test.go
index baa8dd66c8613..5f410fc47047c 100644
--- a/tests/integration/api_packages_generic_test.go
+++ b/tests/integration/api_packages_generic_test.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -131,11 +132,7 @@ func TestPackageGeneric(t *testing.T) {
 
 		t.Run("RequireSignInView", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-
-			setting.Service.RequireSignInView = true
-			defer func() {
-				setting.Service.RequireSignInView = false
-			}()
+			defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
 
 			req = NewRequest(t, "GET", url+"/dummy.bin")
 			MakeRequest(t, req, http.StatusUnauthorized)
diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go
index 132171cde4e56..6f3f26df75c21 100644
--- a/tests/integration/git_smart_http_test.go
+++ b/tests/integration/git_smart_http_test.go
@@ -74,7 +74,7 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
 }
 
 func testRenamedRepoRedirect(t *testing.T) {
-	defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
+	defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
 
 	// git client requires to get a 301 redirect response before 401 unauthorized response
 	req := NewRequest(t, "GET", "/user2/oldrepo1/info/refs")
diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go
index 25b66bd28b577..c0932f444eed9 100644
--- a/tests/integration/signin_test.go
+++ b/tests/integration/signin_test.go
@@ -15,11 +15,13 @@ import (
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/translation"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers"
 	"code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/markbates/goth"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func testLoginFailed(t *testing.T, username, password, message string) {
@@ -158,3 +160,32 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
 		NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true)
 	})
 }
+
+func TestRequireSignInView(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	t.Run("NoRequireSignInView", func(t *testing.T) {
+		require.False(t, setting.Service.RequireSignInViewStrict)
+		require.False(t, setting.Service.BlockAnonymousAccessExpensive)
+		req := NewRequest(t, "GET", "/user2/repo1/src/branch/master")
+		MakeRequest(t, req, http.StatusOK)
+	})
+	t.Run("RequireSignInView", func(t *testing.T) {
+		defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
+		defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
+		req := NewRequest(t, "GET", "/user2/repo1/src/branch/master")
+		resp := MakeRequest(t, req, http.StatusSeeOther)
+		assert.Equal(t, "/user/login", resp.Header().Get("Location"))
+	})
+	t.Run("BlockAnonymousAccessExpensive", func(t *testing.T) {
+		defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, false)()
+		defer test.MockVariableValue(&setting.Service.BlockAnonymousAccessExpensive, true)()
+		defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
+
+		req := NewRequest(t, "GET", "/user2/repo1")
+		MakeRequest(t, req, http.StatusOK)
+
+		req = NewRequest(t, "GET", "/user2/repo1/src/branch/master")
+		resp := MakeRequest(t, req, http.StatusSeeOther)
+		assert.Equal(t, "/user/login", resp.Header().Get("Location"))
+	})
+}

From f8f24d83cfee9b4c8d0ee60183fee0d6a91da67f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Sun, 30 Mar 2025 03:30:55 -0700
Subject: [PATCH 171/206] Hide activity contributors, recent commits and code
 frequrency left tabs if there is no code permission (#34053) (#34065)

Backport #34053

When a team have no code unit permission of a repository, the member of
the team should not view activity contributors, recent commits and code
frequrency.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 routers/web/repo/code_frequency.go |  2 +-
 routers/web/repo/recent_commits.go | 15 ---------------
 routers/web/web.go                 | 29 ++++++++++++++++-------------
 templates/repo/navbar.tmpl         | 22 +++++++++++++---------
 4 files changed, 30 insertions(+), 38 deletions(-)

diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index c76f492da071e..e62965ec70231 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
 			ctx.Status(http.StatusAccepted)
 			return
 		}
-		ctx.ServerError("GetCodeFrequencyData", err)
+		ctx.ServerError("GetContributorStats", err)
 	} else {
 		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
 	}
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index c158fb30b641f..65a47333b68c0 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -4,12 +4,10 @@
 package repo
 
 import (
-	"errors"
 	"net/http"
 
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/services/context"
-	contributors_service "code.gitea.io/gitea/services/repository"
 )
 
 const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
 
 	ctx.HTML(http.StatusOK, tplRecentCommits)
 }
-
-// RecentCommitsData returns JSON of recent commits data
-func RecentCommitsData(ctx *context.Context) {
-	if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
-		if errors.Is(err, contributors_service.ErrAwaitGeneration) {
-			ctx.Status(http.StatusAccepted)
-			return
-		}
-		ctx.ServerError("RecentCommitsData", err)
-	} else {
-		ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
-	}
-}
diff --git a/routers/web/web.go b/routers/web/web.go
index 9f3fb7aa5f1b4..3ec349243329b 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1454,20 +1454,23 @@ func registerWebRoutes(m *web.Router) {
 	m.Group("/{username}/{reponame}/activity", func() {
 		m.Get("", repo.Activity)
 		m.Get("/{period}", repo.Activity)
-		m.Group("/contributors", func() {
-			m.Get("", repo.Contributors)
-			m.Get("/data", repo.ContributorsData)
-		})
-		m.Group("/code-frequency", func() {
-			m.Get("", repo.CodeFrequency)
-			m.Get("/data", repo.CodeFrequencyData)
-		})
-		m.Group("/recent-commits", func() {
-			m.Get("", repo.RecentCommits)
-			m.Get("/data", repo.RecentCommitsData)
-		})
+
+		m.Group("", func() {
+			m.Group("/contributors", func() {
+				m.Get("", repo.Contributors)
+				m.Get("/data", repo.ContributorsData)
+			})
+			m.Group("/code-frequency", func() {
+				m.Get("", repo.CodeFrequency)
+				m.Get("/data", repo.CodeFrequencyData)
+			})
+			m.Group("/recent-commits", func() {
+				m.Get("", repo.RecentCommits)
+				m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
+			})
+		}, reqRepoCodeReader)
 	},
-		optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
+		optSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases),
 		context.RepoRef(), repo.MustBeNotEmpty,
 	)
 	// end "/{username}/{reponame}/activity"
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
index b2471dc17e7c0..d6e9b1b8d7868 100644
--- a/templates/repo/navbar.tmpl
+++ b/templates/repo/navbar.tmpl
@@ -1,14 +1,18 @@
+{{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
+
 <div class="ui fluid vertical menu">
 	<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
 		{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
 	</a>
-	<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
-		{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
-	</a>
-	<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
-		{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
-	</a>
-	<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
-		{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
-	</a>
+	{{if $canReadCode}}
+		<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
+			{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
+		</a>
+		<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
+			{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
+		</a>
+		<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
+			{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
+		</a>
+	{{end}}
 </div>

From 25e409e025a125a5a8012f9f015089e93927d08c Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 2 Apr 2025 09:37:17 +0800
Subject: [PATCH 172/206] Return default avatar url when user id is zero rather
 than updating database (#34094) (#34095)

Backport #34094 by @lunny

When visit commit list, it would update the user avatar even if id = 0,
which was unnecessary operations. This PR returned default avatar for
the git only user avatar rendering who's user id is zero.

```log
database duration=0.0005s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
database duration=0.0007s db.sql="UPDATE `user` SET `avatar` = ?, `updated_unix` = ? WHERE `id`=?"
...
```

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 models/user/avatar.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/models/user/avatar.go b/models/user/avatar.go
index 2a41b9912928b..3d9fc4452f8ab 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -61,7 +61,9 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
 
 // AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
 func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
-	if u.IsGhost() || u.IsGiteaActions() {
+	// ghost user was deleted, Gitea actions is a bot user, 0 means the user should be a virtual user
+	// which comes from git configure information
+	if u.IsGhost() || u.IsGiteaActions() || u.ID <= 0 {
 		return avatars.DefaultAvatarLink()
 	}
 

From 8f75f61b64bdf70824a54021be43c579239bdcc5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 2 Apr 2025 18:16:41 +0800
Subject: [PATCH 173/206] Do not show 500 error when default branch doesn't
 exist (#34096) (#34097)

Backport #34096
---
 models/git/branch.go                    | 12 ++++++++++++
 options/locale/locale_en-US.ini         |  1 +
 routers/web/repo/actions/actions.go     |  7 ++++++-
 routers/web/repo/activity.go            | 19 +++++++++++++++++--
 services/context/context_response.go    |  2 +-
 tests/integration/repo_activity_test.go | 12 ++++++++++++
 6 files changed, 49 insertions(+), 4 deletions(-)

diff --git a/models/git/branch.go b/models/git/branch.go
index d1caa35947b40..9ac6c45578f7c 100644
--- a/models/git/branch.go
+++ b/models/git/branch.go
@@ -173,6 +173,18 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
 	return &branch, nil
 }
 
+// IsBranchExist returns true if the branch exists in the repository.
+func IsBranchExist(ctx context.Context, repoID int64, branchName string) (bool, error) {
+	var branch Branch
+	has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("name=?", branchName).Get(&branch)
+	if err != nil {
+		return false, err
+	} else if !has {
+		return false, nil
+	}
+	return !branch.IsDeleted, nil
+}
+
 func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) {
 	branches := make([]*Branch, 0, len(branchNames))
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 9d71ccb6d5314..e28840277a3d6 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2699,6 +2699,7 @@ branch.restore_success = Branch "%s" has been restored.
 branch.restore_failed = Failed to restore branch "%s".
 branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted.
 branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted.
+branch.default_branch_not_exist = Default branch "%s" does not exist.
 branch.restore = Restore Branch "%s"
 branch.download = Download Branch "%s"
 branch.rename = Rename Branch "%s"
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 1de1835936554..46b7461883f18 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -6,6 +6,7 @@ package actions
 import (
 	"bytes"
 	stdCtx "context"
+	"errors"
 	"fmt"
 	"net/http"
 	"slices"
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
 		return
 	} else if !empty {
 		commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
-		if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+			ctx.NotFound("GetBranchCommit", err)
+			return
+		} else if err != nil {
 			ctx.ServerError("GetBranchCommit", err)
 			return
 		}
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 65dd9e392f338..65fd379406aba 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
+	"code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/services/context"
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
 	ctx.Data["DateUntil"] = timeUntil
 	ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
 
+	canReadCode := ctx.Repo.CanRead(unit.TypeCode)
+	if canReadCode {
+		// GetActivityStats needs to read the default branch to get some information
+		branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
+		if !branchExist {
+			ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+			ctx.NotFound("", nil)
+			return
+		}
+	}
+
 	var err error
-	if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
+	// TODO: refactor these arguments to a struct
+	ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
 		ctx.Repo.CanRead(unit.TypeReleases),
 		ctx.Repo.CanRead(unit.TypeIssues),
 		ctx.Repo.CanRead(unit.TypePullRequests),
-		ctx.Repo.CanRead(unit.TypeCode)); err != nil {
+		canReadCode,
+	)
+	if err != nil {
 		ctx.ServerError("GetActivityStats", err)
 		return
 	}
diff --git a/services/context/context_response.go b/services/context/context_response.go
index c43a649b49e12..14a31dad84540 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -151,7 +151,7 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
 
 	ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
 	ctx.Data["Title"] = "Page Not Found"
-	ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
+	ctx.HTML(http.StatusNotFound, "status/404")
 }
 
 // ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
diff --git a/tests/integration/repo_activity_test.go b/tests/integration/repo_activity_test.go
index b04560379d2cd..e520a0ed56e16 100644
--- a/tests/integration/repo_activity_test.go
+++ b/tests/integration/repo_activity_test.go
@@ -4,12 +4,15 @@
 package integration
 
 import (
+	"context"
 	"net/http"
 	"net/url"
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
 	"code.gitea.io/gitea/modules/test"
 
 	"github.com/stretchr/testify/assert"
@@ -61,5 +64,14 @@ func TestRepoActivity(t *testing.T) {
 		// Should be 3 new issues
 		list = htmlDoc.doc.Find("#new-issues").Next().Find("p.desc")
 		assert.Len(t, list.Nodes, 3)
+
+		// Non-existing default branch
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
+		repo1.DefaultBranch = "no-such-branch"
+		_, _ = db.GetEngine(context.Background()).Cols("default_branch").Update(repo1)
+		req = NewRequest(t, "GET", "/user2/repo1/activity")
+		req.Header.Add("Accept", "text/html")
+		resp = session.MakeRequest(t, req, http.StatusNotFound)
+		assert.Contains(t, resp.Body.String(), `Default branch "no-such-branch" does not exist.`)
 	})
 }

From a40e15a116366f505e4871d6288bd05454c3a342 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 3 Apr 2025 01:05:28 +0800
Subject: [PATCH 174/206] Add new CLI flags to set name and scopes when
 creating a user with access token (#34080) (#34103)

Backport #34080

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
---
 cmd/admin_user_create.go                | 45 +++++++++---
 cmd/admin_user_create_test.go           | 98 ++++++++++++++++++++-----
 cmd/admin_user_generate_access_token.go |  9 ++-
 models/auth/access_token_scope.go       |  4 +
 4 files changed, 125 insertions(+), 31 deletions(-)

diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index bf8cbc7c4c0b0..37853cd31210d 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -61,6 +62,16 @@ var microcmdUserCreate = &cli.Command{
 			Name:  "access-token",
 			Usage: "Generate access token for the user",
 		},
+		&cli.StringFlag{
+			Name:  "access-token-name",
+			Usage: `Name of the generated access token`,
+			Value: "gitea-admin",
+		},
+		&cli.StringFlag{
+			Name:  "access-token-scopes",
+			Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
+			Value: "all",
+		},
 		&cli.BoolFlag{
 			Name:  "restricted",
 			Usage: "Make a restricted user account",
@@ -162,23 +173,39 @@ func runCreateUser(c *cli.Context) error {
 		IsRestricted: restricted,
 	}
 
-	if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
-		return fmt.Errorf("CreateUser: %w", err)
+	var accessTokenName string
+	var accessTokenScope auth_model.AccessTokenScope
+	if c.IsSet("access-token") {
+		accessTokenName = strings.TrimSpace(c.String("access-token-name"))
+		if accessTokenName == "" {
+			return errors.New("access-token-name cannot be empty")
+		}
+		var err error
+		accessTokenScope, err = auth_model.AccessTokenScope(c.String("access-token-scopes")).Normalize()
+		if err != nil {
+			return fmt.Errorf("invalid access token scope provided: %w", err)
+		}
+		if !accessTokenScope.HasPermissionScope() {
+			return errors.New("access token does not have any permission")
+		}
+	} else if c.IsSet("access-token-name") || c.IsSet("access-token-scopes") {
+		return errors.New("access-token-name and access-token-scopes flags are only valid when access-token flag is set")
 	}
 
-	if c.Bool("access-token") {
-		t := &auth_model.AccessToken{
-			Name: "gitea-admin",
-			UID:  u.ID,
-		}
+	// arguments should be prepared before creating the user & access token, in case there is anything wrong
 
+	// create the user
+	if err := user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
+		return fmt.Errorf("CreateUser: %w", err)
+	}
+	// create the access token
+	if accessTokenScope != "" {
+		t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope}
 		if err := auth_model.NewAccessToken(ctx, t); err != nil {
 			return err
 		}
-
 		fmt.Printf("Access token was successfully created... %s\n", t.Token)
 	}
-
 	fmt.Printf("New user '%s' has been successfully created!\n", username)
 	return nil
 }
diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go
index 83754e97b10a4..9f109b888e235 100644
--- a/cmd/admin_user_create_test.go
+++ b/cmd/admin_user_create_test.go
@@ -8,37 +8,97 @@ import (
 	"strings"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestAdminUserCreate(t *testing.T) {
 	app := NewMainApp(AppVersion{})
 
 	reset := func() {
-		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
-		assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+		require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+		require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+		require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
 	}
+	t.Run("MustChangePassword", func(t *testing.T) {
+		type check struct{ IsAdmin, MustChangePassword bool }
+		createCheck := func(name, args string) check {
+			assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
+			u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
+			return check{u.IsAdmin, u.MustChangePassword}
+		}
+		reset()
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
 
-	type createCheck struct{ IsAdmin, MustChangePassword bool }
-	createUser := func(name, args string) createCheck {
-		assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
-		u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
-		return createCheck{u.IsAdmin, u.MustChangePassword}
+		reset()
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
+
+		reset()
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
+		assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
+		assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
+	})
+
+	createUser := func(name, args string) error {
+		return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
 	}
-	reset()
-	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
-
-	reset()
-	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
-
-	reset()
-	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
-	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
-	assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
-	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
-	assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
+
+	t.Run("AccessToken", func(t *testing.T) {
+		// no generated access token
+		reset()
+		assert.NoError(t, createUser("u", "--random-password"))
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
+
+		// using "--access-token" only means "all" access
+		reset()
+		assert.NoError(t, createUser("u", "--random-password --access-token"))
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
+		accessToken := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "gitea-admin"})
+		hasScopes, err := accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
+		assert.NoError(t, err)
+		assert.True(t, hasScopes)
+
+		// using "--access-token" with name & scopes
+		reset()
+		assert.NoError(t, createUser("u", "--random-password --access-token --access-token-name new-token-name --access-token-scopes read:issue,read:user"))
+		assert.Equal(t, 1, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 1, unittest.GetCount(t, &auth_model.AccessToken{}))
+		accessToken = unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{Name: "new-token-name"})
+		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadUser)
+		assert.NoError(t, err)
+		assert.True(t, hasScopes)
+		hasScopes, err = accessToken.Scope.HasScope(auth_model.AccessTokenScopeWriteAdmin, auth_model.AccessTokenScopeWriteRepository)
+		assert.NoError(t, err)
+		assert.False(t, hasScopes)
+
+		// using "--access-token-name" without "--access-token"
+		reset()
+		err = createUser("u", "--random-password --access-token-name new-token-name")
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
+		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
+
+		// using "--access-token-scopes" without "--access-token"
+		reset()
+		err = createUser("u", "--random-password --access-token-scopes read:issue")
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
+		assert.ErrorContains(t, err, "access-token-name and access-token-scopes flags are only valid when access-token flag is set")
+
+		// empty permission
+		reset()
+		err = createUser("u", "--random-password --access-token --access-token-scopes public-only")
+		assert.Equal(t, 0, unittest.GetCount(t, &user_model.User{}))
+		assert.Equal(t, 0, unittest.GetCount(t, &auth_model.AccessToken{}))
+		assert.ErrorContains(t, err, "access token does not have any permission")
+	})
 }
diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go
index 6c2c10494ee5f..f6db7a74bd1ec 100644
--- a/cmd/admin_user_generate_access_token.go
+++ b/cmd/admin_user_generate_access_token.go
@@ -34,8 +34,8 @@ var microcmdUserGenerateAccessToken = &cli.Command{
 		},
 		&cli.StringFlag{
 			Name:  "scopes",
-			Value: "",
-			Usage: "Comma separated list of scopes to apply to access token",
+			Value: "all",
+			Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
 		},
 	},
 	Action: runGenerateAccessToken,
@@ -43,7 +43,7 @@ var microcmdUserGenerateAccessToken = &cli.Command{
 
 func runGenerateAccessToken(c *cli.Context) error {
 	if !c.IsSet("username") {
-		return errors.New("You must provide a username to generate a token for")
+		return errors.New("you must provide a username to generate a token for")
 	}
 
 	ctx, cancel := installSignals()
@@ -77,6 +77,9 @@ func runGenerateAccessToken(c *cli.Context) error {
 	if err != nil {
 		return fmt.Errorf("invalid access token scope provided: %w", err)
 	}
+	if !accessTokenScope.HasPermissionScope() {
+		return errors.New("access token does not have any permission")
+	}
 	t.Scope = accessTokenScope
 
 	// create the token
diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go
index 897ff3fc9ee3d..aa1cc5b9deba4 100644
--- a/models/auth/access_token_scope.go
+++ b/models/auth/access_token_scope.go
@@ -283,6 +283,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
 	return bitmap.toScope(), nil
 }
 
+func (s AccessTokenScope) HasPermissionScope() bool {
+	return s != "" && s != AccessTokenScopePublicOnly
+}
+
 // PublicOnly checks if this token scope is limited to public resources
 func (s AccessTokenScope) PublicOnly() (bool, error) {
 	bitmap, err := s.parse()

From 3063e37802e1a3d892b861e91d0665ce85000a04 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 3 Apr 2025 15:26:43 +0800
Subject: [PATCH 175/206] Fix markdown frontmatter rendering (#34102) (#34107)

Backport #34102
Fix #34101
---
 modules/markup/markdown/ast.go               | 53 +++++++--------
 modules/markup/markdown/convertyaml.go       | 43 +++++++++----
 modules/markup/markdown/goldmark.go          | 34 ++--------
 modules/markup/markdown/markdown.go          |  6 +-
 modules/markup/markdown/markdown_test.go     | 68 ++++++++++++++++++--
 modules/markup/markdown/renderconfig.go      | 11 +---
 modules/markup/markdown/renderconfig_test.go | 30 ++-------
 web_src/css/markup/content.css               | 12 ++++
 8 files changed, 140 insertions(+), 117 deletions(-)

diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go
index ca165b1ba045f..f29f883734915 100644
--- a/modules/markup/markdown/ast.go
+++ b/modules/markup/markdown/ast.go
@@ -4,6 +4,7 @@
 package markdown
 
 import (
+	"html/template"
 	"strconv"
 
 	"github.com/yuin/goldmark/ast"
@@ -29,9 +30,7 @@ func (n *Details) Kind() ast.NodeKind {
 
 // NewDetails returns a new Paragraph node.
 func NewDetails() *Details {
-	return &Details{
-		BaseBlock: ast.BaseBlock{},
-	}
+	return &Details{}
 }
 
 // Summary is a block that contains the summary of details block
@@ -54,9 +53,7 @@ func (n *Summary) Kind() ast.NodeKind {
 
 // NewSummary returns a new Summary node.
 func NewSummary() *Summary {
-	return &Summary{
-		BaseBlock: ast.BaseBlock{},
-	}
+	return &Summary{}
 }
 
 // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
@@ -95,29 +92,6 @@ type Icon struct {
 	Name []byte
 }
 
-// Dump implements Node.Dump .
-func (n *Icon) Dump(source []byte, level int) {
-	m := map[string]string{}
-	m["Name"] = string(n.Name)
-	ast.DumpHelper(n, source, level, m, nil)
-}
-
-// KindIcon is the NodeKind for Icon
-var KindIcon = ast.NewNodeKind("Icon")
-
-// Kind implements Node.Kind.
-func (n *Icon) Kind() ast.NodeKind {
-	return KindIcon
-}
-
-// NewIcon returns a new Paragraph node.
-func NewIcon(name string) *Icon {
-	return &Icon{
-		BaseInline: ast.BaseInline{},
-		Name:       []byte(name),
-	}
-}
-
 // ColorPreview is an inline for a color preview
 type ColorPreview struct {
 	ast.BaseInline
@@ -175,3 +149,24 @@ func NewAttention(attentionType string) *Attention {
 		AttentionType: attentionType,
 	}
 }
+
+var KindRawHTML = ast.NewNodeKind("RawHTML")
+
+type RawHTML struct {
+	ast.BaseBlock
+	rawHTML template.HTML
+}
+
+func (n *RawHTML) Dump(source []byte, level int) {
+	m := map[string]string{}
+	m["RawHTML"] = string(n.rawHTML)
+	ast.DumpHelper(n, source, level, m, nil)
+}
+
+func (n *RawHTML) Kind() ast.NodeKind {
+	return KindRawHTML
+}
+
+func NewRawHTML(rawHTML template.HTML) *RawHTML {
+	return &RawHTML{rawHTML: rawHTML}
+}
diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go
index 1675b68be2a9a..04664a9c1db4f 100644
--- a/modules/markup/markdown/convertyaml.go
+++ b/modules/markup/markdown/convertyaml.go
@@ -4,23 +4,22 @@
 package markdown
 
 import (
+	"strings"
+
+	"code.gitea.io/gitea/modules/htmlutil"
+	"code.gitea.io/gitea/modules/svg"
+
 	"github.com/yuin/goldmark/ast"
 	east "github.com/yuin/goldmark/extension/ast"
 	"gopkg.in/yaml.v3"
 )
 
 func nodeToTable(meta *yaml.Node) ast.Node {
-	for {
-		if meta == nil {
-			return nil
-		}
-		switch meta.Kind {
-		case yaml.DocumentNode:
-			meta = meta.Content[0]
-			continue
-		default:
-		}
-		break
+	for meta != nil && meta.Kind == yaml.DocumentNode {
+		meta = meta.Content[0]
+	}
+	if meta == nil {
+		return nil
 	}
 	switch meta.Kind {
 	case yaml.MappingNode:
@@ -72,12 +71,28 @@ func sequenceNodeToTable(meta *yaml.Node) ast.Node {
 	return table
 }
 
-func nodeToDetails(meta *yaml.Node, icon string) ast.Node {
+func nodeToDetails(g *ASTTransformer, meta *yaml.Node) ast.Node {
+	for meta != nil && meta.Kind == yaml.DocumentNode {
+		meta = meta.Content[0]
+	}
+	if meta == nil {
+		return nil
+	}
+	if meta.Kind != yaml.MappingNode {
+		return nil
+	}
+	var keys []string
+	for i := 0; i < len(meta.Content); i += 2 {
+		if meta.Content[i].Kind == yaml.ScalarNode {
+			keys = append(keys, meta.Content[i].Value)
+		}
+	}
 	details := NewDetails()
+	details.SetAttributeString(g.renderInternal.SafeAttr("class"), g.renderInternal.SafeValue("frontmatter-content"))
 	summary := NewSummary()
-	summary.AppendChild(summary, NewIcon(icon))
+	summaryInnerHTML := htmlutil.HTMLFormat("%s %s", svg.RenderHTML("octicon-table", 12), strings.Join(keys, ", "))
+	summary.AppendChild(summary, NewRawHTML(summaryInnerHTML))
 	details.AppendChild(details, summary)
 	details.AppendChild(details, nodeToTable(meta))
-
 	return details
 }
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 69c2a96ff1fd7..bc37fbfe68500 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -5,9 +5,6 @@ package markdown
 
 import (
 	"fmt"
-	"regexp"
-	"strings"
-	"sync"
 
 	"code.gitea.io/gitea/modules/container"
 	"code.gitea.io/gitea/modules/markup"
@@ -51,7 +48,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 
 	tocList := make([]Header, 0, 20)
 	if rc.yamlNode != nil {
-		metaNode := rc.toMetaNode()
+		metaNode := rc.toMetaNode(g)
 		if metaNode != nil {
 			node.InsertBefore(node, firstChild, metaNode)
 		}
@@ -111,11 +108,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 	}
 }
 
-// it is copied from old code, which is quite doubtful whether it is correct
-var reValidIconName = sync.OnceValue(func() *regexp.Regexp {
-	return regexp.MustCompile(`^[-\w]+$`) // old: regexp.MustCompile("^[a-z ]+$")
-})
-
 // NewHTMLRenderer creates a HTMLRenderer to render in the gitea form.
 func NewHTMLRenderer(renderInternal *internal.RenderInternal, opts ...html.Option) renderer.NodeRenderer {
 	r := &HTMLRenderer{
@@ -140,11 +132,11 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	reg.Register(ast.KindDocument, r.renderDocument)
 	reg.Register(KindDetails, r.renderDetails)
 	reg.Register(KindSummary, r.renderSummary)
-	reg.Register(KindIcon, r.renderIcon)
 	reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
 	reg.Register(KindAttention, r.renderAttention)
 	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
 	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
+	reg.Register(KindRawHTML, r.renderRawHTML)
 }
 
 func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
@@ -206,30 +198,14 @@ func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.N
 	return ast.WalkContinue, nil
 }
 
-func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+func (r *HTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
 	if !entering {
 		return ast.WalkContinue, nil
 	}
-
-	n := node.(*Icon)
-
-	name := strings.TrimSpace(strings.ToLower(string(n.Name)))
-
-	if len(name) == 0 {
-		// skip this
-		return ast.WalkContinue, nil
-	}
-
-	if !reValidIconName().MatchString(name) {
-		// skip this
-		return ast.WalkContinue, nil
-	}
-
-	// FIXME: the "icon xxx" is from Fomantic UI, it's really questionable whether it still works correctly
-	err := r.renderInternal.FormatWithSafeAttrs(w, `<i class="icon %s"></i>`, name)
+	n := node.(*RawHTML)
+	_, err := w.WriteString(string(r.renderInternal.ProtectSafeAttrs(n.rawHTML)))
 	if err != nil {
 		return ast.WalkStop, err
 	}
-
 	return ast.WalkContinue, nil
 }
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index ace31eb540778..19fd933866232 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -184,11 +184,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 	// Preserve original length.
 	bufWithMetadataLength := len(buf)
 
-	rc := &RenderConfig{
-		Meta: markup.RenderMetaAsDetails,
-		Icon: "table",
-		Lang: "",
-	}
+	rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
 	buf, _ = ExtractMetadataBytes(buf, rc)
 
 	metaLength := bufWithMetadataLength - len(buf)
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 7a09be8665827..268a543835db6 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -383,18 +383,74 @@ func TestColorPreview(t *testing.T) {
 	}
 }
 
-func TestTaskList(t *testing.T) {
+func TestMarkdownFrontmatter(t *testing.T) {
 	testcases := []struct {
-		testcase string
+		name     string
+		input    string
 		expected string
 	}{
+		{
+			"MapInFrontmatter",
+			`---
+key1: val1
+key2: val2
+---
+test
+`,
+			`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> key1, key2</summary><table>
+<thead>
+<tr>
+<th>key1</th>
+<th>key2</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>val1</td>
+<td>val2</td>
+</tr>
+</tbody>
+</table>
+</details><p>test</p>
+`,
+		},
+
+		{
+			"ListInFrontmatter",
+			`---
+- item1
+- item2
+---
+test
+`,
+			`- item1
+- item2
+
+<p>test</p>
+`,
+		},
+
+		{
+			"StringInFrontmatter",
+			`---
+anything
+---
+test
+`,
+			`anything
+
+<p>test</p>
+`,
+		},
+
 		{
 			// data-source-position should take into account YAML frontmatter.
+			"ListAfterFrontmatter",
 			`---
 foo: bar
 ---
 - [ ] task 1`,
-			`<details><summary><i class="icon table"></i></summary><table>
+			`<details class="frontmatter-content"><summary><span>octicon-table(12/)</span> foo</summary><table>
 <thead>
 <tr>
 <th>foo</th>
@@ -414,9 +470,9 @@ foo: bar
 	}
 
 	for _, test := range testcases {
-		res, err := markdown.RenderString(markup.NewTestRenderContext(), test.testcase)
-		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
-		assert.Equal(t, template.HTML(test.expected), res, "Unexpected result in testcase %q", test.testcase)
+		res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input)
+		assert.NoError(t, err, "Unexpected error in testcase: %q", test.name)
+		assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name)
 	}
 }
 
diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go
index f4c48d1b3d8f9..d8b1b10ce632a 100644
--- a/modules/markup/markdown/renderconfig.go
+++ b/modules/markup/markdown/renderconfig.go
@@ -16,7 +16,6 @@ import (
 // RenderConfig represents rendering configuration for this file
 type RenderConfig struct {
 	Meta     markup.RenderMetaMode
-	Icon     string
 	TOC      string // "false": hide,  "side"/empty: in sidebar,  "main"/"true": in main view
 	Lang     string
 	yamlNode *yaml.Node
@@ -74,7 +73,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 
 	type yamlRenderConfig struct {
 		Meta *string `yaml:"meta"`
-		Icon *string `yaml:"details_icon"`
+		Icon *string `yaml:"details_icon"` // deprecated, because there is no font icon, so no custom icon
 		TOC  *string `yaml:"include_toc"`
 		Lang *string `yaml:"lang"`
 	}
@@ -96,10 +95,6 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 		rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta)
 	}
 
-	if cfg.Gitea.Icon != nil {
-		rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon))
-	}
-
 	if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" {
 		rc.Lang = *cfg.Gitea.Lang
 	}
@@ -111,7 +106,7 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error {
 	return nil
 }
 
-func (rc *RenderConfig) toMetaNode() ast.Node {
+func (rc *RenderConfig) toMetaNode(g *ASTTransformer) ast.Node {
 	if rc.yamlNode == nil {
 		return nil
 	}
@@ -119,7 +114,7 @@ func (rc *RenderConfig) toMetaNode() ast.Node {
 	case markup.RenderMetaAsTable:
 		return nodeToTable(rc.yamlNode)
 	case markup.RenderMetaAsDetails:
-		return nodeToDetails(rc.yamlNode, rc.Icon)
+		return nodeToDetails(g, rc.yamlNode)
 	default:
 		return nil
 	}
diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go
index c53acdc77a788..8513bf75b652e 100644
--- a/modules/markup/markdown/renderconfig_test.go
+++ b/modules/markup/markdown/renderconfig_test.go
@@ -7,6 +7,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"gopkg.in/yaml.v3"
 )
 
@@ -19,42 +20,36 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 		{
 			"empty", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "",
 			}, "",
 		},
 		{
 			"lang", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "test",
 			}, "lang: test",
 		},
 		{
 			"metatable", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "",
 			}, "gitea: table",
 		},
 		{
 			"metanone", &RenderConfig{
 				Meta: "none",
-				Icon: "table",
 				Lang: "",
 			}, "gitea: none",
 		},
 		{
 			"metadetails", &RenderConfig{
 				Meta: "details",
-				Icon: "table",
 				Lang: "",
 			}, "gitea: details",
 		},
 		{
 			"metawrong", &RenderConfig{
 				Meta: "details",
-				Icon: "table",
 				Lang: "",
 			}, "gitea: wrong",
 		},
@@ -62,7 +57,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 			"toc", &RenderConfig{
 				TOC:  "true",
 				Meta: "table",
-				Icon: "table",
 				Lang: "",
 			}, "include_toc: true",
 		},
@@ -70,14 +64,12 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 			"tocfalse", &RenderConfig{
 				TOC:  "false",
 				Meta: "table",
-				Icon: "table",
 				Lang: "",
 			}, "include_toc: false",
 		},
 		{
 			"toclang", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				TOC:  "true",
 				Lang: "testlang",
 			}, `
@@ -88,7 +80,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 		{
 			"complexlang", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "testlang",
 			}, `
 				gitea:
@@ -98,7 +89,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 		{
 			"complexlang2", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "testlang",
 			}, `
 	lang: notright
@@ -109,7 +99,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 		{
 			"complexlang", &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "testlang",
 			}, `
 	gitea:
@@ -121,7 +110,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 				Lang: "two",
 				Meta: "table",
 				TOC:  "true",
-				Icon: "smiley",
 			}, `
 	lang: one
 	include_toc: true
@@ -137,7 +125,6 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			got := &RenderConfig{
 				Meta: "table",
-				Icon: "table",
 				Lang: "",
 			}
 			if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", "    ")), got); err != nil {
@@ -145,18 +132,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) {
 				return
 			}
 
-			if got.Meta != tt.expected.Meta {
-				t.Errorf("Meta Expected %s Got %s", tt.expected.Meta, got.Meta)
-			}
-			if got.Icon != tt.expected.Icon {
-				t.Errorf("Icon Expected %s Got %s", tt.expected.Icon, got.Icon)
-			}
-			if got.Lang != tt.expected.Lang {
-				t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang)
-			}
-			if got.TOC != tt.expected.TOC {
-				t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC)
-			}
+			assert.Equal(t, tt.expected.Meta, got.Meta)
+			assert.Equal(t, tt.expected.Lang, got.Lang)
+			assert.Equal(t, tt.expected.TOC, got.TOC)
 		})
 	}
 }
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index 4c6940e027784..0ee1d0b213242 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -516,6 +516,18 @@
   padding-left: 2em;
 }
 
+.markup details.frontmatter-content summary {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  margin-bottom: 0.25em;
+}
+
+.markup details.frontmatter-content svg {
+  vertical-align: middle;
+  margin: 0 0.25em;
+}
+
 .file-revisions-btn {
   display: block;
   float: left;

From 4ee4c06b07a70efacd7366baee677522b1cb5b4f Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 4 Apr 2025 04:38:24 +0800
Subject: [PATCH 176/206] also check default ssh-cert location for host
 (#34099) (#34100) (#34116)

Backport #34100 by @ManInDark

Fixes #34099.

Resolved by checking the `key-cert.pub` location alongside the
previously configured location. In case a certificate is already found,
this won't change anything, but if there is one in `key-cert.pub` but
not in `key_cert`, it'll use that one now.

Co-authored-by: ManInDark <61268856+ManInDark@users.noreply.github.com>
---
 docker/root/etc/s6/openssh/setup | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup
index dbb3bafd35245..6fbc599cc5f2d 100755
--- a/docker/root/etc/s6/openssh/setup
+++ b/docker/root/etc/s6/openssh/setup
@@ -31,6 +31,18 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
   SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
 fi
 
+if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then
+  SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"}
+fi
+
+if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then
+  SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"}
+fi
+
+if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then
+  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"}
+fi
+
 if [ -d /etc/ssh ]; then
     SSH_PORT=${SSH_PORT:-"22"} \
     SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \

From 67dc1ff926258decd7cf169d551365374f73622e Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 4 Apr 2025 05:18:46 +0800
Subject: [PATCH 177/206] Fix invalid version in RPM package path (#34112)
 (#34115)

Backport #34112 by @KN4CK3R

Fixes #34017

Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
---
 services/packages/rpm/repository.go | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index a7d196c15c5fa..5027021c52047 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -408,7 +408,6 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
 				files = append(files, f)
 			}
 		}
-		packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
 		packages = append(packages, &Package{
 			Type:         "rpm",
 			Name:         pd.Package.Name,
@@ -437,7 +436,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
 				Archive:   pd.FileMetadata.ArchiveSize,
 			},
 			Location: Location{
-				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
+				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
 			},
 			Format: Format{
 				License:   pd.VersionMetadata.License,

From 0e6489317ebb8c2a01229f785e12ed440141eaa5 Mon Sep 17 00:00:00 2001
From: Zettat123 <zettat123@gmail.com>
Date: Thu, 3 Apr 2025 23:10:54 -0600
Subject: [PATCH 178/206] Get changed files based on merge base when checking
 `pull_request` actions trigger (#34106) (#34120)

Backport #34106

Fix #33941
---
 modules/actions/workflows.go              |   4 +-
 tests/integration/actions_trigger_test.go | 110 ++++++++++++++++++++++
 2 files changed, 112 insertions(+), 2 deletions(-)

diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go
index 0d2b0dd9194d9..a538b6e290bec 100644
--- a/modules/actions/workflows.go
+++ b/modules/actions/workflows.go
@@ -463,7 +463,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 				matchTimes++
 			}
 		case "paths":
-			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
+			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
 			if err != nil {
 				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
 			} else {
@@ -476,7 +476,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
 				}
 			}
 		case "paths-ignore":
-			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref)
+			filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
 			if err != nil {
 				log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
 			} else {
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index 4718aa7e7352c..a026e1f239811 100644
--- a/tests/integration/actions_trigger_test.go
+++ b/tests/integration/actions_trigger_test.go
@@ -4,7 +4,9 @@
 package integration
 
 import (
+	"encoding/base64"
 	"fmt"
+	"net/http"
 	"net/url"
 	"strings"
 	"testing"
@@ -24,7 +26,9 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
+	"code.gitea.io/gitea/modules/util"
 	pull_service "code.gitea.io/gitea/services/pull"
 	release_service "code.gitea.io/gitea/services/release"
 	repo_service "code.gitea.io/gitea/services/repository"
@@ -451,3 +455,109 @@ func TestCreateDeleteRefEvent(t *testing.T) {
 		assert.NotNil(t, run)
 	})
 }
+
+func TestClosePullRequestWithPath(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		// user2 is the owner of the base repo
+		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+		user2Token := getTokenForLoggedInUser(t, loginUser(t, user2.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+		// user4 is the owner of the fork repo
+		user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+		user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+
+		// create the base repo
+		req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
+			Name:          "close-pull-request-with-path",
+			Private:       false,
+			Readme:        "Default",
+			AutoInit:      true,
+			DefaultBranch: "main",
+		}).AddTokenAuth(user2Token)
+		resp := MakeRequest(t, req, http.StatusCreated)
+		var apiBaseRepo api.Repository
+		DecodeJSON(t, resp, &apiBaseRepo)
+		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
+		user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
+
+		// init the workflow
+		wfTreePath := ".gitea/workflows/pull.yml"
+		wfFileContent := `name: Pull Request
+on:
+  pull_request:
+    types:
+      - closed
+    paths:
+      - 'app/**'
+jobs:
+  echo:
+    runs-on: ubuntu-latest
+    steps:
+      - run: echo 'Hello World'
+`
+
+		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", baseRepo.OwnerName, baseRepo.Name, wfTreePath), &api.CreateFileOptions{
+			FileOptions: api.FileOptions{
+				BranchName: baseRepo.DefaultBranch,
+				Message:    "create " + wfTreePath,
+				Author: api.Identity{
+					Name:  user2.Name,
+					Email: user2.Email,
+				},
+				Committer: api.Identity{
+					Name:  user2.Name,
+					Email: user2.Email,
+				},
+				Dates: api.CommitDateOptions{
+					Author:    time.Now(),
+					Committer: time.Now(),
+				},
+			},
+			ContentBase64: base64.StdEncoding.EncodeToString([]byte(wfFileContent)),
+		}).AddTokenAuth(user2Token)
+		MakeRequest(t, req, http.StatusCreated)
+
+		// user4 forks the repo
+		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
+			&api.CreateForkOption{
+				Name: util.ToPointer("close-pull-request-with-path-fork"),
+			}).AddTokenAuth(user4Token)
+		resp = MakeRequest(t, req, http.StatusAccepted)
+		var apiForkRepo api.Repository
+		DecodeJSON(t, resp, &apiForkRepo)
+		forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
+		user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
+
+		// user4 creates a pull request to add file "app/main.go"
+		doAPICreateFile(user4APICtx, "app/main.go", &api.CreateFileOptions{
+			FileOptions: api.FileOptions{
+				NewBranchName: "user4/add-main",
+				Message:       "create main.go",
+				Author: api.Identity{
+					Name:  user4.Name,
+					Email: user4.Email,
+				},
+				Committer: api.Identity{
+					Name:  user4.Name,
+					Email: user4.Email,
+				},
+				Dates: api.CommitDateOptions{
+					Author:    time.Now(),
+					Committer: time.Now(),
+				},
+			},
+			ContentBase64: base64.StdEncoding.EncodeToString([]byte("// main.go")),
+		})(t)
+		apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/add-main")(t)
+		assert.NoError(t, err)
+
+		doAPIMergePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, apiPull.Index)(t)
+
+		pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
+
+		// load and compare ActionRun
+		assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: baseRepo.ID}))
+		actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID})
+		assert.Equal(t, actions_module.GithubEventPullRequest, actionRun.TriggerEvent)
+		assert.Equal(t, pullRequest.MergedCommitID, actionRun.CommitSHA)
+	})
+}

From 66ccae8b906f1ed107023120508f2bba46e0c133 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 5 Apr 2025 10:08:04 +0800
Subject: [PATCH 179/206] Fix discord webhook 400 status code when description
 limit is exceeded (#34084) (#34124)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Backport #34084 by @Mopcho

Fixes [#34027](https://github.com/go-gitea/gitea/issues/34027)

Discord does not allow for description bigger than 2048 bytes. If the
description is bigger than that it will throw 400 and the event won't
appear in discord. To fix that, in the createPayload method we now slice
the description to ensure it doesn’t exceed the limit.

---------

Co-authored-by: Mopcho <47301161+Mopcho@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 services/webhook/discord.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 43e5e533bff65..d8d8e984374dd 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -14,6 +14,7 @@ import (
 	"unicode/utf8"
 
 	webhook_model "code.gitea.io/gitea/models/webhook"
+	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -101,6 +102,13 @@ var (
 	redColor         = color("ff3232")
 )
 
+// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
+// Discord has some limits in place for the embeds.
+// According to some tests, there is no consistent limit for different character sets.
+// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
+// To keep it simple, we currently truncate at 2000.
+const discordDescriptionCharactersLimit = 2000
+
 type discordConvertor struct {
 	Username  string
 	AvatarURL string
@@ -307,7 +315,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
 		Embeds: []DiscordEmbed{
 			{
 				Title:       title,
-				Description: text,
+				Description: base.TruncateString(text, discordDescriptionCharactersLimit),
 				URL:         url,
 				Color:       color,
 				Author: DiscordEmbedAuthor{

From 9ef2a338d87c4655eeecff0a31b0d9362cb5dd83 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 6 Apr 2025 21:50:37 +0800
Subject: [PATCH 180/206] Fix block expensive for 1.23 (#34127)

---
 routers/common/blockexpensive.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/routers/common/blockexpensive.go b/routers/common/blockexpensive.go
index 1aa0f48d2f013..ba11ff4cf93cb 100644
--- a/routers/common/blockexpensive.go
+++ b/routers/common/blockexpensive.go
@@ -54,6 +54,7 @@ func isRoutePathExpensive(routePattern string) bool {
 		"/{username}/{reponame}/{type:issues}",
 		"/{username}/{reponame}/pulls",
 		"/{username}/{reponame}/{type:pulls}",
+		"/{username}/{reponame}/{type:issues|pulls}", // for 1.23 only
 
 		// wiki
 		"/{username}/{reponame}/wiki/",

From 97171be1b4acb262c0ae8eadeb0c6b56e4463324 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 7 Apr 2025 11:31:28 -0700
Subject: [PATCH 181/206] Release note 1.23.7 (#34117)

---
 CHANGELOG.md | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c112483325ca..476c9d68e9176 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,32 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
+
+* Enhancements
+  * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
+  * Also check default ssh-cert location for host (#34099) (#34100) (#34116)
+* BUGFIXES
+  * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
+  * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
+  * Fix invalid version in RPM package path (#34112) (#34115)
+  * Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
+  * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
+  * Try to fix check-attr bug (#34029) (#34033)
+  * Git client will follow 301 but 307 (#34005) (#34010)
+  * Fix block expensive for 1.23 (#34127)
+  * Fix markdown frontmatter rendering (#34102) (#34107)
+  * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
+  * Do not show 500 error when default branch doesn't exist (#34096) (#34097)
+  * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
+  * Simplify emoji rendering (#34048) (#34049)
+  * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
+  * Pull request updates will also trigger code owners review requests (#33744) (#34045)
+  * Fix org repo creation being limited by user limits (#34030) (#34044)
+  * Fix git client accessing renamed repo (#34034) (#34043)
+  * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
+  * Polyfill WeakRef (#34025) (#34028)
+
 ## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
 
 * SECURITY

From 3eda79647be6147044464c1bc5b9a636a0237a1a Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 9 Apr 2025 10:57:54 +0200
Subject: [PATCH 182/206] Bump go version in go.mod (#34160)

Fixes: https://pkg.go.dev/vuln/GO-2025-3563

Because of a bug in go, we can not remove the minor version in go.mod
without `make tidy` introducing a potentially harmful `toolchain`
directive, so just bump the go version here. From what I gather, this
will be fixed in go 1.25.
---
 go.mod | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/go.mod b/go.mod
index 1ca481e073ef7..e333a1e5624a1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module code.gitea.io/gitea
 
-go 1.23.6
+go 1.23.8
 
 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
 // But some CAs use negative serial number, just relax the check. related:

From 39b6abf955fda4ec05369de4d277ac077a18310f Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 9 Apr 2025 18:14:28 +0800
Subject: [PATCH 183/206] remove hardcoded 'code' string in clone_panel.tmpl
 (#34153) (#34158)

Backport #34153 by @wkelly17

This commit replaces the hardcoded string "code" in the clone panel
button with the i18n local for repo.code.

Co-authored-by: Will Kelly <67284402+wkelly17@users.noreply.github.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 templates/repo/clone_panel.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl
index 19370837a6fd6..a7609ca4e851d 100644
--- a/templates/repo/clone_panel.tmpl
+++ b/templates/repo/clone_panel.tmpl
@@ -1,6 +1,6 @@
 <button class="ui primary button js-btn-clone-panel">
 	{{svg "octicon-code" 16}}
-	<span>Code</span>
+	<span>{{ctx.Locale.Tr "repo.code"}}</span>
 	{{svg "octicon-triangle-down" 14 "dropdown icon"}}
 </button>
 <div class="clone-panel-popup tippy-target">

From 328ce0485fe697a592d50e0abf034859e0744f77 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 10 Apr 2025 12:00:43 +0800
Subject: [PATCH 184/206] bugfix check for alternate ssh host certificate
 location (#34146) (#34166)

Backport #34146 by ManInDark

fixes #34145

Edited all locations to actually be correct.

Co-authored-by: ManInDark <61268856+ManInDark@users.noreply.github.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 docker/root/etc/s6/openssh/setup | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup
index 6fbc599cc5f2d..48e7d4b2117a6 100755
--- a/docker/root/etc/s6/openssh/setup
+++ b/docker/root/etc/s6/openssh/setup
@@ -31,16 +31,19 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
   SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
 fi
 
-if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then
-  SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"}
+# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
+# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
+# the generated key file name is `{keyname}-cert.pub`
+if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
+  SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
 fi
 
-if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then
-  SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"}
+if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
+  SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
 fi
 
-if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then
-  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"}
+if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
+  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
 fi
 
 if [ -d /etc/ssh ]; then

From dc5adce70dea458fc2768c32bdce3e2899861959 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 11 Apr 2025 09:56:06 +0800
Subject: [PATCH 185/206] fix(#33711): cross-publish docker images to ghcr.io
 (#34148) (#34176)

---
 .github/workflows/release-nightly.yml     | 24 +++++++++++++++++++++--
 .github/workflows/release-tag-rc.yml      | 24 +++++++++++++++++++++--
 .github/workflows/release-tag-version.yml | 24 +++++++++++++++++++++--
 3 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 2264c9e822d1c..f459e3910d6a5 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -59,6 +59,8 @@ jobs:
           aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
   nightly-docker-rootful:
     runs-on: namespace-profile-gitea-release-docker
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -85,6 +87,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: fetch go modules
         run: make vendor
       - name: build rootful docker image
@@ -93,9 +101,13 @@ jobs:
           context: .
           platforms: linux/amd64,linux/arm64
           push: true
-          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
+          tags: |-
+            gitea/gitea:${{ steps.clean_name.outputs.branch }}
+            ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
   nightly-docker-rootless:
     runs-on: namespace-profile-gitea-release-docker
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -122,6 +134,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: fetch go modules
         run: make vendor
       - name: build rootless docker image
@@ -131,4 +149,6 @@ jobs:
           platforms: linux/amd64,linux/arm64
           push: true
           file: Dockerfile.rootless
-          tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
+          tags: |-
+            gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
+            ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml
index a406602dc0a74..02da6d1eabdb2 100644
--- a/.github/workflows/release-tag-rc.yml
+++ b/.github/workflows/release-tag-rc.yml
@@ -69,6 +69,8 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
   docker-rootful:
     runs-on: namespace-profile-gitea-release-docker
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -79,7 +81,9 @@ jobs:
       - uses: docker/metadata-action@v5
         id: meta
         with:
-          images: gitea/gitea
+          images: |-
+            gitea/gitea
+            ghcr.io/go-gitea/gitea
           flavor: |
             latest=false
           # 1.2.3-rc0
@@ -90,6 +94,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: build rootful docker image
         uses: docker/build-push-action@v5
         with:
@@ -100,6 +110,8 @@ jobs:
           labels: ${{ steps.meta.outputs.labels }}
   docker-rootless:
     runs-on: namespace-profile-gitea-release-docker
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -110,7 +122,9 @@ jobs:
       - uses: docker/metadata-action@v5
         id: meta
         with:
-          images: gitea/gitea
+          images: |-
+            gitea/gitea
+            ghcr.io/go-gitea/gitea
           # each tag below will have the suffix of -rootless
           flavor: |
             latest=false
@@ -123,6 +137,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: build rootless docker image
         uses: docker/build-push-action@v5
         with:
diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml
index 08bb9baecfebb..158945b615d74 100644
--- a/.github/workflows/release-tag-version.yml
+++ b/.github/workflows/release-tag-version.yml
@@ -14,6 +14,8 @@ concurrency:
 jobs:
   binary:
     runs-on: namespace-profile-gitea-release-binary
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -71,6 +73,8 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
   docker-rootful:
     runs-on: namespace-profile-gitea-release-docker
+    permissions:
+      packages: write # to publish to ghcr.io
     steps:
       - uses: actions/checkout@v4
       # fetch all commits instead of only the last as some branches are long lived and could have many between versions
@@ -81,7 +85,9 @@ jobs:
       - uses: docker/metadata-action@v5
         id: meta
         with:
-          images: gitea/gitea
+          images: |-
+            gitea/gitea
+            ghcr.io/go-gitea/gitea
           # this will generate tags in the following format:
           # latest
           # 1
@@ -96,6 +102,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: build rootful docker image
         uses: docker/build-push-action@v5
         with:
@@ -116,7 +128,9 @@ jobs:
       - uses: docker/metadata-action@v5
         id: meta
         with:
-          images: gitea/gitea
+          images: |-
+            gitea/gitea
+            ghcr.io/go-gitea/gitea
           # each tag below will have the suffix of -rootless
           flavor: |
             suffix=-rootless,onlatest=true
@@ -134,6 +148,12 @@ jobs:
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
+      - name: Login to GHCR using PAT
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
       - name: build rootless docker image
         uses: docker/build-push-action@v5
         with:

From ed0e8865f37447c19908494b75a01043cc15f8f3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 15 Apr 2025 00:57:43 +0800
Subject: [PATCH 186/206] feat: add riscv64 support (#34199) (#34204)

Backport #34199 by @mengzhuo

Co-authored-by: Meng Zhuo <mengzhuo1203@gmail.com>
---
 .github/workflows/release-nightly.yml     | 2 +-
 .github/workflows/release-tag-rc.yml      | 4 ++--
 .github/workflows/release-tag-version.yml | 4 ++--
 Makefile                                  | 2 +-
 docker/manifest.rootless.tmpl             | 5 +++++
 docker/manifest.tmpl                      | 5 +++++
 options/gitignore/Processing              | 1 +
 7 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index f459e3910d6a5..2558a16a71692 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -99,7 +99,7 @@ jobs:
         uses: docker/build-push-action@v5
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: linux/amd64,linux/arm64,linux/riscv64
           push: true
           tags: |-
             gitea/gitea:${{ steps.clean_name.outputs.branch }}
diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml
index 02da6d1eabdb2..37b3ff57d2fc2 100644
--- a/.github/workflows/release-tag-rc.yml
+++ b/.github/workflows/release-tag-rc.yml
@@ -104,7 +104,7 @@ jobs:
         uses: docker/build-push-action@v5
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: linux/amd64,linux/arm64,linux/riscv64
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
@@ -147,7 +147,7 @@ jobs:
         uses: docker/build-push-action@v5
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: linux/amd64,linux/arm64,linux/riscv64
           push: true
           file: Dockerfile.rootless
           tags: ${{ steps.meta.outputs.tags }}
diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml
index 158945b615d74..4250623da0ffb 100644
--- a/.github/workflows/release-tag-version.yml
+++ b/.github/workflows/release-tag-version.yml
@@ -112,7 +112,7 @@ jobs:
         uses: docker/build-push-action@v5
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: linux/amd64,linux/arm64,linux/riscv64
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
@@ -158,7 +158,7 @@ jobs:
         uses: docker/build-push-action@v5
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: linux/amd64,linux/arm64,linux/riscv64
           push: true
           file: Dockerfile.rootless
           tags: ${{ steps.meta.outputs.tags }}
diff --git a/Makefile b/Makefile
index ba98f72550cdf..fca5350899865 100644
--- a/Makefile
+++ b/Makefile
@@ -109,7 +109,7 @@ endif
 
 LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
 
-LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
+LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
 
 GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
 MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
diff --git a/docker/manifest.rootless.tmpl b/docker/manifest.rootless.tmpl
index 1ebf5b73c8477..3fa94ab0ec3e4 100644
--- a/docker/manifest.rootless.tmpl
+++ b/docker/manifest.rootless.tmpl
@@ -22,3 +22,8 @@ manifests:
       architecture: arm64
       os: linux
       variant: v8
+  -
+    image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64-rootless
+    platform:
+      architecture: riscv64
+      os: linux
diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl
index 08ccf61b57854..c68ca46dd8695 100644
--- a/docker/manifest.tmpl
+++ b/docker/manifest.tmpl
@@ -22,3 +22,8 @@ manifests:
       architecture: arm64
       os: linux
       variant: v8
+  -
+    image: gitea/gitea:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}{{#if (hasPrefix "refs/heads/release/v" build.ref)}}{{trimPrefix "refs/heads/release/v" build.ref}}-{{/if}}nightly{{/if}}-linux-riscv64
+    platform:
+      architecture: riscv64
+      os: linux
diff --git a/options/gitignore/Processing b/options/gitignore/Processing
index 942ebbccb5de8..2d243c96bd24c 100644
--- a/options/gitignore/Processing
+++ b/options/gitignore/Processing
@@ -2,6 +2,7 @@
 applet
 application.linux-arm64
 application.linux-armv6hf
+application.linux-riscv64
 application.linux32
 application.linux64
 application.windows32

From 6e4e3ca012f26355af2c4fc85809dd0faa960214 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Wed, 16 Apr 2025 15:46:33 +0800
Subject: [PATCH 187/206] Fix empty repo clone panel border (#34219) (#34220)

Backport #34219 by kerwin612

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 web_src/css/repo/clone.css | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css
index c6887fbf161c4..866b64c0275db 100644
--- a/web_src/css/repo/clone.css
+++ b/web_src/css/repo/clone.css
@@ -5,9 +5,12 @@
   flex: 1;
 }
 
-.clone-buttons-combo input {
-  border-left: none !important;
-  border-radius: 0 !important;
+.clone-buttons-combo > .ui.button:not(:last-child) {
+  border-right: none;
+}
+
+.ui.action.input.clone-buttons-combo input {
+  border-radius: 0; /* override fomantic border-radius for ".ui.input > input" */
   height: 30px;
 }
 

From c01459a504e8da966fc5a87ee00897720b1e3071 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Thu, 17 Apr 2025 00:23:55 +0800
Subject: [PATCH 188/206] Fix two missed null value checks on the wiki page.
 (#34205) (#34215)

Backport #34205 by @kerwin612

before:

![image](https://github.com/user-attachments/assets/83e5513f-a4fa-406d-a010-8ec8cd873203)

after:

![image](https://github.com/user-attachments/assets/6bca76c7-0445-429a-92b1-1a9f96d6daca)

Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
---
 templates/repo/wiki/revision.tmpl | 2 +-
 templates/repo/wiki/view.tmpl     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
index ca8954928d26f..c1676ba1f6af9 100644
--- a/templates/repo/wiki/revision.tmpl
+++ b/templates/repo/wiki/revision.tmpl
@@ -6,7 +6,7 @@
 		<div class="ui stackable grid">
 			<div class="ui eight wide column">
 				<div class="ui header">
-					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}"><span>{{.revision}}</span> {{svg "octicon-home"}}</a>
+					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}">{{if .revision}}<span>{{.revision}}</span> {{end}}{{svg "octicon-home"}}</a>
 					{{$title}}
 					<div class="ui sub header tw-break-anywhere">
 						{{$timeSince := DateUtils.TimeSince .Author.When}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index 843a977e3e57c..efae40fa51bf5 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -33,7 +33,7 @@
 		<div class="ui dividing header">
 			<div class="flex-text-block tw-flex-wrap tw-justify-end">
 				<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
-					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" ><span>{{.CommitCount}}</span> {{svg "octicon-history"}}</a>
+					<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" >{{if .CommitCount}}<span>{{.CommitCount}}</span> {{end}}{{svg "octicon-history"}}</a>
 					<div class="tw-flex-1 gt-ellipsis">
 						{{$title}}
 						<div class="ui sub header gt-ellipsis">

From a01f56a61a274f3b970ec32b0468daae211ffa8a Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 17 Apr 2025 18:45:59 -0700
Subject: [PATCH 189/206] Update net package (#34228) (#34232)

---
 go.mod | 2 +-
 go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/go.mod b/go.mod
index e333a1e5624a1..3ea7c7becbb69 100644
--- a/go.mod
+++ b/go.mod
@@ -120,7 +120,7 @@ require (
 	github.com/yuin/goldmark-meta v1.1.0
 	golang.org/x/crypto v0.36.0
 	golang.org/x/image v0.21.0
-	golang.org/x/net v0.37.0
+	golang.org/x/net v0.38.0
 	golang.org/x/oauth2 v0.27.0
 	golang.org/x/sync v0.12.0
 	golang.org/x/sys v0.31.0
diff --git a/go.sum b/go.sum
index ebf40cb62849a..2b21528544eda 100644
--- a/go.sum
+++ b/go.sum
@@ -870,8 +870,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
 golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

From 260816d64acd34f6ff1610abef7a266186887a96 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 18 Apr 2025 11:10:58 +0800
Subject: [PATCH 190/206] Fix project board links to related Pull Requests
 (#34213) (#34222)

Backport #34213 by @badhezi

Resolves https://github.com/go-gitea/gitea/issues/34181

Co-authored-by: badhezi <zlilaharon@gmail.com>
---
 templates/repo/issue/card.tmpl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl
index 2e19e86d7af30..a3f361004417e 100644
--- a/templates/repo/issue/card.tmpl
+++ b/templates/repo/issue/card.tmpl
@@ -45,7 +45,7 @@
 		{{if $.Page.LinkedPRs}}
 		{{range index $.Page.LinkedPRs .ID}}
 		<div class="meta tw-my-1">
-			<a href="{{$.Issue.Repo.Link}}/pulls/{{.Index}}">
+			<a href="{{.Repo.Link}}/pulls/{{.Index}}">
 				<span class="tw-m-0 text {{if .PullRequest.HasMerged}}purple{{else if .IsClosed}}red{{else}}green{{end}}">{{svg "octicon-git-merge" 16 "tw-mr-1 tw-align-middle"}}</span>
 				<span class="tw-align-middle">{{.Title}} <span class="text light grey">#{{.Index}}</span></span>
 			</a>

From 60f175f7ff7aa5046413a57384334225f037bf41 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Fri, 18 Apr 2025 20:34:28 +0800
Subject: [PATCH 191/206] Swift files can be passed either as file or as form
 value (#34068) (#34236)

Backport #34068 by wgr1984

Fix #33990

Co-authored-by: Wolfgang Reithmeier <w.reithmeier@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/packages/package_version.go           |  4 +-
 routers/api/packages/swift/swift.go          | 36 +++++--
 tests/integration/api_packages_swift_test.go | 98 +++++++++++++++++++-
 3 files changed, 123 insertions(+), 15 deletions(-)

diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 278e8e3a86b0e..bb7fd895f81da 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -279,9 +279,7 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
 	default:
 		e.Desc("package_version.created_unix")
 	}
-
-	// Sort by id for stable order with duplicates in the other field
-	e.Asc("package_version.id")
+	e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
 }
 
 // SearchVersions gets all versions of packages matching the search options
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 4d7fb8b1a6992..9c909d49186f0 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) {
 	})
 }
 
-// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
+// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present.
+func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) {
+	multipartFile, _, err := ctx.Req.FormFile(formKey)
+	if err != nil && !errors.Is(err, http.ErrMissingFile) {
+		return nil, err
+	}
+	if multipartFile != nil {
+		return multipartFile, nil
+	}
+
+	content := ctx.Req.FormValue(formKey)
+	if content == "" {
+		return nil, nil
+	}
+	return io.NopCloser(strings.NewReader(ctx.Req.FormValue(formKey))), nil
+}
+
+// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
 func UploadPackageFile(ctx *context.Context) {
 	packageScope := ctx.PathParam("scope")
 	packageName := ctx.PathParam("name")
@@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) {
 
 	packageVersion := v.Core().String()
 
-	file, _, err := ctx.Req.FormFile("source-archive")
-	if err != nil {
-		apiError(ctx, http.StatusBadRequest, err)
+	file, err := formFileOptionalReadCloser(ctx, "source-archive")
+	if file == nil || err != nil {
+		apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
 		return
 	}
 	defer file.Close()
@@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) {
 	}
 	defer buf.Close()
 
-	var mr io.Reader
-	metadata := ctx.Req.FormValue("metadata")
-	if metadata != "" {
-		mr = strings.NewReader(metadata)
+	mr, err := formFileOptionalReadCloser(ctx, "metadata")
+	if err != nil {
+		apiError(ctx, http.StatusBadRequest, "unable to read metadata file")
+		return
+	}
+	if mr != nil {
+		defer mr.Close()
 	}
 
 	pck, err := swift_module.ParsePackage(buf, buf.Size(), mr)
diff --git a/tests/integration/api_packages_swift_test.go b/tests/integration/api_packages_swift_test.go
index c0e0dccfab11b..b29e8459ffb2b 100644
--- a/tests/integration/api_packages_swift_test.go
+++ b/tests/integration/api_packages_swift_test.go
@@ -23,6 +23,7 @@ import (
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestPackageSwift(t *testing.T) {
@@ -34,6 +35,7 @@ func TestPackageSwift(t *testing.T) {
 	packageName := "test_package"
 	packageID := packageScope + "." + packageName
 	packageVersion := "1.0.3"
+	packageVersion2 := "1.0.4"
 	packageAuthor := "KN4CK3R"
 	packageDescription := "Gitea Test Package"
 	packageRepositoryURL := "https://gitea.io/gitea/gitea"
@@ -183,6 +185,94 @@ func TestPackageSwift(t *testing.T) {
 		)
 	})
 
+	t.Run("UploadMultipart", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		uploadPackage := func(t *testing.T, url string, expectedStatus int, sr io.Reader, metadata string) {
+			var body bytes.Buffer
+			mpw := multipart.NewWriter(&body)
+
+			// Read the source archive content
+			sourceContent, err := io.ReadAll(sr)
+			assert.NoError(t, err)
+			mpw.WriteField("source-archive", string(sourceContent))
+
+			if metadata != "" {
+				mpw.WriteField("metadata", metadata)
+			}
+
+			mpw.Close()
+
+			req := NewRequestWithBody(t, "PUT", url, &body).
+				SetHeader("Content-Type", mpw.FormDataContentType()).
+				SetHeader("Accept", swift_router.AcceptJSON).
+				AddBasicAuth(user.Name)
+			MakeRequest(t, req, expectedStatus)
+		}
+
+		createArchive := func(files map[string]string) *bytes.Buffer {
+			var buf bytes.Buffer
+			zw := zip.NewWriter(&buf)
+			for filename, content := range files {
+				w, _ := zw.Create(filename)
+				w.Write([]byte(content))
+			}
+			zw.Close()
+			return &buf
+		}
+
+		uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion2)
+
+		req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
+		MakeRequest(t, req, http.StatusUnauthorized)
+
+		// Test with metadata as form field
+		uploadPackage(
+			t,
+			uploadURL,
+			http.StatusCreated,
+			createArchive(map[string]string{
+				"Package.swift":           contentManifest1,
+				"Package@swift-5.6.swift": contentManifest2,
+			}),
+			`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
+		)
+
+		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
+		assert.NoError(t, err)
+		require.Len(t, pvs, 2) // ATTENTION: many subtests are unable to run separately, they depend on the results of previous tests
+		thisPackageVersion := pvs[0]
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, thisPackageVersion)
+		assert.NoError(t, err)
+		assert.NotNil(t, pd.SemVer)
+		assert.Equal(t, packageID, pd.Package.Name)
+		assert.Equal(t, packageVersion2, pd.Version.Version)
+		assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
+		metadata := pd.Metadata.(*swift_module.Metadata)
+		assert.Equal(t, packageDescription, metadata.Description)
+		assert.Len(t, metadata.Manifests, 2)
+		assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
+		assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
+		assert.Len(t, pd.VersionProperties, 1)
+		assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
+
+		pfs, err := packages.GetFilesByVersionID(db.DefaultContext, thisPackageVersion.ID)
+		assert.NoError(t, err)
+		assert.Len(t, pfs, 1)
+		assert.Equal(t, fmt.Sprintf("%s-%s.zip", packageName, packageVersion2), pfs[0].Name)
+		assert.True(t, pfs[0].IsLead)
+
+		uploadPackage(
+			t,
+			uploadURL,
+			http.StatusConflict,
+			createArchive(map[string]string{
+				"Package.swift": contentManifest1,
+			}),
+			"",
+		)
+	})
+
 	t.Run("Download", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
@@ -211,7 +301,7 @@ func TestPackageSwift(t *testing.T) {
 			SetHeader("Accept", swift_router.AcceptJSON)
 		resp := MakeRequest(t, req, http.StatusOK)
 
-		versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion)
+		versionURL := setting.AppURL + url[1:] + fmt.Sprintf("/%s/%s/%s", packageScope, packageName, packageVersion2)
 
 		assert.Equal(t, "1", resp.Header().Get("Content-Version"))
 		assert.Equal(t, fmt.Sprintf(`<%s>; rel="latest-version"`, versionURL), resp.Header().Get("Link"))
@@ -221,9 +311,9 @@ func TestPackageSwift(t *testing.T) {
 		var result *swift_router.EnumeratePackageVersionsResponse
 		DecodeJSON(t, resp, &result)
 
-		assert.Len(t, result.Releases, 1)
-		assert.Contains(t, result.Releases, packageVersion)
-		assert.Equal(t, versionURL, result.Releases[packageVersion].URL)
+		assert.Len(t, result.Releases, 2)
+		assert.Contains(t, result.Releases, packageVersion2)
+		assert.Equal(t, versionURL, result.Releases[packageVersion2].URL)
 
 		req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.json", url, packageScope, packageName)).
 			AddBasicAuth(user.Name)

From 030ed9462dd04fcd1921889d1b67b4baf74a53cf Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Sat, 19 Apr 2025 11:37:22 +0800
Subject: [PATCH 192/206] Don't assume the default wiki branch is master in the
 wiki API (#34244) (#34245)

Backport #34244 by kemzeb

Resolves #34218.

In the recent past, the default wiki branch was made to be changeable.
This change reflects this.

Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com>
---
 routers/api/v1/repo/wiki.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index f9906ed250d18..53dc948632ecf 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
 	}
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 
 	// Get last change information.
 	lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@@ -432,7 +432,7 @@ func ListPageRevisions(ctx *context.APIContext) {
 	}
 
 	// get commit count - wiki revisions
-	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+	commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
 
 	page := ctx.FormInt("page")
 	if page <= 1 {
@@ -442,7 +442,7 @@ func ListPageRevisions(ctx *context.APIContext) {
 	// get Commit Count
 	commitsHistory, err := wikiRepo.CommitsByFileAndRange(
 		git.CommitsByFileAndRangeOptions{
-			Revision: "master",
+			Revision: ctx.Repo.Repository.DefaultWikiBranch,
 			File:     pageFilename,
 			Page:     page,
 		})
@@ -486,7 +486,7 @@ func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit)
 		return nil, nil
 	}
 
-	commit, err := wikiRepo.GetBranchCommit("master")
+	commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
 	if err != nil {
 		if git.IsErrNotExist(err) {
 			ctx.NotFound(err)

From 4e5aca62ee76ac0651cf27c5fd21917eb884ea81 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Thu, 24 Apr 2025 18:28:02 -0700
Subject: [PATCH 193/206] Fix panic when comment is nil (#34257) (#34277)

Fix #34254
Backport #34257
---
 models/issues/review.go               |  2 +-
 services/issue/pull.go                |  6 ++++++
 tests/integration/pull_review_test.go | 24 +++---------------------
 3 files changed, 10 insertions(+), 22 deletions(-)

diff --git a/models/issues/review.go b/models/issues/review.go
index 3e787273be8e1..d8dc539c3b384 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -663,7 +663,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
 	}
 
 	if review != nil {
-		// skip it when reviewer hase been request to review
+		// skip it when reviewer has been request to review
 		if review.Type == ReviewTypeRequest {
 			return nil, committer.Commit() // still commit the transaction, or committer.Close() will rollback it, even if it's a reused transaction.
 		}
diff --git a/services/issue/pull.go b/services/issue/pull.go
index bd19c254362b9..c9d32000aff5c 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -145,6 +145,9 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
 				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
 				return nil, err
 			}
+			if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
+				continue
+			}
 			notifiers = append(notifiers, &ReviewRequestNotifier{
 				Comment:  comment,
 				IsAdd:    true,
@@ -158,6 +161,9 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
 			log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
 			return nil, err
 		}
+		if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
+			continue
+		}
 		notifiers = append(notifiers, &ReviewRequestNotifier{
 			Comment:    comment,
 			IsAdd:      true,
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 878020625b48b..fef11496e4c69 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -8,7 +8,6 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"path"
-	"sort"
 	"strings"
 	"testing"
 
@@ -76,7 +75,7 @@ func TestPullView_CodeOwner(t *testing.T) {
 
 		t.Run("First Pull Request", func(t *testing.T) {
 			// create a new branch to prepare for pull request
-			resp1, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
 				NewBranch: "codeowner-basebranch",
 				Files: []*files_service.ChangeRepoFile{
 					{
@@ -96,13 +95,8 @@ func TestPullView_CodeOwner(t *testing.T) {
 			unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
 			assert.NoError(t, pr.LoadIssue(db.DefaultContext))
 
-			reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
-			assert.NoError(t, err)
-			assert.Len(t, reviewNotifiers, 1)
-			assert.EqualValues(t, 5, reviewNotifiers[0].Reviewer.ID)
-
 			// update the file on the pr branch
-			resp2, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+			_, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
 				OldBranch: "codeowner-basebranch",
 				Files: []*files_service.ChangeRepoFile{
 					{
@@ -114,14 +108,7 @@ func TestPullView_CodeOwner(t *testing.T) {
 			})
 			assert.NoError(t, err)
 
-			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
-			assert.NoError(t, err)
-			assert.Len(t, reviewNotifiers, 2)
-			reviewerIDs := []int64{reviewNotifiers[0].Reviewer.ID, reviewNotifiers[1].Reviewer.ID}
-			sort.Slice(reviewerIDs, func(i, j int) bool { return reviewerIDs[i] < reviewerIDs[j] })
-			assert.EqualValues(t, []int64{5, 8}, reviewerIDs)
-
-			reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(db.DefaultContext, pr, resp1.Commit.SHA, resp2.Commit.SHA)
+			reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
 			assert.NoError(t, err)
 			assert.Len(t, reviewNotifiers, 1)
 			assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
@@ -171,11 +158,6 @@ func TestPullView_CodeOwner(t *testing.T) {
 
 			pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
 			unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
-
-			reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(db.DefaultContext, pr)
-			assert.NoError(t, err)
-			assert.Len(t, reviewNotifiers, 1)
-			assert.EqualValues(t, 8, reviewNotifiers[0].Reviewer.ID)
 		})
 
 		t.Run("Forked Repo Pull Request", func(t *testing.T) {

From 4f6d09fb6870e88f656e66084e98d46904541843 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 28 Apr 2025 10:34:46 +0800
Subject: [PATCH 194/206] Update token creation API swagger documentation
 (#34288) (#34296)

Backport #34288 by @lunny

Fix #34231

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 modules/structs/user_app.go    |  4 +++-
 templates/swagger/v1_json.tmpl | 13 ++++++++++++-
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/modules/structs/user_app.go b/modules/structs/user_app.go
index a7d2e28b41291..8401252bd6591 100644
--- a/modules/structs/user_app.go
+++ b/modules/structs/user_app.go
@@ -23,9 +23,11 @@ type AccessToken struct {
 type AccessTokenList []*AccessToken
 
 // CreateAccessTokenOption options when create access token
+// swagger:model CreateAccessTokenOption
 type CreateAccessTokenOption struct {
 	// required: true
-	Name   string   `json:"name" binding:"Required"`
+	Name string `json:"name" binding:"Required"`
+	// example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"]
 	Scopes []string `json:"scopes"`
 }
 
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index fb37d45ce8de2..3c87bc752e5ad 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -19616,7 +19616,18 @@
           "items": {
             "type": "string"
           },
-          "x-go-name": "Scopes"
+          "x-go-name": "Scopes",
+          "example": [
+            "all",
+            "read:activitypub",
+            "read:issue",
+            "write:misc",
+            "read:notification",
+            "read:organization",
+            "read:package",
+            "read:repository",
+            "read:user"
+          ]
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"

From 5546b4279c6737916518bcca1eca973e8587fc09 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 28 Apr 2025 09:30:42 -0700
Subject: [PATCH 195/206] Explicitly not update indexes when sync database
 schemas (#34281) (#34295)

Fix #34275
Backport #34281

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/migrations/v1_23/v299.go | 6 +++++-
 models/migrations/v1_23/v300.go | 6 +++++-
 models/migrations/v1_23/v301.go | 6 +++++-
 models/migrations/v1_23/v303.go | 6 +++++-
 models/migrations/v1_23/v306.go | 6 +++++-
 models/migrations/v1_23/v310.go | 6 +++++-
 models/migrations/v1_23/v311.go | 7 +++++--
 7 files changed, 35 insertions(+), 8 deletions(-)

diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go
index f6db960c3b41b..e5fde3749b6ce 100644
--- a/models/migrations/v1_23/v299.go
+++ b/models/migrations/v1_23/v299.go
@@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error {
 		ContentVersion int `xorm:"NOT NULL DEFAULT 0"`
 	}
 
-	return x.Sync(new(Comment), new(Issue))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(Comment), new(Issue))
+	return err
 }
diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go
index f1f1cccdbfb5e..51de43da5e6da 100644
--- a/models/migrations/v1_23/v300.go
+++ b/models/migrations/v1_23/v300.go
@@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error {
 		ForcePushAllowlistTeamIDs    []int64 `xorm:"JSON TEXT"`
 		ForcePushAllowlistDeployKeys bool    `xorm:"NOT NULL DEFAULT false"`
 	}
-	return x.Sync(new(ProtectedBranch))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(ProtectedBranch))
+	return err
 }
diff --git a/models/migrations/v1_23/v301.go b/models/migrations/v1_23/v301.go
index b7797f6c6b5eb..99c8e3d8eac2f 100644
--- a/models/migrations/v1_23/v301.go
+++ b/models/migrations/v1_23/v301.go
@@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
 	type oauth2Application struct {
 		SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
 	}
-	return x.Sync(new(oauth2Application))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(oauth2Application))
+	return err
 }
diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go
index adfe917d3f241..1e3638893021b 100644
--- a/models/migrations/v1_23/v303.go
+++ b/models/migrations/v1_23/v303.go
@@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error {
 		CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field
 	}
 
-	return x.Sync(new(Comment))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(Comment))
+	return err
 }
diff --git a/models/migrations/v1_23/v306.go b/models/migrations/v1_23/v306.go
index 276b438e95bcf..a1e698fe31f03 100644
--- a/models/migrations/v1_23/v306.go
+++ b/models/migrations/v1_23/v306.go
@@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error {
 	type ProtectedBranch struct {
 		BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"`
 	}
-	return x.Sync(new(ProtectedBranch))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(ProtectedBranch))
+	return err
 }
diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go
index 394417f5a01b0..c856a708f9175 100644
--- a/models/migrations/v1_23/v310.go
+++ b/models/migrations/v1_23/v310.go
@@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error {
 		Priority int64 `xorm:"NOT NULL DEFAULT 0"`
 	}
 
-	return x.Sync(new(ProtectedBranch))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(ProtectedBranch))
+	return err
 }
diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go
index 0fc1ac8c0e85c..21293d83be046 100644
--- a/models/migrations/v1_23/v311.go
+++ b/models/migrations/v1_23/v311.go
@@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
 	type Issue struct {
 		TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
 	}
-
-	return x.Sync(new(Issue))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreConstrains: true,
+		IgnoreIndices:    true,
+	}, new(Issue))
+	return err
 }

From a9d5ab8f888ef84bdaa725816cfda8863f721788 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 29 Apr 2025 01:12:36 +0800
Subject: [PATCH 196/206] fix github migration error when using multiple tokens
 (#34144) (#34302)

Backport #34144 by @TheFox0x7

Git authorization was not taking into account multiple token feature,
leading to auth failures

Closes: https://github.com/go-gitea/gitea/issues/34141

---------

Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 services/migrations/github.go      | 17 ++++++++++++++-
 services/migrations/github_test.go | 34 ++++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/services/migrations/github.go b/services/migrations/github.go
index 604ab84b39645..bbb378b8b29af 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -135,7 +135,7 @@ func (g *GithubDownloaderV3) LogString() string {
 func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
 	githubClient := github.NewClient(client)
 	if baseURL != "https://github.com" {
-		githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
+		githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
 	}
 	g.clients = append(g.clients, githubClient)
 	g.rates = append(g.rates, nil)
@@ -879,3 +879,18 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
 	}
 	return allReviews, nil
 }
+
+// FormatCloneURL add authentication into remote URLs
+func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
+	u, err := url.Parse(remoteAddr)
+	if err != nil {
+		return "", err
+	}
+	if len(opts.AuthToken) > 0 {
+		// "multiple tokens" are used to benefit more "API rate limit quota"
+		// git clone doesn't count for rate limits, so only use the first token.
+		// source: https://github.com/orgs/community/discussions/44515
+		u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
+	}
+	return u.String(), nil
+}
diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go
index 2b89e6dc0fd16..13f4b358c50d3 100644
--- a/services/migrations/github_test.go
+++ b/services/migrations/github_test.go
@@ -13,6 +13,7 @@ import (
 	base "code.gitea.io/gitea/modules/migration"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestGitHubDownloadRepo(t *testing.T) {
@@ -429,3 +430,36 @@ func TestGitHubDownloadRepo(t *testing.T) {
 		},
 	}, reviews)
 }
+
+func TestGithubMultiToken(t *testing.T) {
+	testCases := []struct {
+		desc             string
+		token            string
+		expectedCloneURL string
+	}{
+		{
+			desc:             "Single Token",
+			token:            "single_token",
+			expectedCloneURL: "https://oauth2:single_token@github.com",
+		},
+		{
+			desc:             "Multi Token",
+			token:            "token1,token2",
+			expectedCloneURL: "https://oauth2:token1@github.com",
+		},
+	}
+	factory := GithubDownloaderV3Factory{}
+
+	for _, tC := range testCases {
+		t.Run(tC.desc, func(t *testing.T) {
+			opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
+			client, err := factory.New(context.Background(), opts)
+			require.NoError(t, err)
+
+			cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
+			require.NoError(t, err)
+
+			assert.Equal(t, tC.expectedCloneURL, cloneURL)
+		})
+	}
+}

From e3e705200a672b56b412d25ecc05a4061aae3f3f Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 29 Apr 2025 10:48:45 +0800
Subject: [PATCH 197/206] Fix wrong review requests when updating the pull
 request (#34286) (#34304)

Backport #34286 by @lunny

Fix #34224

The previous implementation in #33744 will get the pushed commits
changed files. But it's not always right when push a merged commit. This
PR reverted the logic in #33744 and will always get the PR's changed
files and get code owners.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 services/issue/pull.go | 36 ++++++++++++++++++++++--------------
 services/pull/pull.go  |  7 +------
 2 files changed, 23 insertions(+), 20 deletions(-)

diff --git a/services/issue/pull.go b/services/issue/pull.go
index c9d32000aff5c..3543b05b18f78 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -48,10 +48,6 @@ func IsCodeOwnerFile(f string) bool {
 }
 
 func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
-	return PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, "", "") // no commit is provided, then it uses PR's base&head branch
-}
-
-func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_model.PullRequest, startCommitID, endCommitID string) ([]*ReviewRequestNotifier, error) {
 	if err := pr.LoadIssue(ctx); err != nil {
 		return nil, err
 	}
@@ -100,19 +96,15 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
 		return nil, nil
 	}
 
-	if startCommitID == "" && endCommitID == "" {
-		// get the mergebase
-		mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
-		if err != nil {
-			return nil, err
-		}
-		startCommitID = mergeBase
-		endCommitID = pr.GetGitRefName()
+	// get the mergebase
+	mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
+	if err != nil {
+		return nil, err
 	}
 
 	// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
 	// between the merge base and the head commit but not the base branch and the head commit
-	changedFiles, err := repo.GetFilesChangedBetween(startCommitID, endCommitID)
+	changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitRefName())
 	if err != nil {
 		return nil, err
 	}
@@ -138,8 +130,23 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
 		return nil, err
 	}
 
+	// load all reviews from database
+	latestReivews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID)
+	if err != nil {
+		return nil, err
+	}
+
+	contain := func(list issues_model.ReviewList, u *user_model.User) bool {
+		for _, review := range list {
+			if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID {
+				return true
+			}
+		}
+		return false
+	}
+
 	for _, u := range uniqUsers {
-		if u.ID != issue.Poster.ID {
+		if u.ID != issue.Poster.ID && !contain(latestReivews, u) {
 			comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster)
 			if err != nil {
 				log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
@@ -155,6 +162,7 @@ func PullRequestCodeOwnersReviewSpecialCommits(ctx context.Context, pr *issues_m
 			})
 		}
 	}
+
 	for _, t := range uniqTeams {
 		comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster)
 		if err != nil {
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 11718c2f877ef..3027059405009 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -440,12 +440,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
 					}
 
 					if !pr.IsWorkInProgress(ctx) {
-						var reviewNotifiers []*issue_service.ReviewRequestNotifier
-						if opts.IsForcePush {
-							reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, pr)
-						} else {
-							reviewNotifiers, err = issue_service.PullRequestCodeOwnersReviewSpecialCommits(ctx, pr, opts.OldCommitID, opts.NewCommitID)
-						}
+						reviewNotifiers, err := issue_service.PullRequestCodeOwnersReview(ctx, pr)
 						if err != nil {
 							log.Error("PullRequestCodeOwnersReview: %v", err)
 						}

From 046fc8684cc97b6f6192dfb484e84e646588b052 Mon Sep 17 00:00:00 2001
From: NorthRealm <155140859+NorthRealm@users.noreply.github.com>
Date: Tue, 29 Apr 2025 20:11:08 +0000
Subject: [PATCH 198/206] Fix CI Build (#34315)

Backport  #34309
---
 go.mod | 2 ++
 go.sum | 4 ++--
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 3ea7c7becbb69..98de6ec93f6ce 100644
--- a/go.mod
+++ b/go.mod
@@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra
 // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
 replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
 
+replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
+
 exclude github.com/gofrs/uuid v3.2.0+incompatible
 
 exclude github.com/gofrs/uuid v4.0.0+incompatible
diff --git a/go.sum b/go.sum
index 2b21528544eda..d8ca43ebbd06d 100644
--- a/go.sum
+++ b/go.sum
@@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
-git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
 gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4=
 gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
 gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
 gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
+gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
+gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
 gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
 gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
 gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=

From 8e5aa8fb1e0a11a5a0815c2d759f4da80f203cb0 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 5 May 2025 03:56:47 +0800
Subject: [PATCH 199/206] Fix bug when visiting comparation page (#34334)
 (#34364)

Backport #34334 by @lunny

The `ci.HeadGitRepo` was opened and closed in the function
`ParseCompareInfo` but reused in the function `PrepareCompareDiff`.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/web/repo/compare.go | 3 +--
 routers/web/repo/pull.go    | 2 +-
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 278974bec3ecf..9d241d3b511ed 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -405,7 +405,6 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
 			ctx.ServerError("OpenRepository", err)
 			return nil
 		}
-		defer ci.HeadGitRepo.Close()
 	} else {
 		ctx.NotFound("ParseCompareInfo", nil)
 		return nil
@@ -708,7 +707,7 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
 func CompareDiff(ctx *context.Context) {
 	ci := ParseCompareInfo(ctx)
 	defer func() {
-		if ci != nil && ci.HeadGitRepo != nil {
+		if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
 			ci.HeadGitRepo.Close()
 		}
 	}()
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index e068ac45c6588..ac08bf2a4ff5d 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1263,7 +1263,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
 
 	ci := ParseCompareInfo(ctx)
 	defer func() {
-		if ci != nil && ci.HeadGitRepo != nil {
+		if !ctx.Repo.PullRequest.SameRepo && ci != nil && ci.HeadGitRepo != nil {
 			ci.HeadGitRepo.Close()
 		}
 	}()

From a4cc86740102b53de7ace157ae50fcd739226290 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 6 May 2025 01:04:52 +0800
Subject: [PATCH 200/206] Fix bug when API get pull changed files for deleted
 head repository (#34333) (#34368)

Backport #34333 by @lunny

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 routers/api/v1/repo/pull.go        |  4 +-
 tests/integration/api_pull_test.go | 98 ++++++++++++++++++++++++++++++
 2 files changed, 101 insertions(+), 1 deletion(-)

diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 6f4f3efaa10fa..75db2bd55b484 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -1638,7 +1638,9 @@ func GetPullRequestFiles(ctx *context.APIContext) {
 
 	apiFiles := make([]*api.ChangedFile, 0, limit)
 	for i := start; i < start+limit; i++ {
-		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID))
+		// refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository.
+		// The head repository might have been deleted, so we should not rely on it here.
+		apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
 	}
 
 	ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index 4dfc8a332a89a..f76aa013865ae 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -9,7 +9,10 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"net/url"
+	"strings"
 	"testing"
+	"time"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -18,11 +21,15 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/services/convert"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/gitdiff"
 	issue_service "code.gitea.io/gitea/services/issue"
+	pull_service "code.gitea.io/gitea/services/pull"
+	files_service "code.gitea.io/gitea/services/repository/files"
 	"code.gitea.io/gitea/tests"
 
 	"github.com/stretchr/testify/assert"
@@ -425,3 +432,94 @@ func TestAPICommitPullRequest(t *testing.T) {
 	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/commits/%s/pull", owner.Name, repo.Name, invalidCommitSHA).AddTokenAuth(ctx.Token)
 	ctx.Session.MakeRequest(t, req, http.StatusNotFound)
 }
+
+func TestAPIViewPullFilesWithHeadRepoDeleted(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+		user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+		ctx := NewAPITestContext(t, "user1", baseRepo.Name, auth_model.AccessTokenScopeAll)
+
+		doAPIForkRepository(ctx, "user2")(t)
+
+		forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ForkID: baseRepo.ID, OwnerName: "user1"})
+
+		// add a new file to the forked repo
+		addFileToForkedResp, err := files_service.ChangeRepoFiles(git.DefaultContext, forkedRepo, user1, &files_service.ChangeRepoFilesOptions{
+			Files: []*files_service.ChangeRepoFile{
+				{
+					Operation:     "create",
+					TreePath:      "file_1.txt",
+					ContentReader: strings.NewReader("file1"),
+				},
+			},
+			Message:   "add file1",
+			OldBranch: "master",
+			NewBranch: "fork-branch-1",
+			Author: &files_service.IdentityOptions{
+				Name:  user1.Name,
+				Email: user1.Email,
+			},
+			Committer: &files_service.IdentityOptions{
+				Name:  user1.Name,
+				Email: user1.Email,
+			},
+			Dates: &files_service.CommitDateOptions{
+				Author:    time.Now(),
+				Committer: time.Now(),
+			},
+		})
+		assert.NoError(t, err)
+		assert.NotEmpty(t, addFileToForkedResp)
+
+		// create Pull
+		pullIssue := &issues_model.Issue{
+			RepoID:   baseRepo.ID,
+			Title:    "Test pull-request-target-event",
+			PosterID: user1.ID,
+			Poster:   user1,
+			IsPull:   true,
+		}
+		pullRequest := &issues_model.PullRequest{
+			HeadRepoID: forkedRepo.ID,
+			BaseRepoID: baseRepo.ID,
+			HeadBranch: "fork-branch-1",
+			BaseBranch: "master",
+			HeadRepo:   forkedRepo,
+			BaseRepo:   baseRepo,
+			Type:       issues_model.PullRequestGitea,
+		}
+
+		prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
+		err = pull_service.NewPullRequest(git.DefaultContext, prOpts)
+		assert.NoError(t, err)
+		pr := convert.ToAPIPullRequest(context.Background(), pullRequest, user1)
+
+		ctx = NewAPITestContext(t, "user2", baseRepo.Name, auth_model.AccessTokenScopeAll)
+		doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
+			if assert.Len(t, files, 1) {
+				assert.Equal(t, "file_1.txt", files[0].Filename)
+				assert.Empty(t, files[0].PreviousFilename)
+				assert.Equal(t, 1, files[0].Additions)
+				assert.Equal(t, 1, files[0].Changes)
+				assert.Equal(t, 0, files[0].Deletions)
+				assert.Equal(t, "added", files[0].Status)
+			}
+		})(t)
+
+		// delete the head repository of the pull request
+		forkCtx := NewAPITestContext(t, "user1", forkedRepo.Name, auth_model.AccessTokenScopeAll)
+		doAPIDeleteRepository(forkCtx)(t)
+
+		doAPIGetPullFiles(ctx, pr, func(t *testing.T, files []*api.ChangedFile) {
+			if assert.Len(t, files, 1) {
+				assert.Equal(t, "file_1.txt", files[0].Filename)
+				assert.Empty(t, files[0].PreviousFilename)
+				assert.Equal(t, 1, files[0].Additions)
+				assert.Equal(t, 1, files[0].Changes)
+				assert.Equal(t, 0, files[0].Deletions)
+				assert.Equal(t, "added", files[0].Status)
+			}
+		})(t)
+	})
+}

From e67b0045357ab2d627f60258d43d50bc8bfdedb9 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Wed, 7 May 2025 11:42:30 -0700
Subject: [PATCH 201/206] upgrade github v61 -> v71 to fix migrating bug
 (#34389)

backport #34385
---
 assets/go-licenses.json       |  4 ++--
 contrib/backport/backport.go  |  2 +-
 go.mod                        |  2 +-
 go.sum                        |  7 +++---
 services/migrations/error.go  |  2 +-
 services/migrations/github.go | 42 ++++++++++++++++++++++-------------
 6 files changed, 35 insertions(+), 24 deletions(-)

diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 6d1b2e16898d4..347e1c43e38d6 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -615,8 +615,8 @@
     "licenseText": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
   },
   {
-    "name": "github.com/google/go-github/v61/github",
-    "path": "github.com/google/go-github/v61/github/LICENSE",
+    "name": "github.com/google/go-github/v71/github",
+    "path": "github.com/google/go-github/v71/github/LICENSE",
     "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
   },
   {
diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go
index eb194374452c5..645030a6fc28a 100644
--- a/contrib/backport/backport.go
+++ b/contrib/backport/backport.go
@@ -17,7 +17,7 @@ import (
 	"strings"
 	"syscall"
 
-	"github.com/google/go-github/v61/github"
+	"github.com/google/go-github/v71/github"
 	"github.com/urfave/cli/v2"
 	"gopkg.in/yaml.v3"
 )
diff --git a/go.mod b/go.mod
index 98de6ec93f6ce..70faeea41a5c5 100644
--- a/go.mod
+++ b/go.mod
@@ -65,7 +65,7 @@ require (
 	github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
 	github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
 	github.com/golang-jwt/jwt/v5 v5.2.2
-	github.com/google/go-github/v61 v61.0.0
+	github.com/google/go-github/v71 v71.0.0
 	github.com/google/licenseclassifier/v2 v2.0.0
 	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db
 	github.com/google/uuid v1.6.0
diff --git a/go.sum b/go.sum
index d8ca43ebbd06d..394eb38d55685 100644
--- a/go.sum
+++ b/go.sum
@@ -410,10 +410,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
-github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
+github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
diff --git a/services/migrations/error.go b/services/migrations/error.go
index c7d912f50be2e..9b470149bf01d 100644
--- a/services/migrations/error.go
+++ b/services/migrations/error.go
@@ -7,7 +7,7 @@ package migrations
 import (
 	"errors"
 
-	"github.com/google/go-github/v61/github"
+	"github.com/google/go-github/v71/github"
 )
 
 // ErrRepoNotCreated returns the error that repository not created
diff --git a/services/migrations/github.go b/services/migrations/github.go
index bbb378b8b29af..82f7cb752bf7e 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -20,7 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/proxy"
 	"code.gitea.io/gitea/modules/structs"
 
-	"github.com/google/go-github/v61/github"
+	"github.com/google/go-github/v71/github"
 	"golang.org/x/oauth2"
 )
 
@@ -448,9 +448,11 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
 		if !g.SkipReactions {
 			for i := 1; ; i++ {
 				g.waitAndPickClient()
-				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
-					Page:    i,
-					PerPage: perPage,
+				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListReactionOptions{
+					ListOptions: github.ListOptions{
+						Page:    i,
+						PerPage: perPage,
+					},
 				})
 				if err != nil {
 					return nil, false, err
@@ -534,9 +536,11 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
 			if !g.SkipReactions {
 				for i := 1; ; i++ {
 					g.waitAndPickClient()
-					res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
-						Page:    i,
-						PerPage: g.maxPerPage,
+					res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
+						ListOptions: github.ListOptions{
+							Page:    i,
+							PerPage: g.maxPerPage,
+						},
 					})
 					if err != nil {
 						return nil, err
@@ -609,9 +613,11 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
 		if !g.SkipReactions {
 			for i := 1; ; i++ {
 				g.waitAndPickClient()
-				res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
-					Page:    i,
-					PerPage: g.maxPerPage,
+				res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListReactionOptions{
+					ListOptions: github.ListOptions{
+						Page:    i,
+						PerPage: g.maxPerPage,
+					},
 				})
 				if err != nil {
 					return nil, false, err
@@ -680,9 +686,11 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
 		if !g.SkipReactions {
 			for i := 1; ; i++ {
 				g.waitAndPickClient()
-				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
-					Page:    i,
-					PerPage: perPage,
+				res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListReactionOptions{
+					ListOptions: github.ListOptions{
+						Page:    i,
+						PerPage: perPage,
+					},
 				})
 				if err != nil {
 					return nil, false, err
@@ -767,9 +775,11 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
 		if !g.SkipReactions {
 			for i := 1; ; i++ {
 				g.waitAndPickClient()
-				res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
-					Page:    i,
-					PerPage: g.maxPerPage,
+				res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListReactionOptions{
+					ListOptions: github.ListOptions{
+						Page:    i,
+						PerPage: g.maxPerPage,
+					},
 				})
 				if err != nil {
 					return nil, err

From 51fa86f0fa4f46ab84f7ff8b8f7660bb8f08b495 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 9 May 2025 20:42:59 +0800
Subject: [PATCH 202/206] Fix a bug when uploading file via lfs ssh command
 1.23 (#34408) (#34411)

Partially backport #34408
---
 routers/private/serv.go               |  8 ++++--
 tests/integration/git_general_test.go | 41 ++++++++++++++++++++++++++-
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/routers/private/serv.go b/routers/private/serv.go
index 12ea01a7e6089..c6153ebb17ed2 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) {
 	ownerName := ctx.PathParam(":owner")
 	repoName := ctx.PathParam(":repo")
 	mode := perm.AccessMode(ctx.FormInt("mode"))
+	verb := ctx.FormString("verb")
 
 	// Set the basic parts of the results to return
 	results := private.ServCommandResults{
@@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) {
 				return
 			}
 		} else {
-			// Because of the special ref "refs/for" we will need to delay write permission check
-			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode {
+			// Because of the special ref "refs/for" (AGit) we will need to delay write permission check,
+			// AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR).
+			// The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go).
+			// Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations.
+			if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == "git-receive-pack" {
 				mode = perm.AccessModeRead
 			}
 
diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go
index 5baf51c6eeebf..03bc8bdab8b4f 100644
--- a/tests/integration/git_general_test.go
+++ b/tests/integration/git_general_test.go
@@ -11,8 +11,10 @@ import (
 	"net/http"
 	"net/url"
 	"os"
+	"os/exec"
 	"path"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"testing"
 	"time"
@@ -31,7 +33,9 @@ import (
 	gitea_context "code.gitea.io/gitea/services/context"
 	"code.gitea.io/gitea/tests"
 
+	"github.com/kballard/go-shellquote"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -105,7 +109,12 @@ func testGitGeneral(t *testing.T, u *url.URL) {
 
 		// Setup key the user ssh key
 		withKeyFile(t, keyname, func(keyFile string) {
-			t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
+			var keyID int64
+			t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile, func(t *testing.T, key api.PublicKey) {
+				keyID = key.ID
+			}))
+			assert.NotZero(t, keyID)
+			t.Run("LFSAccessTest", doSSHLFSAccessTest(sshContext, keyID))
 
 			// Setup remote link
 			// TODO: get url from api
@@ -136,6 +145,36 @@ func testGitGeneral(t *testing.T, u *url.URL) {
 	})
 }
 
+func doSSHLFSAccessTest(_ APITestContext, keyID int64) func(*testing.T) {
+	return func(t *testing.T) {
+		sshCommand := os.Getenv("GIT_SSH_COMMAND")       // it is set in withKeyFile
+		sshCmdParts, err := shellquote.Split(sshCommand) // and parse the ssh command to construct some mocked arguments
+		require.NoError(t, err)
+
+		t.Run("User2AccessOwned", func(t *testing.T) {
+			sshCmdUser2Self := append(slices.Clone(sshCmdParts),
+				"-p", strconv.Itoa(setting.SSH.ListenPort), "git@"+setting.SSH.ListenHost,
+				"git-lfs-authenticate", "user2/repo1.git", "upload", // accessible to own repo
+			)
+			cmd := exec.CommandContext(git.DefaultContext, sshCmdUser2Self[0], sshCmdUser2Self[1:]...)
+			_, err := cmd.Output()
+			assert.NoError(t, err) // accessible, no error
+		})
+
+		t.Run("User2AccessOther", func(t *testing.T) {
+			sshCmdUser2Other := append(slices.Clone(sshCmdParts),
+				"-p", strconv.Itoa(setting.SSH.ListenPort), "git@"+setting.SSH.ListenHost,
+				"git-lfs-authenticate", "user5/repo4.git", "upload", // inaccessible to other's (user5/repo4)
+			)
+			cmd := exec.CommandContext(git.DefaultContext, sshCmdUser2Other[0], sshCmdUser2Other[1:]...)
+			_, err := cmd.Output()
+			var errExit *exec.ExitError
+			require.ErrorAs(t, err, &errExit) // inaccessible, error
+			assert.Contains(t, string(errExit.Stderr), fmt.Sprintf("User: 2:user2 with Key: %d:test-key is not authorized to write to user5/repo4.", keyID))
+		})
+	}
+}
+
 func ensureAnonymousClone(t *testing.T, u *url.URL) {
 	dstLocalPath := t.TempDir()
 	t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))

From ec146b420044d7a57458955a6aa33d9da0dd2373 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Mon, 12 May 2025 21:26:31 +0800
Subject: [PATCH 203/206] Fix bug webhook milestone is not right. (#34419)
 (#34429)

Backport #34419 by @lunny

Fix #34400

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 routers/api/v1/repo/issue.go           |  9 ++++
 routers/api/v1/repo/pull.go            |  5 ++
 routers/web/repo/issue.go              | 10 ++++
 tests/integration/issue_test.go        |  9 ++++
 tests/integration/repo_webhook_test.go | 72 ++++++++++++++++++++++++++
 5 files changed, 105 insertions(+)

diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index cbe709c030db4..da949a56fd2a1 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -895,6 +895,15 @@ func EditIssue(ctx *context.APIContext) {
 		issue.MilestoneID != *form.Milestone {
 		oldMilestoneID := issue.MilestoneID
 		issue.MilestoneID = *form.Milestone
+		if issue.MilestoneID > 0 {
+			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone)
+			if err != nil {
+				ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
+				return
+			}
+		} else {
+			issue.Milestone = nil
+		}
 		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
 			return
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 75db2bd55b484..cb6479e0974d9 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -694,6 +694,11 @@ func EditPullRequest(ctx *context.APIContext) {
 		issue.MilestoneID != form.Milestone {
 		oldMilestoneID := issue.MilestoneID
 		issue.MilestoneID = form.Milestone
+		issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
+		if err != nil {
+			ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
+			return
+		}
 		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
 			return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 5397411b59cc2..93f57b6dc5739 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -418,6 +418,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
 			continue
 		}
 		issue.MilestoneID = milestoneID
+		if milestoneID > 0 {
+			var err error
+			issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
+			if err != nil {
+				ctx.ServerError("GetMilestoneByRepoID", err)
+				return
+			}
+		} else {
+			issue.Milestone = nil
+		}
 		if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
 			ctx.ServerError("ChangeMilestoneAssign", err)
 			return
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
index bd0cedd300baf..065a220406761 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -185,6 +185,15 @@ func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content,
 	return int64(id)
 }
 
+func testIssueChangeMilestone(t *testing.T, session *TestSession, repoLink string, issueID, milestoneID int64) {
+	req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/milestone?issue_ids=%d", issueID), map[string]string{
+		"_csrf": GetUserCSRFToken(t, session),
+		"id":    strconv.FormatInt(milestoneID, 10),
+	})
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	assert.Equal(t, `{"ok":true}`, strings.TrimSpace(resp.Body.String()))
+}
+
 func TestNewIssue(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index 09522639682ce..76565e3c45703 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -353,6 +353,78 @@ func Test_WebhookIssue(t *testing.T) {
 	})
 }
 
+func Test_WebhookIssueMilestone(t *testing.T) {
+	var payloads []api.IssuePayload
+	var triggeredEvent string
+	provider := newMockWebhookProvider(func(r *http.Request) {
+		content, _ := io.ReadAll(r.Body)
+		var payload api.IssuePayload
+		err := json.Unmarshal(content, &payload)
+		assert.NoError(t, err)
+		payloads = append(payloads, payload)
+		triggeredEvent = "issues"
+	}, http.StatusOK)
+	defer provider.Close()
+
+	onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+		// create a new webhook with special webhook for repo1
+		session := loginUser(t, "user2")
+		repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+		testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_milestone")
+
+		t.Run("assign a milestone", func(t *testing.T) {
+			// trigger the webhook
+			testIssueChangeMilestone(t, session, repo1.Link(), 1, 1)
+
+			// validate the webhook is triggered
+			assert.Equal(t, "issues", triggeredEvent)
+			assert.Len(t, payloads, 1)
+			assert.Equal(t, "milestoned", string(payloads[0].Action))
+			assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
+			assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+			assert.Equal(t, "issue1", payloads[0].Issue.Title)
+			assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
+			assert.EqualValues(t, 1, payloads[0].Issue.Milestone.ID)
+		})
+
+		t.Run("change a milestong", func(t *testing.T) {
+			// trigger the webhook again
+			triggeredEvent = ""
+			payloads = make([]api.IssuePayload, 0, 1)
+			// change milestone to 2
+			testIssueChangeMilestone(t, session, repo1.Link(), 1, 2)
+
+			// validate the webhook is triggered
+			assert.Equal(t, "issues", triggeredEvent)
+			assert.Len(t, payloads, 1)
+			assert.Equal(t, "milestoned", string(payloads[0].Action))
+			assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
+			assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+			assert.Equal(t, "issue1", payloads[0].Issue.Title)
+			assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
+			assert.EqualValues(t, 2, payloads[0].Issue.Milestone.ID)
+		})
+
+		t.Run("remove a milestone", func(t *testing.T) {
+			// trigger the webhook again
+			triggeredEvent = ""
+			payloads = make([]api.IssuePayload, 0, 1)
+			// change milestone to 0
+			testIssueChangeMilestone(t, session, repo1.Link(), 1, 0)
+
+			// validate the webhook is triggered
+			assert.Equal(t, "issues", triggeredEvent)
+			assert.Len(t, payloads, 1)
+			assert.Equal(t, "demilestoned", string(payloads[0].Action))
+			assert.Equal(t, "repo1", payloads[0].Issue.Repo.Name)
+			assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName)
+			assert.Equal(t, "issue1", payloads[0].Issue.Title)
+			assert.Equal(t, "content for the first issue", payloads[0].Issue.Body)
+			assert.Nil(t, payloads[0].Issue.Milestone)
+		})
+	})
+}
+
 func Test_WebhookPullRequest(t *testing.T) {
 	var payloads []api.PullRequestPayload
 	var triggeredEvent string

From 3e7cb5b6396edb93c0dbbc0e520cd9a7795514d4 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 12 May 2025 11:47:43 -0700
Subject: [PATCH 204/206] Add changelog for 1.23.8 (#34430)

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 CHANGELOG.md | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 476c9d68e9176..87db42d62292e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,32 @@ 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
 been added to each release, please refer to the [blog](https://blog.gitea.com).
 
+## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
+
+* SECURITY
+  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
+  * Update net package (#34228) (#34232)
+* BUGFIXES
+  * Fix bug webhook milestone is not right. (#34419) #34429
+  * Fix two missed null value checks on the wiki page. (#34205) (#34215)
+  * Swift files can be passed either as file or as form value (#34068) (#34236)
+  * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
+  * Upgrade github v61 -> v71 to fix migrating bug (#34389)
+  * Fix bug when visiting comparation page (#34334) (#34364)
+  * Fix wrong review requests when updating the pull request (#34286) (#34304)
+  * Fix github migration error when using multiple tokens (#34144) (#34302)
+  * Explicitly not update indexes when sync database schemas (#34281) (#34295)
+  * Fix panic when comment is nil (#34257) (#34277)
+  * Fix project board links to related Pull Requests (#34213) (#34222)
+  * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
+* DOCUMENTATION
+  * Update token creation API swagger documentation (#34288) (#34296)
+* MISC
+  * Fix CI Build (#34315)
+  * Add riscv64 support (#34199) (#34204)
+  * Bump go version in go.mod (#34160)
+  * 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
 
 * Enhancements

From cccd54999a6c3b9f2573d2ba20d83d7c4cac77c3 Mon Sep 17 00:00:00 2001
From: Giteabot <teabot@gitea.io>
Date: Tue, 13 May 2025 06:01:45 +0800
Subject: [PATCH 205/206] Fix releases sidebar navigation link (#34436)
 (#34439)

Backport #34436 by @badhezi

Resolves https://github.com/go-gitea/gitea/issues/34435

---------

Co-authored-by: badhezi <zlilaharon@gmail.com>
---
 CHANGELOG.md                            | 1 +
 templates/repo/home_sidebar_bottom.tmpl | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87db42d62292e..635b6534c428e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
   * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
   * Update net package (#34228) (#34232)
 * BUGFIXES
+  * Fix releases sidebar navigation link (#34436) #34439
   * Fix bug webhook milestone is not right. (#34419) #34429
   * Fix two missed null value checks on the wiki page. (#34205) (#34215)
   * Swift files can be passed either as file or as form value (#34068) (#34236)
diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl
index f780dc122d5af..f66faf6d10f76 100644
--- a/templates/repo/home_sidebar_bottom.tmpl
+++ b/templates/repo/home_sidebar_bottom.tmpl
@@ -4,7 +4,7 @@
 		<div class="flex-item">
 			<div class="flex-item-main">
 				<div class="flex-item-title">
-					<a class="item muted" href="{{.Link}}/releases">
+					<a class="item muted" href="{{.RepoLink}}/releases">
 						{{ctx.Locale.Tr "repo.releases"}}
 						<span class="ui small label">{{.NumReleases}}</span>
 					</a>

From e32030f9fa98ff427ecdc157c7fccd10e1fd3d7f Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 19 May 2025 19:47:57 -0700
Subject: [PATCH 206/206] Add migrations tests (#34456) (#34498)

Fix #34455
Backport #34456

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 models/migrations/v1_23/v302.go      |  5 ++-
 models/migrations/v1_23/v302_test.go | 51 ++++++++++++++++++++++++++++
 models/migrations/v1_23/v304.go      |  5 ++-
 models/migrations/v1_23/v304_test.go | 40 ++++++++++++++++++++++
 4 files changed, 99 insertions(+), 2 deletions(-)
 create mode 100644 models/migrations/v1_23/v302_test.go
 create mode 100644 models/migrations/v1_23/v304_test.go

diff --git a/models/migrations/v1_23/v302.go b/models/migrations/v1_23/v302.go
index d7ea03eb3da93..5d2e9b143814a 100644
--- a/models/migrations/v1_23/v302.go
+++ b/models/migrations/v1_23/v302.go
@@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
 		Stopped    timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
 		LogExpired bool               `xorm:"index(stopped_log_expired)"`
 	}
-	return x.Sync(new(ActionTask))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreDropIndices: true,
+	}, new(ActionTask))
+	return err
 }
diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go
new file mode 100644
index 0000000000000..29e85ae9d9ccd
--- /dev/null
+++ b/models/migrations/v1_23/v302_test.go
@@ -0,0 +1,51 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
+	type ActionTask struct {
+		ID       int64
+		JobID    int64
+		Attempt  int64
+		RunnerID int64              `xorm:"index"`
+		Status   int                `xorm:"index"`
+		Started  timeutil.TimeStamp `xorm:"index"`
+		Stopped  timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
+
+		RepoID            int64  `xorm:"index"`
+		OwnerID           int64  `xorm:"index"`
+		CommitSHA         string `xorm:"index"`
+		IsForkPullRequest bool
+
+		Token          string `xorm:"-"`
+		TokenHash      string `xorm:"UNIQUE"` // sha256 of token
+		TokenSalt      string
+		TokenLastEight string `xorm:"index token_last_eight"`
+
+		LogFilename  string  // file name of log
+		LogInStorage bool    // read log from database or from storage
+		LogLength    int64   // lines count
+		LogSize      int64   // blob size
+		LogIndexes   []int64 `xorm:"LONGBLOB"`                   // line number to offset
+		LogExpired   bool    `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
+
+		Created timeutil.TimeStamp `xorm:"created"`
+		Updated timeutil.TimeStamp `xorm:"updated index"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
+	defer deferable()
+
+	assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
+}
diff --git a/models/migrations/v1_23/v304.go b/models/migrations/v1_23/v304.go
index 65cffedbd99be..e108f477799f3 100644
--- a/models/migrations/v1_23/v304.go
+++ b/models/migrations/v1_23/v304.go
@@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
 	type Release struct {
 		Sha1 string `xorm:"INDEX VARCHAR(64)"`
 	}
-	return x.Sync(new(Release))
+	_, err := x.SyncWithOptions(xorm.SyncOptions{
+		IgnoreDropIndices: true,
+	}, new(Release))
+	return err
 }
diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go
new file mode 100644
index 0000000000000..955219d3f97cd
--- /dev/null
+++ b/models/migrations/v1_23/v304_test.go
@@ -0,0 +1,40 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_23 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+	"code.gitea.io/gitea/modules/timeutil"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_AddIndexForReleaseSha1(t *testing.T) {
+	type Release struct {
+		ID               int64  `xorm:"pk autoincr"`
+		RepoID           int64  `xorm:"INDEX UNIQUE(n)"`
+		PublisherID      int64  `xorm:"INDEX"`
+		TagName          string `xorm:"INDEX UNIQUE(n)"`
+		OriginalAuthor   string
+		OriginalAuthorID int64 `xorm:"index"`
+		LowerTagName     string
+		Target           string
+		Title            string
+		Sha1             string `xorm:"VARCHAR(64)"`
+		NumCommits       int64
+		Note             string             `xorm:"TEXT"`
+		IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
+		IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
+		IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
+		CreatedUnix      timeutil.TimeStamp `xorm:"INDEX"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(Release))
+	defer deferable()
+
+	assert.NoError(t, AddIndexForReleaseSha1(x))
+}
