1
1
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:
Henry Goodman 2024-02-02 12:21:35 +11:00 committed by GitHub
commit 945ea90acd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 260 additions and 165 deletions

View File

@ -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: |

View File

@ -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.

View File

@ -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)

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -446,12 +446,9 @@ 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") Join("INNER", "repository", "`repository`.id = `action`.repo_id")
}
sess = sess.Select("`action`.*"). // this line will avoid select other joined table's columns
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
opts.SetDefaultValues() opts.SetDefaultValues()
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)

View File

@ -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

View File

@ -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

View File

@ -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' 已成功禁用。

View File

@ -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)))

View File

@ -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>")

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil
}
repoExist = false
} else {
ctx.ServerError("GetRepositoryByName", err) ctx.ServerError("GetRepositoryByName", err)
return nil return nil
} }
if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil
}
repoExist = false
} }
// 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")
} }
} }

View File

@ -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

View File

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

View File

@ -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
} }

View File

@ -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),
}
}

View File

@ -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
} }

View File

@ -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),
}
}

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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
}); });
}); });

View File

@ -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);
} }
} }