diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index e7039053af..c909f78597 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -35,7 +35,7 @@ jobs: yaml: ${{ steps.changes.outputs.yaml }} steps: - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: changes with: filters: | diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index aa1759d9c9..edd89e1231 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -65,14 +65,17 @@ Recommended implementations: * Vue + Vanilla JS * Fomantic-UI (jQuery) +* htmx (partial page reloads for otherwise static components) * Vanilla JS Discouraged implementations: * Vue + Fomantic-UI (jQuery) * 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. +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, it should also work if the mixing is necessary and the code is well-designed and maintainable. diff --git a/docs/content/usage/profile-readme.en-us.md b/docs/content/usage/profile-readme.en-us.md index fe42fa2723..316a735a1c 100644 --- a/docs/content/usage/profile-readme.en-us.md +++ b/docs/content/usage/profile-readme.en-us.md @@ -22,4 +22,4 @@ Making the `.profile` repository private will hide the Profile README. Example of user with `.profile/README.md`: -![profile readme screenshot](./profile-readme.png) +![profile readme screenshot](/images/usage/profile-readme.png) diff --git a/docs/content/usage/profile-readme.png b/docs/static/images/usage/profile-readme.png similarity index 100% rename from docs/content/usage/profile-readme.png rename to docs/static/images/usage/profile-readme.png diff --git a/models/activities/action.go b/models/activities/action.go index c9745e4a8a..15bd9a52ac 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -446,12 +446,9 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, err } - sess := db.GetEngine(ctx).Where(cond) - if setting.Database.Type.IsMySQL() { - 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") + sess := db.GetEngine(ctx).Where(cond). + Select("`action`.*"). // this line will avoid select other joined table's columns + Join("INNER", "repository", "`repository`.id = `action`.repo_id") opts.SetDefaultValues() sess = db.SetSessionPagination(sess, &opts) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index cbc7e011d1..a883f4181b 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -146,6 +146,41 @@ func DetectWorkflows( 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 { if !canGithubEventMatch(evt.Name, triggedEvent) { return false diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 92a4fcab9c..59f0d8c880 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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.pulling_remote_title = Pulling from a remote repository settings.mirror_settings.mirrored_repository = Mirrored repository +settings.mirror_settings.pushed_repository = Pushed repository settings.mirror_settings.direction = Direction settings.mirror_settings.direction.pull = Pull settings.mirror_settings.direction.push = Push diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 77414f1570..faa1215d5b 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -17,6 +17,7 @@ template=模板 language=语言选项 notifications=通知 active_stopwatch=活动时间跟踪器 +tracked_time_summary=基于问题列表过滤器的跟踪时间概要 create_new=创建… user_profile_and_more=个人信息和配置 signed_in_as=已登录用户 @@ -90,6 +91,7 @@ remove=移除 remove_all=移除所有 remove_label_str=`删除标签 "%s"` edit=编辑 +view=查看 enabled=启用 disabled=禁用 @@ -359,6 +361,7 @@ disable_register_prompt=对不起,注册功能已被关闭。请联系网站 disable_register_mail=已禁用注册的电子邮件确认。 manual_activation_only=请联系您的站点管理员来完成激活。 remember_me=记住此设备 +remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 forgot_password_title=忘记密码 forgot_password=忘记密码? sign_up_now=还没帐户?马上注册。 @@ -862,6 +865,7 @@ revoke_oauth2_grant_description=确定撤销此三方应用程序的授权,并 revoke_oauth2_grant_success=成功撤销了访问权限。 twofa_desc=两步验证可以加强你的账号安全性。 +twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。 twofa_is_enrolled=你的账号已启用了两步验证。 twofa_not_enrolled=你的账号未开启两步验证。 twofa_disable=禁用两步认证 @@ -884,6 +888,8 @@ webauthn_register_key=添加安全密钥 webauthn_nickname=昵称 webauthn_delete_key=移除安全密钥 webauthn_delete_key_desc=如果删除了安全密钥,则不能再使用它登录。继续? +webauthn_key_loss_warning=如果您丢失了您的安全密钥,您将无法访问您的帐户。 +webauthn_alternative_tip=您可能想要配置额外的身份验证方法。 manage_account_links=管理绑定过的账号 manage_account_links_desc=这些外部帐户已经绑定到您的 Gitea 帐户。 @@ -920,6 +926,7 @@ visibility.private=私有 visibility.private_tooltip=仅对您已加入的组织的成员可见。 [repo] +new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史记录。已经在其他地方托管了?迁移仓库。 owner=拥有者 owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。 repo_name=仓库名称 @@ -1782,6 +1789,8 @@ pulls.status_checks_failure=一些检查失败了 pulls.status_checks_error=一些检查报告了错误 pulls.status_checks_requested=必须 pulls.status_checks_details=详情 +pulls.status_checks_hide_all=隐藏所有检查 +pulls.status_checks_show_all=显示所有检查 pulls.update_branch=通过合并更新分支 pulls.update_branch_rebase=通过变基更新分支 pulls.update_branch_success=分支更新成功 @@ -1790,6 +1799,11 @@ pulls.outdated_with_base_branch=此分支相比基础分支已过期 pulls.close=关闭合并请求 pulls.closed_at=`于 %[2]s 关闭此合并请求 ` pulls.reopened_at=`重新打开此合并请求 %[2]s` +pulls.cmd_instruction_hint=`查看 命令行提示。` +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_hint=清除合并消息只会删除提交消息内容,并保留生成的 git 附加内容,如“Co-Authored-By …”。 @@ -2301,6 +2315,7 @@ settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被 settings.require_signed_commits=需要签名提交 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支 settings.protect_branch_name_pattern=受保护的分支名称模式 +settings.protect_branch_name_pattern_desc=分支保护的名称匹配规则。语法请参阅 文档 。如:main, release/** settings.protect_patterns=规则 settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔): settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件,也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见github.com/gobwas/glob文档了解模式语法。例如: .drone.yml, /docs/**/*.txt @@ -2365,7 +2380,7 @@ settings.lfs_findcommits=查找提交 settings.lfs_lfs_file_no_commits=没有找到关于此 LFS 文件的提交 settings.lfs_noattribute=此路径在默认分支中没有可锁定的属性 settings.lfs_delete=删除 OID 为 %s 的 LFS 文件 -settings.lfs_delete_warning=删除一个 LFS 文件可能导致签出时显示'对象不存在'的错误。确定继续吗? +settings.lfs_delete_warning=删除一个 LFS 文件可能导致检出时显示'对象不存在'的错误。确定继续吗? settings.lfs_findpointerfiles=查找指针文件 settings.lfs_locks=锁定 settings.lfs_invalid_locking_path=无效路径:%s @@ -2846,6 +2861,7 @@ emails.updated=电子邮件已更新 emails.not_updated=无法更新请求的电子邮件地址: %v emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。 emails.change_email_header=更新电子邮件属性 +emails.change_email_text=您确定要更新该电子邮件地址吗? orgs.org_manage_panel=组织管理 orgs.name=名称 @@ -2870,6 +2886,7 @@ packages.package_manage_panel=软件包管理 packages.total_size=总大小:%s packages.unreferenced_size=未引用大小: %s packages.cleanup=清理过期数据 +packages.cleanup.success=清理过期数据成功 packages.owner=所有者 packages.creator=创建者 packages.name=名称 @@ -3508,12 +3525,17 @@ runs.commit=提交 runs.scheduled=已计划的 runs.pushed_by=推送者 runs.invalid_workflow_helper=工作流配置文件无效。请检查您的配置文件: %s +runs.no_matching_online_runner_helper=没有匹配标签的在线 runner: %s runs.actor=操作者 runs.status=状态 runs.actors_no_select=所有操作者 runs.status_no_select=所有状态 runs.no_results=没有匹配的结果。 +runs.no_workflows=目前还没有工作流。 +runs.no_workflows.quick_start=不知道如何启动Gitea Action?请参阅 快速启动指南 +runs.no_workflows.documentation=更多有关 Gitea Action 的信息,请访问 文档。 runs.no_runs=工作流尚未运行过。 +runs.empty_commit_message=(空白的提交消息) workflow.disable=禁用工作流 workflow.disable_success=工作流 '%s' 已成功禁用。 diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index ba3e978a83..4f27500496 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -203,7 +203,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } return &api.WikiPage{ - WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), + WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), ContentBase64: content, CommitCount: commitsCount, Sidebar: sidebarContent, @@ -333,7 +333,7 @@ func ListWikiPages(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err) 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))) diff --git a/routers/utils/utils.go b/routers/utils/utils.go index d6856fceac..1f4d11fd3c 100644 --- a/routers/utils/utils.go +++ b/routers/utils/utils.go @@ -11,14 +11,6 @@ import ( "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 func SanitizeFlashErrorString(x string) string { return strings.ReplaceAll(html.EscapeString(x), "\n", "
") diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index 6d19214c88..440aad87c6 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -11,12 +11,6 @@ import ( "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) { setting.AppURL = "https://try.gitea.io/" type test struct { diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 9e65c8ba9c..71fe99c97c 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -24,7 +24,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" @@ -127,7 +126,7 @@ func TeamsAction(ctx *context.Context) { ctx.Error(http.StatusNotFound) return } - uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname"))) + uname := strings.ToLower(ctx.FormString("uname")) var u *user_model.User u, err = user_model.GetUserByName(ctx, uname) if err != nil { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 6d3dd5a3fe..f52abbfb02 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -11,7 +11,7 @@ import ( "fmt" "net/http" "os" - "path" + "path/filepath" "regexp" "strconv" "strings" @@ -90,11 +90,10 @@ func httpBase(ctx *context.Context) *serviceHandler { isWiki := false unitType := unit.TypeCode - var wikiRepoName string + if strings.HasSuffix(reponame, ".wiki") { isWiki = true unitType = unit.TypeWiki - wikiRepoName = reponame reponame = reponame[:len(reponame)-5] } @@ -107,16 +106,16 @@ func httpBase(ctx *context.Context) *serviceHandler { repoExist := true repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame) if err != nil { - 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 { + if !repo_model.IsErrRepoNotExist(err) { ctx.ServerError("GetRepositoryByName", err) 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 @@ -292,22 +291,9 @@ func httpBase(ctx *context.Context) *serviceHandler { environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID)) - w := ctx.Resp - r := ctx.Req - cfg := &serviceConfig{ - UploadPack: true, - ReceivePack: true, - Env: environ, - } + ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name - r.URL.Path = strings.ToLower(r.URL.Path) // blue: In case some repo name has upper case name - - dir := repo_model.RepoPath(username, reponame) - if isWiki { - dir = repo_model.RepoPath(username, wikiRepoName) - } - - return &serviceHandler{cfg, w, r, dir, cfg.Env} + return &serviceHandler{repo, isWiki, environ} } var ( @@ -352,32 +338,31 @@ func dummyInfoRefs(ctx *context.Context) { _, _ = ctx.Write(infoRefsCache) } -type serviceConfig struct { - UploadPack bool - ReceivePack bool - Env []string -} - type serviceHandler struct { - cfg *serviceConfig - w http.ResponseWriter - r *http.Request - dir string + repo *repo_model.Repository + isWiki bool environ []string } -func (h *serviceHandler) setHeaderNoCache() { - h.w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT") - h.w.Header().Set("Pragma", "no-cache") - h.w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate") +func (h *serviceHandler) getRepoDir() string { + if h.isWiki { + return h.repo.WikiPath() + } + 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() expires := now + 31536000 - h.w.Header().Set("Date", fmt.Sprintf("%d", now)) - h.w.Header().Set("Expires", fmt.Sprintf("%d", expires)) - h.w.Header().Set("Cache-Control", "public, max-age=31536000") + ctx.Resp.Header().Set("Date", fmt.Sprintf("%d", now)) + ctx.Resp.Header().Set("Expires", fmt.Sprintf("%d", expires)) + ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000") } func containsParentDirectorySeparator(v string) bool { @@ -394,71 +379,71 @@ func containsParentDirectorySeparator(v string) bool { 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) { log.Error("request file path contains invalid path: %v", file) - h.w.WriteHeader(http.StatusBadRequest) + ctx.Resp.WriteHeader(http.StatusBadRequest) return } - reqFile := path.Join(h.dir, file) + reqFile := filepath.Join(h.getRepoDir(), file) fi, err := os.Stat(reqFile) if os.IsNotExist(err) { - h.w.WriteHeader(http.StatusNotFound) + ctx.Resp.WriteHeader(http.StatusNotFound) return } - h.w.Header().Set("Content-Type", contentType) - h.w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) - h.w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) - http.ServeFile(h.w, h.r, reqFile) + ctx.Resp.Header().Set("Content-Type", contentType) + ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) + ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) + http.ServeFile(ctx.Resp, ctx.Req, reqFile) } // 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]+)*$`) -func prepareGitCmdWithAllowedService(service string, h *serviceHandler) (*git.Command, error) { - if service == "receive-pack" && h.cfg.ReceivePack { - return git.NewCommand(h.r.Context(), "receive-pack"), nil +func prepareGitCmdWithAllowedService(ctx *context.Context, service string) (*git.Command, error) { + if service == "receive-pack" { + return git.NewCommand(ctx, "receive-pack"), nil } - if service == "upload-pack" && h.cfg.UploadPack { - return git.NewCommand(h.r.Context(), "upload-pack"), nil + if service == "upload-pack" { + return git.NewCommand(ctx, "upload-pack"), nil } 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() { - if err := h.r.Body.Close(); err != nil { + if err := ctx.Req.Body.Close(); err != nil { log.Error("serviceRPC: Close: %v", err) } }() expectedContentType := fmt.Sprintf("application/x-git-%s-request", service) - if h.r.Header.Get("Content-Type") != expectedContentType { - log.Error("Content-Type (%q) doesn't match expected: %q", h.r.Header.Get("Content-Type"), expectedContentType) - h.w.WriteHeader(http.StatusUnauthorized) + if ctx.Req.Header.Get("Content-Type") != expectedContentType { + log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType) + ctx.Resp.WriteHeader(http.StatusUnauthorized) return } - cmd, err := prepareGitCmdWithAllowedService(service, h) + cmd, err := prepareGitCmdWithAllowedService(ctx, service) if err != nil { log.Error("Failed to prepareGitCmdWithService: %v", err) - h.w.WriteHeader(http.StatusUnauthorized) + ctx.Resp.WriteHeader(http.StatusUnauthorized) 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. - if h.r.Header.Get("Content-Encoding") == "gzip" { + if ctx.Req.Header.Get("Content-Encoding") == "gzip" { reqBody, err = gzip.NewReader(reqBody) if err != nil { log.Error("Fail to create gzip reader: %v", err) - h.w.WriteHeader(http.StatusInternalServerError) + ctx.Resp.WriteHeader(http.StatusInternalServerError) return } } @@ -466,23 +451,23 @@ func serviceRPC(h *serviceHandler, service string) { // set this for allow pre-receive and post-receive execute 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) } var stderr bytes.Buffer - cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.dir) - cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", 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.getRepoDir())) if err := cmd.Run(&git.RunOpts{ - Dir: h.dir, + Dir: h.getRepoDir(), Env: append(os.Environ(), h.environ...), - Stdout: h.w, + Stdout: ctx.Resp, Stdin: reqBody, Stderr: &stderr, UseContextTimeout: true, }); err != nil { 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 } @@ -492,7 +477,7 @@ func serviceRPC(h *serviceHandler, service string) { func ServiceUploadPack(ctx *context.Context) { h := httpBase(ctx) 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) { h := httpBase(ctx) if h != nil { - serviceRPC(h, "receive-pack") + serviceRPC(ctx, h, "receive-pack") } } -func getServiceType(r *http.Request) string { - serviceType := r.FormValue("service") +func getServiceType(ctx *context.Context) string { + serviceType := ctx.Req.FormValue("service") if !strings.HasPrefix(serviceType, "git-") { return "" } @@ -534,28 +519,28 @@ func GetInfoRefs(ctx *context.Context) { if h == nil { return } - h.setHeaderNoCache() - service := getServiceType(h.r) - cmd, err := prepareGitCmdWithAllowedService(service, h) + setHeaderNoCache(ctx) + service := getServiceType(ctx) + cmd, err := prepareGitCmdWithAllowedService(ctx, service) 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(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 { log.Error(fmt.Sprintf("%v - %s", err, string(refs))) } - h.w.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) - h.w.WriteHeader(http.StatusOK) - _, _ = h.w.Write(packetWrite("# service=git-" + service + "\n")) - _, _ = h.w.Write([]byte("0000")) - _, _ = h.w.Write(refs) + ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service)) + ctx.Resp.WriteHeader(http.StatusOK) + _, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n")) + _, _ = ctx.Resp.Write([]byte("0000")) + _, _ = ctx.Resp.Write(refs) } else { - updateServerInfo(ctx, h.dir) - h.sendFile("text/plain; charset=utf-8", "info/refs") + updateServerInfo(ctx, h.getRepoDir()) + 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) { h := httpBase(ctx) if h != nil { - h.setHeaderNoCache() + setHeaderNoCache(ctx) file := ctx.Params("file") if file != "" { - h.sendFile("text/plain", "objects/info/"+file) + h.sendFile(ctx, "text/plain", "objects/info/"+file) } 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) { h := httpBase(ctx) if h != nil { - h.setHeaderCacheForever() - h.sendFile("text/plain; charset=utf-8", "objects/info/packs") + setHeaderCacheForever(ctx) + 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) { h := httpBase(ctx) if h != nil { - h.setHeaderCacheForever() - h.sendFile("application/x-git-loose-object", fmt.Sprintf("objects/%s/%s", + setHeaderCacheForever(ctx) + h.sendFile(ctx, "application/x-git-loose-object", fmt.Sprintf("objects/%s/%s", ctx.Params("head"), ctx.Params("hash"))) } } @@ -598,8 +583,8 @@ func GetLooseObject(ctx *context.Context) { func GetPackFile(ctx *context.Context) { h := httpBase(ctx) if h != nil { - h.setHeaderCacheForever() - h.sendFile("application/x-git-packed-objects", "objects/pack/pack-"+ctx.Params("file")+".pack") + setHeaderCacheForever(ctx) + 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) { h := httpBase(ctx) if h != nil { - h.setHeaderCacheForever() - h.sendFile("application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx") + setHeaderCacheForever(ctx) + h.sendFile(ctx, "application/x-git-packed-objects-toc", "objects/pack/pack-"+ctx.Params("file")+".idx") } } diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index e217697cc0..c5c2a88c49 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/mailer" org_service "code.gitea.io/gitea/services/org" 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 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 { ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) return @@ -144,7 +143,7 @@ func AddTeamPost(ctx *context.Context) { return } - name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("team"))) + name := strings.ToLower(ctx.FormString("team")) if len(name) == 0 { ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") return diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 9900de3d2e..77173e58a3 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -474,3 +474,35 @@ func handleSchedules( 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) +} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index e7aa4a39ac..79dd84e0cc 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -10,6 +10,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" @@ -65,8 +66,15 @@ func startTasks(ctx context.Context) error { } } - cfg := row.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() - if cfg.IsWorkflowDisabled(row.Schedule.WorkflowID) { + cfg, err := row.Repo.GetUnit(ctx, unit.TypeActions) + 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 } diff --git a/services/convert/wiki.go b/services/convert/wiki.go index 1f04843483..767bfdb88d 100644 --- a/services/convert/wiki.go +++ b/services/convert/wiki.go @@ -6,11 +6,8 @@ package convert import ( "time" - 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" - wiki_service "code.gitea.io/gitea/services/wiki" ) // ToWikiCommit convert a git commit into a WikiCommit @@ -46,15 +43,3 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList { 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), - } -} diff --git a/services/repository/setting.go b/services/repository/setting.go index 6496ac4014..b82f24271e 100644 --- a/services/repository/setting.go +++ b/services/repository/setting.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" + actions_service "code.gitea.io/gitea/services/actions" ) // 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 { return err } diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go index e51d6c630c..74c7064043 100644 --- a/services/wiki/wiki_path.go +++ b/services/wiki/wiki_path.go @@ -9,7 +9,10 @@ import ( "strings" 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/services/convert" ) // To define the wiki related concepts: @@ -155,3 +158,15 @@ func UserTitleToWebPath(base, title string) WebPath { } 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), + } +} diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index bb2e25e86c..0aeb2af178 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -33,7 +33,7 @@ {{else if .IsPDFFile}}
{{else}} - {{ctx.Locale.Tr "repo.file_view_raw"}} + {{ctx.Locale.Tr "repo.file_view_raw"}} {{end}} {{else if .FileSize}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 07b2f58d53..dfb909e743 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -105,8 +105,9 @@ {{else}} {{ctx.Locale.Tr "repo.settings.mirror_settings.docs.no_new_mirrors"}} {{ctx.Locale.Tr "repo.settings.mirror_settings.docs.can_still_use"}}
{{end}} + + {{if .Repository.IsMirror}} - {{if $existingPushMirror}} @@ -200,8 +201,18 @@ - - {{end}}{{/* end if: IsMirror */}} +
{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}
+ {{end}}{{/* end if: IsMirror */}} + + + + + + + + + + {{range .PushMirrors}} diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index 8f02a489e9..d0730f23c1 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,7 +1,7 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}}
- +
{{ctx.Locale.Tr "repo.invisible_runes_header"}}
@@ -12,7 +12,7 @@
{{else if .EscapeStatus.HasAmbiguous}}
- +
{{ctx.Locale.Tr "repo.ambiguous_runes_header"}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index e6591df7e3..1f9e0b5028 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -108,7 +108,7 @@ {{else if .IsPDFFile}}
{{else}} - {{ctx.Locale.Tr "repo.file_view_raw"}} + {{ctx.Locale.Tr "repo.file_view_raw"}} {{end}}
{{else if .FileSize}} diff --git a/web_src/css/base.css b/web_src/css/base.css index cc1f6a8397..198e87c0e2 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -196,10 +196,14 @@ a.label, .ui.search > .results { background: var(--color-body); border-color: var(--color-secondary); + overflow-wrap: anywhere; /* allow text to wrap as fomantic limits this to 18em width */ } .ui.search > .results .result { background: var(--color-body); + border-color: var(--color-secondary); + display: flex; + align-items: center; } .ui.search > .results .result .title { diff --git a/web_src/css/repo.css b/web_src/css/repo.css index dfe0d6c77f..55c6ec4817 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2128,14 +2128,16 @@ } #search-user-box .results .result .image { - float: left; - margin-right: 8px; + order: 0; + margin-right: 12px; width: 2em; height: 2em; + min-width: 2em; + min-height: 2em; } #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) { diff --git a/web_src/js/features/comp/SearchUserBox.js b/web_src/js/features/comp/SearchUserBox.js index 960b787fea..992d4ef020 100644 --- a/web_src/js/features/comp/SearchUserBox.js +++ b/web_src/js/features/comp/SearchUserBox.js @@ -17,14 +17,13 @@ export function initCompSearchUserBox() { const searchQuery = $searchUserBox.find('input').val(); const searchQueryUppercase = searchQuery.toUpperCase(); $.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 = { - title, + title: item.login, image: item.avatar_url }; + if (item.full_name) { + resultItem.description = htmlEscape(item.full_name); + } if (searchQueryUppercase === item.login.toUpperCase()) { items.unshift(resultItem); } else { diff --git a/web_src/js/features/repo-settings.js b/web_src/js/features/repo-settings.js index 04974200bb..75e624a6a7 100644 --- a/web_src/js/features/repo-settings.js +++ b/web_src/js/features/repo-settings.js @@ -52,9 +52,9 @@ export function initRepoSettingSearchTeamBox() { onResponse(response) { const items = []; $.each(response.data, (_i, item) => { - const title = `${item.name} (${item.permission} access)`; items.push({ - title, + title: item.name, + description: `${item.permission} access` // TODO: translate this string }); }); diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js index a12802ef73..078d741253 100644 --- a/web_src/js/markup/codecopy.js +++ b/web_src/js/markup/codecopy.js @@ -12,8 +12,10 @@ export function renderCodeCopy() { if (!els.length) return; for (const el of els) { + if (!el.textContent) continue; 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); } }
{{ctx.Locale.Tr "repo.settings.mirror_settings.pushed_repository"}}{{ctx.Locale.Tr "repo.settings.mirror_settings.direction"}}{{ctx.Locale.Tr "repo.settings.mirror_settings.last_update"}}