mirror of
https://github.com/go-gitea/gitea
synced 2024-11-14 06:04:25 +00:00
Merge branch 'main' into allow-force-push-protected-branches
This commit is contained in:
commit
945ea90acd
2
.github/workflows/files-changed.yml
vendored
2
.github/workflows/files-changed.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
yaml: ${{ steps.changes.outputs.yaml }}
|
yaml: ${{ steps.changes.outputs.yaml }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v3
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
@ -65,14 +65,17 @@ Recommended implementations:
|
|||||||
|
|
||||||
* Vue + Vanilla JS
|
* Vue + Vanilla JS
|
||||||
* Fomantic-UI (jQuery)
|
* Fomantic-UI (jQuery)
|
||||||
|
* htmx (partial page reloads for otherwise static components)
|
||||||
* Vanilla JS
|
* Vanilla JS
|
||||||
|
|
||||||
Discouraged implementations:
|
Discouraged implementations:
|
||||||
|
|
||||||
* Vue + Fomantic-UI (jQuery)
|
* Vue + Fomantic-UI (jQuery)
|
||||||
* jQuery + Vanilla JS
|
* jQuery + Vanilla JS
|
||||||
|
* htmx + any other framework which requires heavy JS code, or unnecessary features like htmx scripting (`hx-on`)
|
||||||
|
|
||||||
To make UI consistent, Vue components can use Fomantic-UI CSS classes.
|
To make UI consistent, Vue components can use Fomantic-UI CSS classes.
|
||||||
|
We use htmx for simple interactions. You can see an example for simple interactions where htmx should be used in this [PR](https://github.com/go-gitea/gitea/pull/28908). Do not use htmx if you require more advanced reactivity, use another framework (Vue/Vanilla JS).
|
||||||
Although mixing different frameworks is discouraged,
|
Although mixing different frameworks is discouraged,
|
||||||
it should also work if the mixing is necessary and the code is well-designed and maintainable.
|
it should also work if the mixing is necessary and the code is well-designed and maintainable.
|
||||||
|
|
||||||
|
@ -22,4 +22,4 @@ Making the `.profile` repository private will hide the Profile README.
|
|||||||
|
|
||||||
Example of user with `.profile/README.md`:
|
Example of user with `.profile/README.md`:
|
||||||
|
|
||||||
![profile readme screenshot](./profile-readme.png)
|
![profile readme screenshot](/images/usage/profile-readme.png)
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@ -446,11 +446,8 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := db.GetEngine(ctx).Where(cond)
|
sess := db.GetEngine(ctx).Where(cond).
|
||||||
if setting.Database.Type.IsMySQL() {
|
Select("`action`.*"). // this line will avoid select other joined table's columns
|
||||||
sess = sess.IndexHint("USE", "JOIN", "IDX_action_c_u_d")
|
|
||||||
}
|
|
||||||
sess = sess.Select("`action`.*"). // this line will avoid select other joined table's columns
|
|
||||||
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
|
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
|
||||||
|
|
||||||
opts.SetDefaultValues()
|
opts.SetDefaultValues()
|
||||||
|
@ -146,6 +146,41 @@ func DetectWorkflows(
|
|||||||
return workflows, schedules, nil
|
return workflows, schedules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
|
||||||
|
entries, err := ListWorkflows(commit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wfs := make([]*DetectedWorkflow, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
content, err := GetContentFromEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// one workflow may have multiple events
|
||||||
|
events, err := GetEventsFromContent(content)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, evt := range events {
|
||||||
|
if evt.IsSchedule() {
|
||||||
|
log.Trace("detect scheduled workflow: %q", entry.Name())
|
||||||
|
dwf := &DetectedWorkflow{
|
||||||
|
EntryName: entry.Name(),
|
||||||
|
TriggerEvent: evt,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
wfs = append(wfs, dwf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wfs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||||
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
if !canGithubEventMatch(evt.Name, triggedEvent) {
|
||||||
return false
|
return false
|
||||||
|
@ -2013,6 +2013,7 @@ settings.mirror_settings.docs.doc_link_title = How do I mirror repositories?
|
|||||||
settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote repository" section of the documentation.
|
settings.mirror_settings.docs.doc_link_pull_section = the "Pulling from a remote repository" section of the documentation.
|
||||||
settings.mirror_settings.docs.pulling_remote_title = Pulling from a remote repository
|
settings.mirror_settings.docs.pulling_remote_title = Pulling from a remote repository
|
||||||
settings.mirror_settings.mirrored_repository = Mirrored repository
|
settings.mirror_settings.mirrored_repository = Mirrored repository
|
||||||
|
settings.mirror_settings.pushed_repository = Pushed repository
|
||||||
settings.mirror_settings.direction = Direction
|
settings.mirror_settings.direction = Direction
|
||||||
settings.mirror_settings.direction.pull = Pull
|
settings.mirror_settings.direction.pull = Pull
|
||||||
settings.mirror_settings.direction.push = Push
|
settings.mirror_settings.direction.push = Push
|
||||||
|
@ -17,6 +17,7 @@ template=模板
|
|||||||
language=语言选项
|
language=语言选项
|
||||||
notifications=通知
|
notifications=通知
|
||||||
active_stopwatch=活动时间跟踪器
|
active_stopwatch=活动时间跟踪器
|
||||||
|
tracked_time_summary=基于问题列表过滤器的跟踪时间概要
|
||||||
create_new=创建…
|
create_new=创建…
|
||||||
user_profile_and_more=个人信息和配置
|
user_profile_and_more=个人信息和配置
|
||||||
signed_in_as=已登录用户
|
signed_in_as=已登录用户
|
||||||
@ -90,6 +91,7 @@ remove=移除
|
|||||||
remove_all=移除所有
|
remove_all=移除所有
|
||||||
remove_label_str=`删除标签 "%s"`
|
remove_label_str=`删除标签 "%s"`
|
||||||
edit=编辑
|
edit=编辑
|
||||||
|
view=查看
|
||||||
|
|
||||||
enabled=启用
|
enabled=启用
|
||||||
disabled=禁用
|
disabled=禁用
|
||||||
@ -359,6 +361,7 @@ disable_register_prompt=对不起,注册功能已被关闭。请联系网站
|
|||||||
disable_register_mail=已禁用注册的电子邮件确认。
|
disable_register_mail=已禁用注册的电子邮件确认。
|
||||||
manual_activation_only=请联系您的站点管理员来完成激活。
|
manual_activation_only=请联系您的站点管理员来完成激活。
|
||||||
remember_me=记住此设备
|
remember_me=记住此设备
|
||||||
|
remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。
|
||||||
forgot_password_title=忘记密码
|
forgot_password_title=忘记密码
|
||||||
forgot_password=忘记密码?
|
forgot_password=忘记密码?
|
||||||
sign_up_now=还没帐户?马上注册。
|
sign_up_now=还没帐户?马上注册。
|
||||||
@ -862,6 +865,7 @@ revoke_oauth2_grant_description=确定撤销此三方应用程序的授权,并
|
|||||||
revoke_oauth2_grant_success=成功撤销了访问权限。
|
revoke_oauth2_grant_success=成功撤销了访问权限。
|
||||||
|
|
||||||
twofa_desc=两步验证可以加强你的账号安全性。
|
twofa_desc=两步验证可以加强你的账号安全性。
|
||||||
|
twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。
|
||||||
twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。
|
twofa_is_enrolled=你的账号<strong>已启用</strong>了两步验证。
|
||||||
twofa_not_enrolled=你的账号未开启两步验证。
|
twofa_not_enrolled=你的账号未开启两步验证。
|
||||||
twofa_disable=禁用两步认证
|
twofa_disable=禁用两步认证
|
||||||
@ -884,6 +888,8 @@ webauthn_register_key=添加安全密钥
|
|||||||
webauthn_nickname=昵称
|
webauthn_nickname=昵称
|
||||||
webauthn_delete_key=移除安全密钥
|
webauthn_delete_key=移除安全密钥
|
||||||
webauthn_delete_key_desc=如果删除了安全密钥,则不能再使用它登录。继续?
|
webauthn_delete_key_desc=如果删除了安全密钥,则不能再使用它登录。继续?
|
||||||
|
webauthn_key_loss_warning=如果您丢失了您的安全密钥,您将无法访问您的帐户。
|
||||||
|
webauthn_alternative_tip=您可能想要配置额外的身份验证方法。
|
||||||
|
|
||||||
manage_account_links=管理绑定过的账号
|
manage_account_links=管理绑定过的账号
|
||||||
manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。
|
manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。
|
||||||
@ -920,6 +926,7 @@ visibility.private=私有
|
|||||||
visibility.private_tooltip=仅对您已加入的组织的成员可见。
|
visibility.private_tooltip=仅对您已加入的组织的成员可见。
|
||||||
|
|
||||||
[repo]
|
[repo]
|
||||||
|
new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史记录。已经在其他地方托管了?<a href="%s">迁移仓库。</a>
|
||||||
owner=拥有者
|
owner=拥有者
|
||||||
owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。
|
owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。
|
||||||
repo_name=仓库名称
|
repo_name=仓库名称
|
||||||
@ -1782,6 +1789,8 @@ pulls.status_checks_failure=一些检查失败了
|
|||||||
pulls.status_checks_error=一些检查报告了错误
|
pulls.status_checks_error=一些检查报告了错误
|
||||||
pulls.status_checks_requested=必须
|
pulls.status_checks_requested=必须
|
||||||
pulls.status_checks_details=详情
|
pulls.status_checks_details=详情
|
||||||
|
pulls.status_checks_hide_all=隐藏所有检查
|
||||||
|
pulls.status_checks_show_all=显示所有检查
|
||||||
pulls.update_branch=通过合并更新分支
|
pulls.update_branch=通过合并更新分支
|
||||||
pulls.update_branch_rebase=通过变基更新分支
|
pulls.update_branch_rebase=通过变基更新分支
|
||||||
pulls.update_branch_success=分支更新成功
|
pulls.update_branch_success=分支更新成功
|
||||||
@ -1790,6 +1799,11 @@ pulls.outdated_with_base_branch=此分支相比基础分支已过期
|
|||||||
pulls.close=关闭合并请求
|
pulls.close=关闭合并请求
|
||||||
pulls.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此合并请求 `
|
pulls.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此合并请求 `
|
||||||
pulls.reopened_at=`重新打开此合并请求 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
pulls.reopened_at=`重新打开此合并请求 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||||
|
pulls.cmd_instruction_hint=`查看 <a class="show-instruction">命令行提示</a>。`
|
||||||
|
pulls.cmd_instruction_checkout_title=检出
|
||||||
|
pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。
|
||||||
|
pulls.cmd_instruction_merge_title=合并
|
||||||
|
pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上
|
||||||
pulls.clear_merge_message=清除合并信息
|
pulls.clear_merge_message=清除合并信息
|
||||||
pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 git 附加内容,如“Co-Authored-By …”。
|
pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 git 附加内容,如“Co-Authored-By …”。
|
||||||
|
|
||||||
@ -2301,6 +2315,7 @@ settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被
|
|||||||
settings.require_signed_commits=需要签名提交
|
settings.require_signed_commits=需要签名提交
|
||||||
settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
|
settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支
|
||||||
settings.protect_branch_name_pattern=受保护的分支名称模式
|
settings.protect_branch_name_pattern=受保护的分支名称模式
|
||||||
|
settings.protect_branch_name_pattern_desc=分支保护的名称匹配规则。语法请参阅 <a href="github.com/gobwas/glob">文档</a> 。如:main, release/**
|
||||||
settings.protect_patterns=规则
|
settings.protect_patterns=规则
|
||||||
settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔):
|
settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔):
|
||||||
settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件,也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见<a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a>文档了解模式语法。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
|
settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件,也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见<a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a>文档了解模式语法。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>
|
||||||
@ -2365,7 +2380,7 @@ settings.lfs_findcommits=查找提交
|
|||||||
settings.lfs_lfs_file_no_commits=没有找到关于此 LFS 文件的提交
|
settings.lfs_lfs_file_no_commits=没有找到关于此 LFS 文件的提交
|
||||||
settings.lfs_noattribute=此路径在默认分支中没有可锁定的属性
|
settings.lfs_noattribute=此路径在默认分支中没有可锁定的属性
|
||||||
settings.lfs_delete=删除 OID 为 %s 的 LFS 文件
|
settings.lfs_delete=删除 OID 为 %s 的 LFS 文件
|
||||||
settings.lfs_delete_warning=删除一个 LFS 文件可能导致签出时显示'对象不存在'的错误。确定继续吗?
|
settings.lfs_delete_warning=删除一个 LFS 文件可能导致检出时显示'对象不存在'的错误。确定继续吗?
|
||||||
settings.lfs_findpointerfiles=查找指针文件
|
settings.lfs_findpointerfiles=查找指针文件
|
||||||
settings.lfs_locks=锁定
|
settings.lfs_locks=锁定
|
||||||
settings.lfs_invalid_locking_path=无效路径:%s
|
settings.lfs_invalid_locking_path=无效路径:%s
|
||||||
@ -2846,6 +2861,7 @@ emails.updated=电子邮件已更新
|
|||||||
emails.not_updated=无法更新请求的电子邮件地址: %v
|
emails.not_updated=无法更新请求的电子邮件地址: %v
|
||||||
emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。
|
emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。
|
||||||
emails.change_email_header=更新电子邮件属性
|
emails.change_email_header=更新电子邮件属性
|
||||||
|
emails.change_email_text=您确定要更新该电子邮件地址吗?
|
||||||
|
|
||||||
orgs.org_manage_panel=组织管理
|
orgs.org_manage_panel=组织管理
|
||||||
orgs.name=名称
|
orgs.name=名称
|
||||||
@ -2870,6 +2886,7 @@ packages.package_manage_panel=软件包管理
|
|||||||
packages.total_size=总大小:%s
|
packages.total_size=总大小:%s
|
||||||
packages.unreferenced_size=未引用大小: %s
|
packages.unreferenced_size=未引用大小: %s
|
||||||
packages.cleanup=清理过期数据
|
packages.cleanup=清理过期数据
|
||||||
|
packages.cleanup.success=清理过期数据成功
|
||||||
packages.owner=所有者
|
packages.owner=所有者
|
||||||
packages.creator=创建者
|
packages.creator=创建者
|
||||||
packages.name=名称
|
packages.name=名称
|
||||||
@ -3508,12 +3525,17 @@ runs.commit=提交
|
|||||||
runs.scheduled=已计划的
|
runs.scheduled=已计划的
|
||||||
runs.pushed_by=推送者
|
runs.pushed_by=推送者
|
||||||
runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
|
runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s
|
||||||
|
runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s
|
||||||
runs.actor=操作者
|
runs.actor=操作者
|
||||||
runs.status=状态
|
runs.status=状态
|
||||||
runs.actors_no_select=所有操作者
|
runs.actors_no_select=所有操作者
|
||||||
runs.status_no_select=所有状态
|
runs.status_no_select=所有状态
|
||||||
runs.no_results=没有匹配的结果。
|
runs.no_results=没有匹配的结果。
|
||||||
|
runs.no_workflows=目前还没有工作流。
|
||||||
|
runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 <a target="_blank" rel="noopener noreferrer" href="%s">快速启动指南</a>
|
||||||
|
runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。
|
||||||
runs.no_runs=工作流尚未运行过。
|
runs.no_runs=工作流尚未运行过。
|
||||||
|
runs.empty_commit_message=(空白的提交消息)
|
||||||
|
|
||||||
workflow.disable=禁用工作流
|
workflow.disable=禁用工作流
|
||||||
workflow.disable_success=工作流 '%s' 已成功禁用。
|
workflow.disable_success=工作流 '%s' 已成功禁用。
|
||||||
|
@ -203,7 +203,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &api.WikiPage{
|
return &api.WikiPage{
|
||||||
WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
|
WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
|
||||||
ContentBase64: content,
|
ContentBase64: content,
|
||||||
CommitCount: commitsCount,
|
CommitCount: commitsCount,
|
||||||
Sidebar: sidebarContent,
|
Sidebar: sidebarContent,
|
||||||
@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) {
|
|||||||
ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
|
ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pages = append(pages, convert.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
|
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.SetTotalCountHeader(int64(len(entries)))
|
ctx.SetTotalCountHeader(int64(len(entries)))
|
||||||
|
@ -11,14 +11,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoveUsernameParameterSuffix returns the username parameter without the (fullname) suffix - leaving just the username
|
|
||||||
func RemoveUsernameParameterSuffix(name string) string {
|
|
||||||
if index := strings.Index(name, " ("); index >= 0 {
|
|
||||||
name = name[:index]
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SanitizeFlashErrorString will sanitize a flash error string
|
// SanitizeFlashErrorString will sanitize a flash error string
|
||||||
func SanitizeFlashErrorString(x string) string {
|
func SanitizeFlashErrorString(x string) string {
|
||||||
return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
|
return strings.ReplaceAll(html.EscapeString(x), "\n", "<br>")
|
||||||
|
@ -11,12 +11,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRemoveUsernameParameterSuffix(t *testing.T) {
|
|
||||||
assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar (Foo Bar)"))
|
|
||||||
assert.Equal(t, "foobar", RemoveUsernameParameterSuffix("foobar"))
|
|
||||||
assert.Equal(t, "", RemoveUsernameParameterSuffix(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsExternalURL(t *testing.T) {
|
func TestIsExternalURL(t *testing.T) {
|
||||||
setting.AppURL = "https://try.gitea.io/"
|
setting.AppURL = "https://try.gitea.io/"
|
||||||
type test struct {
|
type test struct {
|
||||||
|
@ -24,7 +24,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/utils"
|
|
||||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
@ -127,7 +126,7 @@ func TeamsAction(ctx *context.Context) {
|
|||||||
ctx.Error(http.StatusNotFound)
|
ctx.Error(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
|
uname := strings.ToLower(ctx.FormString("uname"))
|
||||||
var u *user_model.User
|
var u *user_model.User
|
||||||
u, err = user_model.GetUserByName(ctx, uname)
|
u, err = user_model.GetUserByName(ctx, uname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -90,11 +90,10 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
|||||||
|
|
||||||
isWiki := false
|
isWiki := false
|
||||||
unitType := unit.TypeCode
|
unitType := unit.TypeCode
|
||||||
var wikiRepoName string
|
|
||||||
if strings.HasSuffix(reponame, ".wiki") {
|
if strings.HasSuffix(reponame, ".wiki") {
|
||||||
isWiki = true
|
isWiki = true
|
||||||
unitType = unit.TypeWiki
|
unitType = unit.TypeWiki
|
||||||
wikiRepoName = reponame
|
|
||||||
reponame = reponame[:len(reponame)-5]
|
reponame = reponame[:len(reponame)-5]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,16 +106,16 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
|||||||
repoExist := true
|
repoExist := true
|
||||||
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame)
|
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repo_model.IsErrRepoNotExist(err) {
|
if !repo_model.IsErrRepoNotExist(err) {
|
||||||
|
ctx.ServerError("GetRepositoryByName", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
|
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
|
||||||
context.RedirectToRepo(ctx.Base, redirectRepoID)
|
context.RedirectToRepo(ctx.Base, redirectRepoID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
repoExist = false
|
repoExist = false
|
||||||
} else {
|
|
||||||
ctx.ServerError("GetRepositoryByName", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow pushing if the repo is archived
|
// Don't allow pushing if the repo is archived
|
||||||
@ -292,22 +291,9 @@ func httpBase(ctx *context.Context) *serviceHandler {
|
|||||||
|
|
||||||
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
|
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
|
||||||
|
|
||||||
w := ctx.Resp
|
ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
|
||||||
r := ctx.Req
|
|
||||||
cfg := &serviceConfig{
|
|
||||||
UploadPack: true,
|
|
||||||
ReceivePack: true,
|
|
||||||
Env: environ,
|
|
||||||
}
|
|
||||||
|
|
||||||
r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name
|
return &serviceHandler{repo, isWiki, environ}
|
||||||
|
|
||||||
dir := repo_model.RepoPath(username, reponame)
|
|
||||||
if isWiki {
|
|
||||||
dir = repo_model.RepoPath(username, wikiRepoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &serviceHandler{cfg, w, r, dir, cfg.Env}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -352,32 +338,31 @@ func dummyInfoRefs(ctx *context.Context) {
|
|||||||
_, _ = ctx.Write(infoRefsCache)
|
_, _ = ctx.Write(infoRefsCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceConfig struct {
|
|
||||||
UploadPack bool
|
|
||||||
ReceivePack bool
|
|
||||||
Env []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type serviceHandler struct {
|
type serviceHandler struct {
|
||||||
cfg *serviceConfig
|
repo *repo_model.Repository
|
||||||
w http.ResponseWriter
|
isWiki bool
|
||||||
r *http.Request
|
|
||||||
dir string
|
|
||||||
environ []string
|
environ []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *serviceHandler) setHeaderNoCache() {
|
func (h *serviceHandler) getRepoDir() string {
|
||||||
h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
|
if h.isWiki {
|
||||||
h.w.Header().Set("Pragma", "no-cache")
|
return h.repo.WikiPath()
|
||||||
h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
}
|
||||||
|
return h.repo.RepoPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *serviceHandler) setHeaderCacheForever() {
|
func setHeaderNoCache(ctx *context.Context) {
|
||||||
|
ctx.Resp.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")
|
||||||
|
ctx.Resp.Header().Set("Pragma", "no-cache")
|
||||||
|
ctx.Resp.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHeaderCacheForever(ctx *context.Context) {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expires := now + 31536000
|
expires := now + 31536000
|
||||||
h.w.Header().Set("Date", fmt.Sprintf("%d", now))
|
ctx.Resp.Header().Set("Date", fmt.Sprintf("%d", now))
|
||||||
h.w.Header().Set("Expires", fmt.Sprintf("%d", expires))
|
ctx.Resp.Header().Set("Expires", fmt.Sprintf("%d", expires))
|
||||||
h.w.Header().Set("Cache-Control", "public, max-age=31536000")
|
ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsParentDirectorySeparator(v string) bool {
|
func containsParentDirectorySeparator(v string) bool {
|
||||||
@ -394,71 +379,71 @@ func containsParentDirectorySeparator(v string) bool {
|
|||||||
|
|
||||||
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
|
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
|
||||||
|
|
||||||
func (h *serviceHandler) sendFile(contentType, file string) {
|
func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
|
||||||
if containsParentDirectorySeparator(file) {
|
if containsParentDirectorySeparator(file) {
|
||||||
log.Error("request file path contains invalid path: %v", file)
|
log.Error("request file path contains invalid path: %v", file)
|
||||||
h.w.WriteHeader(http.StatusBadRequest)
|
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reqFile := path.Join(h.dir, file)
|
reqFile := filepath.Join(h.getRepoDir(), file)
|
||||||
|
|
||||||
fi, err := os.Stat(reqFile)
|
fi, err := os.Stat(reqFile)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
h.w.WriteHeader(http.StatusNotFound)
|
ctx.Resp.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.w.Header().Set("Content-Type", contentType)
|
ctx.Resp.Header().Set("Content-Type", contentType)
|
||||||
h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
||||||
h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
|
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
|
||||||
http.ServeFile(h.w, h.r, reqFile)
|
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// one or more key=value pairs separated by colons
|
// one or more key=value pairs separated by colons
|
||||||
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
|
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
|
||||||
|
|
||||||
func prepareGitCmdWithAllowedService(service string, h *serviceHandler) (*git.Command, error) {
|
func prepareGitCmdWithAllowedService(ctx *context.Context, service string) (*git.Command, error) {
|
||||||
if service == "receive-pack" && h.cfg.ReceivePack {
|
if service == "receive-pack" {
|
||||||
return git.NewCommand(h.r.Context(), "receive-pack"), nil
|
return git.NewCommand(ctx, "receive-pack"), nil
|
||||||
}
|
}
|
||||||
if service == "upload-pack" && h.cfg.UploadPack {
|
if service == "upload-pack" {
|
||||||
return git.NewCommand(h.r.Context(), "upload-pack"), nil
|
return git.NewCommand(ctx, "upload-pack"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("service %q is not allowed", service)
|
return nil, fmt.Errorf("service %q is not allowed", service)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceRPC(h *serviceHandler, service string) {
|
func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := h.r.Body.Close(); err != nil {
|
if err := ctx.Req.Body.Close(); err != nil {
|
||||||
log.Error("serviceRPC: Close: %v", err)
|
log.Error("serviceRPC: Close: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
|
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
|
||||||
if h.r.Header.Get("Content-Type") != expectedContentType {
|
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
|
||||||
log.Error("Content-Type (%q) doesn't match expected: %q", h.r.Header.Get("Content-Type"), expectedContentType)
|
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
|
||||||
h.w.WriteHeader(http.StatusUnauthorized)
|
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, err := prepareGitCmdWithAllowedService(service, h)
|
cmd, err := prepareGitCmdWithAllowedService(ctx, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to prepareGitCmdWithService: %v", err)
|
log.Error("Failed to prepareGitCmdWithService: %v", err)
|
||||||
h.w.WriteHeader(http.StatusUnauthorized)
|
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
||||||
|
|
||||||
reqBody := h.r.Body
|
reqBody := ctx.Req.Body
|
||||||
|
|
||||||
// Handle GZIP.
|
// Handle GZIP.
|
||||||
if h.r.Header.Get("Content-Encoding") == "gzip" {
|
if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
|
||||||
reqBody, err = gzip.NewReader(reqBody)
|
reqBody, err = gzip.NewReader(reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Fail to create gzip reader: %v", err)
|
log.Error("Fail to create gzip reader: %v", err)
|
||||||
h.w.WriteHeader(http.StatusInternalServerError)
|
ctx.Resp.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,23 +451,23 @@ func serviceRPC(h *serviceHandler, service string) {
|
|||||||
// set this for allow pre-receive and post-receive execute
|
// set this for allow pre-receive and post-receive execute
|
||||||
h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
|
h.environ = append(h.environ, "SSH_ORIGINAL_COMMAND="+service)
|
||||||
|
|
||||||
if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
||||||
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
|
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.dir)
|
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
|
||||||
cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir))
|
cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.getRepoDir()))
|
||||||
if err := cmd.Run(&git.RunOpts{
|
if err := cmd.Run(&git.RunOpts{
|
||||||
Dir: h.dir,
|
Dir: h.getRepoDir(),
|
||||||
Env: append(os.Environ(), h.environ...),
|
Env: append(os.Environ(), h.environ...),
|
||||||
Stdout: h.w,
|
Stdout: ctx.Resp,
|
||||||
Stdin: reqBody,
|
Stdin: reqBody,
|
||||||
Stderr: &stderr,
|
Stderr: &stderr,
|
||||||
UseContextTimeout: true,
|
UseContextTimeout: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if err.Error() != "signal: killed" {
|
if err.Error() != "signal: killed" {
|
||||||
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
|
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -492,7 +477,7 @@ func serviceRPC(h *serviceHandler, service string) {
|
|||||||
func ServiceUploadPack(ctx *context.Context) {
|
func ServiceUploadPack(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
serviceRPC(h, "upload-pack")
|
serviceRPC(ctx, h, "upload-pack")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,12 +485,12 @@ func ServiceUploadPack(ctx *context.Context) {
|
|||||||
func ServiceReceivePack(ctx *context.Context) {
|
func ServiceReceivePack(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
serviceRPC(h, "receive-pack")
|
serviceRPC(ctx, h, "receive-pack")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServiceType(r *http.Request) string {
|
func getServiceType(ctx *context.Context) string {
|
||||||
serviceType := r.FormValue("service")
|
serviceType := ctx.Req.FormValue("service")
|
||||||
if !strings.HasPrefix(serviceType, "git-") {
|
if !strings.HasPrefix(serviceType, "git-") {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -534,28 +519,28 @@ func GetInfoRefs(ctx *context.Context) {
|
|||||||
if h == nil {
|
if h == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.setHeaderNoCache()
|
setHeaderNoCache(ctx)
|
||||||
service := getServiceType(h.r)
|
service := getServiceType(ctx)
|
||||||
cmd, err := prepareGitCmdWithAllowedService(service, h)
|
cmd, err := prepareGitCmdWithAllowedService(ctx, service)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if protocol := h.r.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
|
||||||
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
|
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
|
||||||
}
|
}
|
||||||
h.environ = append(os.Environ(), h.environ...)
|
h.environ = append(os.Environ(), h.environ...)
|
||||||
|
|
||||||
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.dir})
|
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
|
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
|
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
|
||||||
h.w.WriteHeader(http.StatusOK)
|
ctx.Resp.WriteHeader(http.StatusOK)
|
||||||
_, _ = h.w.Write(packetWrite("# service=git-" + service + "\n"))
|
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
|
||||||
_, _ = h.w.Write([]byte("0000"))
|
_, _ = ctx.Resp.Write([]byte("0000"))
|
||||||
_, _ = h.w.Write(refs)
|
_, _ = ctx.Resp.Write(refs)
|
||||||
} else {
|
} else {
|
||||||
updateServerInfo(ctx, h.dir)
|
updateServerInfo(ctx, h.getRepoDir())
|
||||||
h.sendFile("text/plain; charset=utf-8", "info/refs")
|
h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,12 +549,12 @@ func GetTextFile(p string) func(*context.Context) {
|
|||||||
return func(ctx *context.Context) {
|
return func(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.setHeaderNoCache()
|
setHeaderNoCache(ctx)
|
||||||
file := ctx.Params("file")
|
file := ctx.Params("file")
|
||||||
if file != "" {
|
if file != "" {
|
||||||
h.sendFile("text/plain", "objects/info/"+file)
|
h.sendFile(ctx, "text/plain", "objects/info/"+file)
|
||||||
} else {
|
} else {
|
||||||
h.sendFile("text/plain", p)
|
h.sendFile(ctx, "text/plain", p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -579,8 +564,8 @@ func GetTextFile(p string) func(*context.Context) {
|
|||||||
func GetInfoPacks(ctx *context.Context) {
|
func GetInfoPacks(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.setHeaderCacheForever()
|
setHeaderCacheForever(ctx)
|
||||||
h.sendFile("text/plain; charset=utf-8", "objects/info/packs")
|
h.sendFile(ctx, "text/plain; charset=utf-8", "objects/info/packs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,8 +573,8 @@ func GetInfoPacks(ctx *context.Context) {
|
|||||||
func GetLooseObject(ctx *context.Context) {
|
func GetLooseObject(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.setHeaderCacheForever()
|
setHeaderCacheForever(ctx)
|
||||||
h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
|
h.sendFile(ctx, "application/x-git-loose-object", fmt.Sprintf("objects/%s/%s",
|
||||||
ctx.Params("head"), ctx.Params("hash")))
|
ctx.Params("head"), ctx.Params("hash")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,8 +583,8 @@ func GetLooseObject(ctx *context.Context) {
|
|||||||
func GetPackFile(ctx *context.Context) {
|
func GetPackFile(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.setHeaderCacheForever()
|
setHeaderCacheForever(ctx)
|
||||||
h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
|
h.sendFile(ctx, "application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,7 +592,7 @@ func GetPackFile(ctx *context.Context) {
|
|||||||
func GetIdxFile(ctx *context.Context) {
|
func GetIdxFile(ctx *context.Context) {
|
||||||
h := httpBase(ctx)
|
h := httpBase(ctx)
|
||||||
if h != nil {
|
if h != nil {
|
||||||
h.setHeaderCacheForever()
|
setHeaderCacheForever(ctx)
|
||||||
h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
|
h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/routers/utils"
|
|
||||||
"code.gitea.io/gitea/services/mailer"
|
"code.gitea.io/gitea/services/mailer"
|
||||||
org_service "code.gitea.io/gitea/services/org"
|
org_service "code.gitea.io/gitea/services/org"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
@ -52,7 +51,7 @@ func Collaboration(ctx *context.Context) {
|
|||||||
|
|
||||||
// CollaborationPost response for actions for a collaboration of a repository
|
// CollaborationPost response for actions for a collaboration of a repository
|
||||||
func CollaborationPost(ctx *context.Context) {
|
func CollaborationPost(ctx *context.Context) {
|
||||||
name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("collaborator")))
|
name := strings.ToLower(ctx.FormString("collaborator"))
|
||||||
if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
|
if len(name) == 0 || ctx.Repo.Owner.LowerName == name {
|
||||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||||
return
|
return
|
||||||
@ -144,7 +143,7 @@ func AddTeamPost(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("team")))
|
name := strings.ToLower(ctx.FormString("team"))
|
||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
|
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration")
|
||||||
return
|
return
|
||||||
|
@ -474,3 +474,35 @@ func handleSchedules(
|
|||||||
|
|
||||||
return actions_model.CreateScheduleTask(ctx, crons)
|
return actions_model.CreateScheduleTask(ctx, crons)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetectAndHandleSchedules detects the schedule workflows on the default branch and create schedule tasks
|
||||||
|
func DetectAndHandleSchedules(ctx context.Context, repo *repo_model.Repository) error {
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(context.Background(), repo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git.OpenRepository: %w", err)
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
// Only detect schedule workflows on the default branch
|
||||||
|
commit, err := gitRepo.GetCommit(repo.DefaultBranch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
||||||
|
}
|
||||||
|
scheduleWorkflows, err := actions_module.DetectScheduledWorkflows(gitRepo, commit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("detect schedule workflows: %w", err)
|
||||||
|
}
|
||||||
|
if len(scheduleWorkflows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need a notifyInput to call handleSchedules
|
||||||
|
// Here we use the commit author as the Doer of the notifyInput
|
||||||
|
commitUser, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get user by email: %w", err)
|
||||||
|
}
|
||||||
|
notifyInput := newNotifyInput(repo, commitUser, webhook_module.HookEventSchedule)
|
||||||
|
|
||||||
|
return handleSchedules(ctx, scheduleWorkflows, commit, notifyInput, repo.DefaultBranch)
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
@ -65,8 +66,15 @@ func startTasks(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := row.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions)
|
||||||
if cfg.IsWorkflowDisabled(row.Schedule.WorkflowID) {
|
if err != nil {
|
||||||
|
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||||
|
// Skip the actions unit of this repo is disabled.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("GetUnit: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,11 +6,8 @@ package convert
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
wiki_service "code.gitea.io/gitea/services/wiki"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToWikiCommit convert a git commit into a WikiCommit
|
// ToWikiCommit convert a git commit into a WikiCommit
|
||||||
@ -46,15 +43,3 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList {
|
|||||||
Count: total,
|
Count: total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
|
||||||
func ToWikiPageMetaData(wikiName wiki_service.WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
|
|
||||||
subURL := string(wikiName)
|
|
||||||
_, title := wiki_service.WebPathToUserTitle(wikiName)
|
|
||||||
return &api.WikiPageMetaData{
|
|
||||||
Title: title,
|
|
||||||
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL),
|
|
||||||
SubURL: subURL,
|
|
||||||
LastCommit: ToWikiCommit(lastCommit),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
actions_service "code.gitea.io/gitea/services/actions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateRepositoryUnits updates a repository's units
|
// UpdateRepositoryUnits updates a repository's units
|
||||||
@ -33,6 +34,15 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, u := range units {
|
||||||
|
if u.Type == unit.TypeActions {
|
||||||
|
if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
|
||||||
|
log.Error("DetectAndHandleSchedules: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil {
|
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(repo_model.RepoUnit)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/convert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// To define the wiki related concepts:
|
// To define the wiki related concepts:
|
||||||
@ -155,3 +158,15 @@ func UserTitleToWebPath(base, title string) WebPath {
|
|||||||
}
|
}
|
||||||
return WebPath(title)
|
return WebPath(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
||||||
|
func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
|
||||||
|
subURL := string(wikiName)
|
||||||
|
_, title := WebPathToUserTitle(wikiName)
|
||||||
|
return &api.WikiPageMetaData{
|
||||||
|
Title: title,
|
||||||
|
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL),
|
||||||
|
SubURL: subURL,
|
||||||
|
LastCommit: convert.ToWikiCommit(lastCommit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
{{else if .IsPDFFile}}
|
{{else if .IsPDFFile}}
|
||||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
|
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="btn btn-gray btn-radius">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .FileSize}}
|
{{else if .FileSize}}
|
||||||
|
@ -105,8 +105,9 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
{{ctx.Locale.Tr "repo.settings.mirror_settings.docs.no_new_mirrors"}} {{ctx.Locale.Tr "repo.settings.mirror_settings.docs.can_still_use"}}<br>
|
{{ctx.Locale.Tr "repo.settings.mirror_settings.docs.no_new_mirrors"}} {{ctx.Locale.Tr "repo.settings.mirror_settings.docs.can_still_use"}}<br>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Repository.IsMirror}}
|
||||||
<table class="ui table">
|
<table class="ui table">
|
||||||
{{if $existingPushMirror}}
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
|
<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</th>
|
||||||
@ -200,8 +201,18 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<thead><tr><th colspan="4"></th></tr></thead>
|
</table>
|
||||||
{{end}}{{/* end if: IsMirror */}}
|
{{end}}{{/* end if: IsMirror */}}
|
||||||
|
|
||||||
|
<table class="ui table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:40%">{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}</th>
|
||||||
|
<th>{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .PushMirrors}}
|
{{range .PushMirrors}}
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{if .EscapeStatus}}
|
{{if .EscapeStatus}}
|
||||||
{{if .EscapeStatus.HasInvisible}}
|
{{if .EscapeStatus.HasInvisible}}
|
||||||
<div class="ui warning message unicode-escape-prompt gt-text-left">
|
<div class="ui warning message unicode-escape-prompt gt-text-left">
|
||||||
<button class="close icon hide-panel button" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
|
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
|
||||||
</div>
|
</div>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else if .EscapeStatus.HasAmbiguous}}
|
{{else if .EscapeStatus.HasAmbiguous}}
|
||||||
<div class="ui warning message unicode-escape-prompt gt-text-left">
|
<div class="ui warning message unicode-escape-prompt gt-text-left">
|
||||||
<button class="close icon hide-panel button" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
<button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
|
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
{{else if .IsPDFFile}}
|
{{else if .IsPDFFile}}
|
||||||
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
<div class="pdf-content is-loading" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="btn btn-gray btn-radius">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .FileSize}}
|
{{else if .FileSize}}
|
||||||
|
@ -196,10 +196,14 @@ a.label,
|
|||||||
.ui.search > .results {
|
.ui.search > .results {
|
||||||
background: var(--color-body);
|
background: var(--color-body);
|
||||||
border-color: var(--color-secondary);
|
border-color: var(--color-secondary);
|
||||||
|
overflow-wrap: anywhere; /* allow text to wrap as fomantic limits this to 18em width */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.search > .results .result {
|
.ui.search > .results .result {
|
||||||
background: var(--color-body);
|
background: var(--color-body);
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.search > .results .result .title {
|
.ui.search > .results .result .title {
|
||||||
|
@ -2128,14 +2128,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#search-user-box .results .result .image {
|
#search-user-box .results .result .image {
|
||||||
float: left;
|
order: 0;
|
||||||
margin-right: 8px;
|
margin-right: 12px;
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
|
min-width: 2em;
|
||||||
|
min-height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-user-box .results .result .content {
|
#search-user-box .results .result .content {
|
||||||
margin: 6px 0; /* this trick is used to align with the sibling avatar image */
|
margin: 0; /* remove margin reserved for avatar because we move it to left via `order: 0` */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.menu .item > img:not(.ui) {
|
.ui.menu .item > img:not(.ui) {
|
||||||
|
@ -17,14 +17,13 @@ export function initCompSearchUserBox() {
|
|||||||
const searchQuery = $searchUserBox.find('input').val();
|
const searchQuery = $searchUserBox.find('input').val();
|
||||||
const searchQueryUppercase = searchQuery.toUpperCase();
|
const searchQueryUppercase = searchQuery.toUpperCase();
|
||||||
$.each(response.data, (_i, item) => {
|
$.each(response.data, (_i, item) => {
|
||||||
let title = item.login;
|
|
||||||
if (item.full_name && item.full_name.length > 0) {
|
|
||||||
title += ` (${htmlEscape(item.full_name)})`;
|
|
||||||
}
|
|
||||||
const resultItem = {
|
const resultItem = {
|
||||||
title,
|
title: item.login,
|
||||||
image: item.avatar_url
|
image: item.avatar_url
|
||||||
};
|
};
|
||||||
|
if (item.full_name) {
|
||||||
|
resultItem.description = htmlEscape(item.full_name);
|
||||||
|
}
|
||||||
if (searchQueryUppercase === item.login.toUpperCase()) {
|
if (searchQueryUppercase === item.login.toUpperCase()) {
|
||||||
items.unshift(resultItem);
|
items.unshift(resultItem);
|
||||||
} else {
|
} else {
|
||||||
|
@ -52,9 +52,9 @@ export function initRepoSettingSearchTeamBox() {
|
|||||||
onResponse(response) {
|
onResponse(response) {
|
||||||
const items = [];
|
const items = [];
|
||||||
$.each(response.data, (_i, item) => {
|
$.each(response.data, (_i, item) => {
|
||||||
const title = `${item.name} (${item.permission} access)`;
|
|
||||||
items.push({
|
items.push({
|
||||||
title,
|
title: item.name,
|
||||||
|
description: `${item.permission} access` // TODO: translate this string
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,8 +12,10 @@ export function renderCodeCopy() {
|
|||||||
if (!els.length) return;
|
if (!els.length) return;
|
||||||
|
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
|
if (!el.textContent) continue;
|
||||||
const btn = makeCodeCopyButton();
|
const btn = makeCodeCopyButton();
|
||||||
btn.setAttribute('data-clipboard-text', el.textContent);
|
// remove final trailing newline introduced during HTML rendering
|
||||||
|
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
|
||||||
el.after(btn);
|
el.after(btn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user