diff --git a/Makefile b/Makefile index eb48766194f18..59cc27ee8a102 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ IMPORT := code.gitea.io/gitea GO ?= go SHASUM ?= shasum -a 256 -HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" ) +HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes) COMMA := , XGO_VERSION := go-1.20.x @@ -41,7 +41,7 @@ DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) -ifeq ($(HAS_GO), GO) +ifeq ($(HAS_GO), yes) GOPATH ?= $(shell $(GO) env GOPATH) export PATH := $(GOPATH)/bin:$(PATH) diff --git a/cmd/actions.go b/cmd/actions.go new file mode 100644 index 0000000000000..66ad336da508e --- /dev/null +++ b/cmd/actions.go @@ -0,0 +1,56 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli" +) + +var ( + // CmdActions represents the available actions sub-commands. + CmdActions = cli.Command{ + Name: "actions", + Usage: "", + Description: "Commands for managing Gitea Actions", + Subcommands: []cli.Command{ + subcmdActionsGenRunnerToken, + }, + } + + subcmdActionsGenRunnerToken = cli.Command{ + Name: "generate-runner-token", + Usage: "Generate a new token for a runner to use to register with the server", + Action: runGenerateActionsRunnerToken, + Aliases: []string{"grt"}, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "scope, s", + Value: "", + Usage: "{owner}[/{repo}] - leave empty for a global runner", + }, + }, + } +) + +func runGenerateActionsRunnerToken(c *cli.Context) error { + ctx, cancel := installSignals() + defer cancel() + + setting.InitProviderFromExistingFile() + setting.LoadCommonSettings() + + scope := c.String("scope") + + respText, extra := private.GenerateActionsRunnerToken(ctx, scope) + if extra.HasError() { + return handleCliResponseExtra(extra) + } + _, _ = fmt.Printf("%s\n", respText) + return nil +} diff --git a/cmd/convert.go b/cmd/convert.go index d9b89495c1bf0..8c7746fdf2970 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -17,7 +17,7 @@ import ( var CmdConvert = cli.Command{ Name: "convert", Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", Action: runConvert, } @@ -35,17 +35,22 @@ func runConvert(ctx *cli.Context) error { log.Info("Log path: %s", setting.Log.RootPath) log.Info("Configuration file: %s", setting.CustomConf) - if !setting.Database.Type.IsMySQL() { - fmt.Println("This command can only be used with a MySQL database") - return nil + switch { + case setting.Database.Type.IsMySQL(): + if err := db.ConvertUtf8ToUtf8mb4(); err != nil { + log.Fatal("Failed to convert database from utf8 to utf8mb4: %v", err) + return err + } + fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4") + case setting.Database.Type.IsMSSQL(): + if err := db.ConvertVarcharToNVarchar(); err != nil { + log.Fatal("Failed to convert database from varchar to nvarchar: %v", err) + return err + } + fmt.Println("Converted successfully, please confirm your database's all columns character is NVARCHAR now") + default: + fmt.Println("This command can only be used with a MySQL or MSSQL database") } - if err := db.ConvertUtf8ToUtf8mb4(); err != nil { - log.Fatal("Failed to convert database from utf8 to utf8mb4: %v", err) - return err - } - - fmt.Println("Converted successfully, please confirm your database's character set is now utf8mb4") - return nil } diff --git a/docs/content/doc/administration/command-line.en-us.md b/docs/content/doc/administration/command-line.en-us.md index d3362e573138b..4d01d6e640ef0 100644 --- a/docs/content/doc/administration/command-line.en-us.md +++ b/docs/content/doc/administration/command-line.en-us.md @@ -551,3 +551,28 @@ Restore-repo restore repository data from disk dir: - `--owner_name lunny`: Restore destination owner name - `--repo_name tango`: Restore destination repository name - `--units `: Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units. + +### actions generate-runner-token + +Generate a new token for a runner to use to register with the server + +- Options: + - `--scope {owner}[/{repo}]`, `-s {owner}[/{repo}]`: To limit the scope of the runner, no scope means the runner can be used for all repos, but you can also limit it to a specific repo or owner + +To register a global runner: + +``` +gitea actions generate-runner-token +``` + +To register a runner for a specific organization, in this case `org`: + +``` +gitea actions generate-runner-token -s org +``` + +To register a runner for a specific repo, in this case `username/test-repo`: + +``` +gitea actions generate-runner-token -s username/test-repo +``` diff --git a/docs/content/doc/administration/https-support.zh-cn.md b/docs/content/doc/administration/https-support.zh-cn.md index 75e9284b59c95..3f55d022ec3b7 100644 --- a/docs/content/doc/administration/https-support.zh-cn.md +++ b/docs/content/doc/administration/https-support.zh-cn.md @@ -58,7 +58,7 @@ PORT_TO_REDIRECT = 3080 [ACME](https://tools.ietf.org/html/rfc8555) 是一种证书颁发机构标准协议,允许您自动请求和续订 SSL/TLS 证书。[Let`s Encrypt](https://letsencrypt.org/) 是使用此标准的免费公开信任的证书颁发机构服务器。仅实施“HTTP-01”和“TLS-ALPN-01”挑战。为了使 ACME 质询通过并验证您的域所有权,“80”端口(“HTTP-01”)或“443”端口(“TLS-ALPN-01”)上 gitea 域的外部流量必须由 gitea 实例提供服务。可能需要设置 [HTTP 重定向](#设置http重定向) 和端口转发才能正确路由外部流量。否则,到端口“80”的正常流量将自动重定向到 HTTPS。**您必须同意**ACME提供商的服务条款(默认为Let's Encrypt的 [服务条款](https://letsencrypt.org/documents/LE-SA-v1.2-2017年11月15日.pdf)。 -实用默认 Let's Encrypt 的最小配置如下: +使用默认 Let's Encrypt 的最小配置如下: ```ini [server] @@ -92,7 +92,7 @@ ACME_EMAIL=email@example.com 按照 [reverse proxy guide](../reverse-proxies) 的规则设置你的反向代理服务器 -然后,按照下面的想到启用 HTTPS: +然后,按照下面的向导启用 HTTPS: - [nginx](https://nginx.org/en/docs/http/configuring_https_servers.html) - [apache2/httpd](https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html) diff --git a/docs/content/doc/installation/upgrade-from-gitea.zh-cn.md b/docs/content/doc/installation/upgrade-from-gitea.zh-cn.md new file mode 100644 index 0000000000000..8429ca492e745 --- /dev/null +++ b/docs/content/doc/installation/upgrade-from-gitea.zh-cn.md @@ -0,0 +1,91 @@ +--- +date: "2021-09-02T16:00:00+08:00" +title: "从旧版 Gitea 升级" +slug: "upgrade-from-gitea" +weight: 100 +toc: false +draft: false +menu: + sidebar: + parent: "installation" + name: "从旧版 Gitea 升级" + weight: 100 + identifier: "upgrade-from-gitea" +--- + +# 从旧版 Gitea 升级 + +**目录** + +{{< toc >}} + +想要升级 Gitea,只需要下载新版,停止运行旧版,进行数据备份,然后运行新版就好。 +每次 Gitea 实例启动时,它都会检查是否要进行数据库迁移。 +如果需要进行数据库迁移,Gitea 会花一些时间完成升级然后继续服务。 + +## 为重大变更检查更新日志 + +为了让 Gitea 变得更好,进行重大变更是不可避免的,尤其是一些里程碑更新的发布。 +在更新前,请 [在 Gitea 博客上阅读更新日志](https://blog.gitea.io/) +并检查重大变更是否会影响你的 Gitea 实例。 + +## 降级前的备份 + +Gitea 会保留首二位版本号相同的版本的兼容性 (`a.b.x` -> `a.b.y`), +这些版本拥有相同的数据库结构,可以自由升级或降级。 +其他情况 (`a.b.?` -> `a.c.?`)下, +新版 Gitea 可能将会将数据库升级到与旧版数据库不同的结构。 + +举个例子: + +| 当前 | 目标 | 结果 | +| --- | --- | --- | +| 1.4.0 | 1.4.1 | ✅ | +| 1.4.1 | 1.4.0 | ⚠️ 不建议,后果自负!尽管数据库结构可能不会变更,让它可以正常工作。我们强烈建议降级前进行完全的备份。 | +| 1.4.x | 1.5.y | ✅ 数据库会被自动升级。你可以直接从 1.4.x 升级到最新的 1.5.y。 | +| 1.5.y | 1.4.x | ❌ 数据库已被升级且不可被旧版 Gitea 利用,使用备份来进行降级。 | + +**因为你不能基于升级后的数据库运行旧版 Gitea,所以你应该在数据库升级前完成数据备份。** + +如果你在生产环境下使用 Gitea,你应该在升级前做好备份,哪怕只是小版本的补丁更新。 + +备份步骤: + +* 停止 Gitea 实例 +* 备份数据库 +* 备份 Gitea 配置文件 +* 备份 Gitea 在 `APP_DATA_PATH` 中的数据文件 +* 备份 Gitea 的外部存储 (例如: S3/MinIO 或被使用的其他存储) + +如果你在使用云服务或拥有快照功能的文件系统, +最好对 Gitea 的数据盘及相关资料存储进行一次快照。 + +## 从 Docker 升级 + +* `docker pull` 拉取 Gitea 的最新发布版。 +* 停止运行中的实例,备份数据。 +* 使用 `docker` 或 `docker-compose` 启动更新的 Gitea Docker 容器. + +## 从包升级 + +* 停止运行中的实例,备份数据。 +* 使用你的包管理器更新 Gitea 到最新版本。 +* 启动 Gitea 实例。 + +## 从二进制升级 + +* 下载最新的 Gitea 二进制文件到临时文件夹中。 +* 停止运行中的实例,备份数据。 +* 将旧的 Gitea 二进制文件覆盖成新的。 +* 启动 Gitea 实例。 + +在 Linux 系统上自动执行以上步骤的脚本可在 [Gitea 的 source tree 中找到 `contrib/upgrade.sh` 来获取](https://github.com/go-gitea/gitea/blob/main/contrib/upgrade.sh). + +## 小心你的自定义模板 + +Gitea 的模板结构与变量可能会随着各个版本的发布发生变化,如果你使用了自定义模板, +你得注意你的模板与你使用的 Gitea 版本的兼容性。 + +如果自定义模板与 Gitea 版本不兼容,你可能会得到: +`50x` 服务器错误,页面元素丢失或故障,莫名其妙的页面布局,等等… +移除或更新不兼容的模板,Gitea Web 才可以正常工作。 diff --git a/main.go b/main.go index eeedf54c27419..1589fa97db434 100644 --- a/main.go +++ b/main.go @@ -75,6 +75,7 @@ arguments - which can alternatively be run by running the subcommand web.` cmd.CmdDocs, cmd.CmdDumpRepository, cmd.CmdRestoreRepository, + cmd.CmdActions, } // Now adjust these commands to add our global configuration options diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index d3f0f0db73b27..33207995267b6 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -69,3 +69,12 @@ func getUserHeatmapData(user *user_model.User, team *organization.Team, doer *us OrderBy("timestamp"). Find(&hdata) } + +// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap +func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 { + var total int64 + for _, v := range hdata { + total += v.Contributions + } + return total +} diff --git a/models/db/convert.go b/models/db/convert.go index b17e68c87e591..112c8575ca2c7 100644 --- a/models/db/convert.go +++ b/models/db/convert.go @@ -42,6 +42,33 @@ func ConvertUtf8ToUtf8mb4() error { return nil } +// ConvertVarcharToNVarchar converts database and tables from varchar to nvarchar if it's mssql +func ConvertVarcharToNVarchar() error { + if x.Dialect().URI().DBType != schemas.MSSQL { + return nil + } + + sess := x.NewSession() + defer sess.Close() + res, err := sess.QuerySliceString(`SELECT 'ALTER TABLE ' + OBJECT_NAME(SC.object_id) + ' MODIFY SC.name NVARCHAR(' + CONVERT(VARCHAR(5),SC.max_length) + ')' +FROM SYS.columns SC +JOIN SYS.types ST +ON SC.system_type_id = ST.system_type_id +AND SC.user_type_id = ST.user_type_id +WHERE ST.name ='varchar'`) + if err != nil { + return err + } + for _, row := range res { + if len(row) == 1 { + if _, err = sess.Exec(row[0]); err != nil { + return err + } + } + } + return err +} + // Cell2Int64 converts a xorm.Cell type to int64, // and handles possible irregular cases. func Cell2Int64(val xorm.Cell) int64 { diff --git a/models/issues/assignees.go b/models/issues/assignees.go index 16f675a83f875..fdd0d6f2274b6 100644 --- a/models/issues/assignees.go +++ b/models/issues/assignees.go @@ -63,8 +63,8 @@ func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.U } // ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. -func ToggleIssueAssignee(issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) +func ToggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) { + ctx, committer, err := db.TxContext(ctx) if err != nil { return false, nil, err } diff --git a/models/issues/assignees_test.go b/models/issues/assignees_test.go index 8a2cef8acd8e6..2185f6fc4244f 100644 --- a/models/issues/assignees_test.go +++ b/models/issues/assignees_test.go @@ -24,17 +24,17 @@ func TestUpdateAssignee(t *testing.T) { // Assign multiple users user2, err := user_model.GetUserByID(db.DefaultContext, 2) assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID) + _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, user2.ID) assert.NoError(t, err) user3, err := user_model.GetUserByID(db.DefaultContext, 3) assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID) + _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, user3.ID) assert.NoError(t, err) user1, err := user_model.GetUserByID(db.DefaultContext, 1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him assert.NoError(t, err) - _, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID) + _, _, err = issues_model.ToggleIssueAssignee(db.DefaultContext, issue, &user_model.User{ID: 1}, user1.ID) assert.NoError(t, err) // Check if he got removed diff --git a/models/issues/issue.go b/models/issues/issue.go index 64b0edd3e7bac..583603047661e 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -743,8 +743,8 @@ func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, } // ChangeIssueTitle changes the title of this issue, as the given user. -func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) +func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User, oldTitle string) (err error) { + ctx, committer, err := db.TxContext(ctx) if err != nil { return err } diff --git a/models/issues/issue_xref_test.go b/models/issues/issue_xref_test.go index 6d96c398d0524..6e94c262723eb 100644 --- a/models/issues/issue_xref_test.go +++ b/models/issues/issue_xref_test.go @@ -83,7 +83,7 @@ func TestXRef_NeuterCrossReferences(t *testing.T) { d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) i.Title = "title2, no mentions" - assert.NoError(t, issues_model.ChangeIssueTitle(i, d, title)) + assert.NoError(t, issues_model.ChangeIssueTitle(db.DefaultContext, i, d, title)) ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}) assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 35a18fb7f2bbc..42806a808f791 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -483,6 +483,8 @@ var migrations = []Migration{ NewMigration("Fix incorrect owner team unit access mode", v1_20.FixIncorrectOwnerTeamUnitAccessMode), // v252 -> v253 NewMigration("Fix incorrect admin team unit access mode", v1_20.FixIncorrectAdminTeamUnitAccessMode), + // v253 -> v254 + NewMigration("Fix ExternalTracker and ExternalWiki accessMode in owner and admin team", v1_20.FixExternalTrackerAndExternalWikiAccessModeInOwnerAndAdminTeam), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_20/v253.go b/models/migrations/v1_20/v253.go new file mode 100644 index 0000000000000..96c494bd8d903 --- /dev/null +++ b/models/migrations/v1_20/v253.go @@ -0,0 +1,49 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "code.gitea.io/gitea/modules/log" + + "xorm.io/xorm" +) + +func FixExternalTrackerAndExternalWikiAccessModeInOwnerAndAdminTeam(x *xorm.Engine) error { + type UnitType int + type AccessMode int + + type TeamUnit struct { + ID int64 `xorm:"pk autoincr"` + Type UnitType `xorm:"UNIQUE(s)"` + AccessMode AccessMode + } + + const ( + // AccessModeRead read access + AccessModeRead = 1 + + // Unit Type + TypeExternalWiki = 6 + TypeExternalTracker = 7 + ) + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + count, err := sess.Table("team_unit"). + Where("type IN (?, ?) AND access_mode > ?", TypeExternalWiki, TypeExternalTracker, AccessModeRead). + Update(&TeamUnit{ + AccessMode: AccessModeRead, + }) + if err != nil { + return err + } + log.Debug("Updated %d ExternalTracker and ExternalWiki access mode to belong to owner and admin", count) + + return sess.Commit() +} diff --git a/models/repo/topic.go b/models/repo/topic.go index 05f50cfe46353..88fe532be9a70 100644 --- a/models/repo/topic.go +++ b/models/repo/topic.go @@ -194,14 +194,16 @@ func (opts *FindTopicOptions) toConds() builder.Cond { // FindTopics retrieves the topics via FindTopicOptions func FindTopics(opts *FindTopicOptions) ([]*Topic, int64, error) { sess := db.GetEngine(db.DefaultContext).Select("topic.*").Where(opts.toConds()) + orderBy := "topic.repo_count DESC" if opts.RepoID > 0 { sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id") + orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result } if opts.PageSize != 0 && opts.Page != 0 { sess = db.SetSessionPagination(sess, opts) } topics := make([]*Topic, 0, 10) - total, err := sess.Desc("topic.repo_count").FindAndCount(&topics) + total, err := sess.OrderBy(orderBy).FindAndCount(&topics) return topics, total, err } diff --git a/models/user/user.go b/models/user/user.go index 5709ed7ff27e9..5f152780bff04 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -41,6 +41,18 @@ const ( // UserTypeOrganization defines an organization UserTypeOrganization + + // UserTypeReserved reserves a (non-existing) user, i.e. to prevent a spam user from re-registering after being deleted, or to reserve the name until the user is actually created later on + UserTypeUserReserved + + // UserTypeOrganizationReserved reserves a (non-existing) organization, to be used in combination with UserTypeUserReserved + UserTypeOrganizationReserved + + // UserTypeBot defines a bot user + UserTypeBot + + // UserTypeRemoteUser defines a remote user for federated users + UserTypeRemoteUser ) const ( @@ -312,6 +324,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO Select("`user`.*"). Join("LEFT", "follow", "`user`.id=follow.user_id"). Where("follow.follow_id=?", u.ID). + And("`user`.type=?", UserTypeIndividual). And(isUserVisibleToViewerCond(viewer)) if listOptions.Page != 0 { @@ -333,6 +346,7 @@ func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListO Select("`user`.*"). Join("LEFT", "follow", "`user`.id=follow.follow_id"). Where("follow.user_id=?", u.ID). + And("`user`.type=?", UserTypeIndividual). And(isUserVisibleToViewerCond(viewer)) if listOptions.Page != 0 { @@ -959,7 +973,7 @@ func GetUserByName(ctx context.Context, name string) (*User, error) { if len(name) == 0 { return nil, ErrUserNotExist{0, name, 0} } - u := &User{LowerName: strings.ToLower(name)} + u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual} has, err := db.GetEngine(ctx).Get(u) if err != nil { return nil, err diff --git a/modules/actions/github.go b/modules/actions/github.go index 1148554139cd1..f3cb335da98c4 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -21,6 +21,7 @@ const ( githubEventIssueComment = "issue_comment" githubEventRelease = "release" githubEventPullRequestComment = "pull_request_comment" + githubEventGollum = "gollum" ) // canGithubEventMatch check if the input Github event can match any Gitea event. @@ -29,6 +30,10 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case githubEventRegistryPackage: return triggedEvent == webhook_module.HookEventPackage + // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum + case githubEventGollum: + return triggedEvent == webhook_module.HookEventWiki + case githubEventIssues: switch triggedEvent { case webhook_module.HookEventIssues, diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index d21dc1d809c01..f37f4f2878af7 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -119,8 +119,6 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType webhook_module.HookEventCreate, webhook_module.HookEventDelete, webhook_module.HookEventFork, - // FIXME: `wiki` event should match `gollum` event - // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum webhook_module.HookEventWiki: if len(evt.Acts()) != 0 { log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 6724abafd859e..6ef5d59942298 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -92,6 +92,13 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on:\n registry_package:\n types: [updated]", expected: false, }, + { + desc: "HookEventWiki(wiki) matches githubEventGollum(gollum)", + triggedEvent: webhook_module.HookEventWiki, + payload: nil, + yamlOn: "on: gollum", + expected: true, + }, } for _, tc := range testCases { diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 26e82bf13acf4..f63c5c5c52b32 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -132,18 +132,10 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, }, } -type nullLocale struct{} - -func (nullLocale) Language() string { return "" } -func (nullLocale) Tr(key string, _ ...interface{}) string { return key } -func (nullLocale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { return "" } - -var _ (translation.Locale) = nullLocale{} - func TestEscapeControlString(t *testing.T) { for _, tt := range escapeControlTests { t.Run(tt.name, func(t *testing.T) { - status, result := EscapeControlString(tt.text, nullLocale{}) + status, result := EscapeControlString(tt.text, &translation.MockLocale{}) if !reflect.DeepEqual(*status, tt.status) { t.Errorf("EscapeControlString() status = %v, wanted= %v", status, tt.status) } @@ -179,7 +171,7 @@ func TestEscapeControlReader(t *testing.T) { t.Run(tt.name, func(t *testing.T) { input := strings.NewReader(tt.text) output := &strings.Builder{} - status, err := EscapeControlReader(input, output, nullLocale{}) + status, err := EscapeControlReader(input, output, &translation.MockLocale{}) result := output.String() if err != nil { t.Errorf("EscapeControlReader(): err = %v", err) @@ -201,5 +193,5 @@ func TestEscapeControlReader_panic(t *testing.T) { for i := 0; i < 6826; i++ { bs = append(bs, []byte("—")...) } - _, _ = EscapeControlString(string(bs), nullLocale{}) + _, _ = EscapeControlString(string(bs), &translation.MockLocale{}) } diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go index 6b91a81fc4795..f6e782a5a460d 100644 --- a/modules/csv/csv_test.go +++ b/modules/csv/csv_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/translation" "github.com/stretchr/testify/assert" ) @@ -550,20 +551,6 @@ a|"he said, ""here I am"""`, } } -type mockLocale struct{} - -func (l mockLocale) Language() string { - return "en" -} - -func (l mockLocale) Tr(s string, _ ...interface{}) string { - return s -} - -func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string { - return key1 -} - func TestFormatError(t *testing.T) { cases := []struct { err error @@ -591,7 +578,7 @@ func TestFormatError(t *testing.T) { } for n, c := range cases { - message, err := FormatError(c.err, mockLocale{}) + message, err := FormatError(c.err, &translation.MockLocale{}) if c.expectsError { assert.Error(t, err, "case %d: expected an error to be returned", n) } else { diff --git a/modules/indexer/issues/meilisearch.go b/modules/indexer/issues/meilisearch.go index 5c45236e66006..319dc3e30b2fa 100644 --- a/modules/indexer/issues/meilisearch.go +++ b/modules/indexer/issues/meilisearch.go @@ -6,6 +6,7 @@ package issues import ( "context" "strconv" + "strings" "sync" "time" @@ -120,10 +121,11 @@ func (b *MeilisearchIndexer) Delete(ids ...int64) error { // Search searches for issues by given conditions. // Returns the matching issue IDs func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { - filter := make([][]string, 0, len(repoIDs)) + repoFilters := make([]string, 0, len(repoIDs)) for _, repoID := range repoIDs { - filter = append(filter, []string{"repo_id = " + strconv.FormatInt(repoID, 10)}) + repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) } + filter := strings.Join(repoFilters, " OR ") searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{ Filter: filter, Limit: int64(limit), diff --git a/modules/markup/markdown/convertyaml.go b/modules/markup/markdown/convertyaml.go index 6e90847e06a9c..1675b68be2a9a 100644 --- a/modules/markup/markdown/convertyaml.go +++ b/modules/markup/markdown/convertyaml.go @@ -34,7 +34,7 @@ func nodeToTable(meta *yaml.Node) ast.Node { func mappingNodeToTable(meta *yaml.Node) ast.Node { table := east.NewTable() - alignments := []east.Alignment{} + alignments := make([]east.Alignment, 0, len(meta.Content)/2) for i := 0; i < len(meta.Content); i += 2 { alignments = append(alignments, east.AlignNone) } diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 50b438219b850..816e93b700fa4 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -34,16 +34,17 @@ type ASTTransformer struct{} // Transform transforms the given AST tree. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { firstChild := node.FirstChild() - createTOC := false + tocMode := "" ctx := pc.Get(renderContextKey).(*markup.RenderContext) rc := pc.Get(renderConfigKey).(*RenderConfig) + + tocList := make([]markup.Header, 0, 20) if rc.yamlNode != nil { metaNode := rc.toMetaNode() if metaNode != nil { node.InsertBefore(node, firstChild, metaNode) } - createTOC = rc.TOC - ctx.TableOfContents = make([]markup.Header, 0, 100) + tocMode = rc.TOC } attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote]) @@ -59,15 +60,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) } } - text := n.Text(reader.Source()) + txt := n.Text(reader.Source()) header := markup.Header{ - Text: util.BytesToReadOnlyString(text), + Text: util.BytesToReadOnlyString(txt), Level: v.Level, } if id, found := v.AttributeString("id"); found { header.ID = util.BytesToReadOnlyString(id.([]byte)) } - ctx.TableOfContents = append(ctx.TableOfContents, header) + tocList = append(tocList, header) case *ast.Image: // Images need two things: // @@ -201,14 +202,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa return ast.WalkContinue, nil }) - if createTOC && len(ctx.TableOfContents) > 0 { - lang := rc.Lang - if len(lang) == 0 { - lang = setting.Langs[0] - } - tocNode := createTOCNode(ctx.TableOfContents, lang) - if tocNode != nil { + showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main" + showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar + if len(tocList) > 0 && (showTocInMain || showTocInSidebar) { + if showTocInMain { + tocNode := createTOCNode(tocList, rc.Lang, nil) node.InsertBefore(node, firstChild, tocNode) + } else { + tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"}) + ctx.SidebarTocNode = tocNode } } @@ -373,7 +375,11 @@ func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast. func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { var err error if entering { - _, err = w.WriteString("
") + if _, err = w.WriteString("") } else { _, err = w.WriteString("
") } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index f1ffea8872465..d4a7195dc5006 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -29,8 +29,8 @@ import ( ) var ( - converter goldmark.Markdown - once = sync.Once{} + specMarkdown goldmark.Markdown + specMarkdownOnce sync.Once ) var ( @@ -56,7 +56,7 @@ func (l *limitWriter) Write(data []byte) (int, error) { if err != nil { return n, err } - return n, fmt.Errorf("Rendered content too large - truncating render") + return n, fmt.Errorf("rendered content too large - truncating render") } n, err := l.w.Write(data) l.sum += int64(n) @@ -73,10 +73,10 @@ func newParserContext(ctx *markup.RenderContext) parser.Context { return pc } -// actualRender renders Markdown to HTML without handling special links. -func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { - once.Do(func() { - converter = goldmark.New( +// SpecializedMarkdown sets up the Gitea specific markdown extensions +func SpecializedMarkdown() goldmark.Markdown { + specMarkdownOnce.Do(func() { + specMarkdown = goldmark.New( goldmark.WithExtensions( extension.NewTable( extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)), @@ -139,13 +139,18 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) ) // Override the original Tasklist renderer! - converter.Renderer().AddOptions( + specMarkdown.Renderer().AddOptions( renderer.WithNodeRenderers( util.Prioritized(NewHTMLRenderer(), 10), ), ) }) + return specMarkdown +} +// actualRender renders Markdown to HTML without handling special links. +func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { + converter := SpecializedMarkdown() lw := &limitWriter{ w: output, limit: setting.UI.MaxDisplayFileSize * 3, @@ -174,7 +179,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) buf = giteautil.NormalizeEOL(buf) rc := &RenderConfig{ - Meta: "table", + Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)), Icon: "table", Lang: "", } diff --git a/modules/markup/markdown/renderconfig.go b/modules/markup/markdown/renderconfig.go index f9c9cbc5f4227..691df7431209a 100644 --- a/modules/markup/markdown/renderconfig.go +++ b/modules/markup/markdown/renderconfig.go @@ -7,32 +7,42 @@ import ( "fmt" "strings" + "code.gitea.io/gitea/modules/markup" + "github.com/yuin/goldmark/ast" "gopkg.in/yaml.v3" ) // RenderConfig represents rendering configuration for this file type RenderConfig struct { - Meta string + Meta markup.RenderMetaMode Icon string - TOC bool + TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view Lang string yamlNode *yaml.Node } +func renderMetaModeFromString(s string) markup.RenderMetaMode { + switch strings.TrimSpace(strings.ToLower(s)) { + case "none": + return markup.RenderMetaAsNone + case "table": + return markup.RenderMetaAsTable + default: // "details" + return markup.RenderMetaAsDetails + } +} + // UnmarshalYAML implement yaml.v3 UnmarshalYAML func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { if rc == nil { - rc = &RenderConfig{ - Meta: "table", - Icon: "table", - Lang: "", - } + return nil } + rc.yamlNode = value type commonRenderConfig struct { - TOC bool `yaml:"include_toc"` + TOC string `yaml:"include_toc"` Lang string `yaml:"lang"` } var basic commonRenderConfig @@ -54,58 +64,45 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { if err := value.Decode(&stringBasic); err == nil { if stringBasic.Gitea != "" { - switch strings.TrimSpace(strings.ToLower(stringBasic.Gitea)) { - case "none": - rc.Meta = "none" - case "table": - rc.Meta = "table" - default: // "details" - rc.Meta = "details" - } + rc.Meta = renderMetaModeFromString(stringBasic.Gitea) } return nil } - type giteaControl struct { + type yamlRenderConfig struct { Meta *string `yaml:"meta"` Icon *string `yaml:"details_icon"` - TOC *bool `yaml:"include_toc"` + TOC *string `yaml:"include_toc"` Lang *string `yaml:"lang"` } - type complexGiteaConfig struct { - Gitea *giteaControl `yaml:"gitea"` + type yamlRenderConfigWrapper struct { + Gitea *yamlRenderConfig `yaml:"gitea"` } - var complex complexGiteaConfig - if err := value.Decode(&complex); err != nil { - return fmt.Errorf("unable to decode into complexRenderConfig %w", err) + + var cfg yamlRenderConfigWrapper + if err := value.Decode(&cfg); err != nil { + return fmt.Errorf("unable to decode into yamlRenderConfigWrapper %w", err) } - if complex.Gitea == nil { + if cfg.Gitea == nil { return nil } - if complex.Gitea.Meta != nil { - switch strings.TrimSpace(strings.ToLower(*complex.Gitea.Meta)) { - case "none": - rc.Meta = "none" - case "table": - rc.Meta = "table" - default: // "details" - rc.Meta = "details" - } + if cfg.Gitea.Meta != nil { + rc.Meta = renderMetaModeFromString(*cfg.Gitea.Meta) } - if complex.Gitea.Icon != nil { - rc.Icon = strings.TrimSpace(strings.ToLower(*complex.Gitea.Icon)) + if cfg.Gitea.Icon != nil { + rc.Icon = strings.TrimSpace(strings.ToLower(*cfg.Gitea.Icon)) } - if complex.Gitea.Lang != nil && *complex.Gitea.Lang != "" { - rc.Lang = *complex.Gitea.Lang + if cfg.Gitea.Lang != nil && *cfg.Gitea.Lang != "" { + rc.Lang = *cfg.Gitea.Lang } - if complex.Gitea.TOC != nil { - rc.TOC = *complex.Gitea.TOC + if cfg.Gitea.TOC != nil { + rc.TOC = *cfg.Gitea.TOC } return nil @@ -116,9 +113,9 @@ func (rc *RenderConfig) toMetaNode() ast.Node { return nil } switch rc.Meta { - case "table": + case markup.RenderMetaAsTable: return nodeToTable(rc.yamlNode) - case "details": + case markup.RenderMetaAsDetails: return nodeToDetails(rc.yamlNode, rc.Icon) default: return nil diff --git a/modules/markup/markdown/renderconfig_test.go b/modules/markup/markdown/renderconfig_test.go index f7f5e885a3b49..c53acdc77a788 100644 --- a/modules/markup/markdown/renderconfig_test.go +++ b/modules/markup/markdown/renderconfig_test.go @@ -60,7 +60,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { }, { "toc", &RenderConfig{ - TOC: true, + TOC: "true", Meta: "table", Icon: "table", Lang: "", @@ -68,7 +68,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { }, { "tocfalse", &RenderConfig{ - TOC: false, + TOC: "false", Meta: "table", Icon: "table", Lang: "", @@ -78,7 +78,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { "toclang", &RenderConfig{ Meta: "table", Icon: "table", - TOC: true, + TOC: "true", Lang: "testlang", }, ` include_toc: true @@ -120,7 +120,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { "complex2", &RenderConfig{ Lang: "two", Meta: "table", - TOC: true, + TOC: "true", Icon: "smiley", }, ` lang: one @@ -155,7 +155,7 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { t.Errorf("Lang Expected %s Got %s", tt.expected.Lang, got.Lang) } if got.TOC != tt.expected.TOC { - t.Errorf("TOC Expected %t Got %t", tt.expected.TOC, got.TOC) + t.Errorf("TOC Expected %q Got %q", tt.expected.TOC, got.TOC) } }) } diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go index 3715f031af584..9602040931aad 100644 --- a/modules/markup/markdown/toc.go +++ b/modules/markup/markdown/toc.go @@ -13,10 +13,14 @@ import ( "github.com/yuin/goldmark/ast" ) -func createTOCNode(toc []markup.Header, lang string) ast.Node { +func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]string) ast.Node { details := NewDetails() summary := NewSummary() + for k, v := range detailsAttrs { + details.SetAttributeString(k, []byte(v)) + } + summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc")))) details.AppendChild(details, summary) ul := ast.NewList('-') @@ -40,7 +44,7 @@ func createTOCNode(toc []markup.Header, lang string) ast.Node { } li := ast.NewListItem(currentLevel * 2) a := ast.NewLink() - a.Destination = []byte(fmt.Sprintf("#%s", url.PathEscape(header.ID))) + a.Destination = []byte(fmt.Sprintf("#%s", url.QueryEscape(header.ID))) a.AppendChild(a, ast.NewString([]byte(header.Text))) li.AppendChild(li, a) ul.AppendChild(ul, li) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index e934aed925e3e..f2477f1e9ee9f 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -16,6 +16,16 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + + "github.com/yuin/goldmark/ast" +) + +type RenderMetaMode string + +const ( + RenderMetaAsDetails RenderMetaMode = "details" // default + RenderMetaAsNone RenderMetaMode = "none" + RenderMetaAsTable RenderMetaMode = "table" ) type ProcessorHelper struct { @@ -63,7 +73,8 @@ type RenderContext struct { GitRepo *git.Repository ShaExistCache map[string]bool cancelFn func() - TableOfContents []Header + SidebarTocNode ast.Node + RenderMetaAs RenderMetaMode InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page } diff --git a/modules/private/actions.go b/modules/private/actions.go new file mode 100644 index 0000000000000..be24e16d3ff3f --- /dev/null +++ b/modules/private/actions.go @@ -0,0 +1,27 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "context" + + "code.gitea.io/gitea/modules/setting" +) + +// Email structure holds a data for sending general emails +type GenerateTokenRequest struct { + Scope string +} + +// GenerateActionsRunnerToken calls the internal GenerateActionsRunnerToken function +func GenerateActionsRunnerToken(ctx context.Context, scope string) (string, ResponseExtra) { + reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token" + + req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{ + Scope: scope, + }) + + resp, extra := requestJSONResp(req, &responseText{}) + return resp.Text, extra +} diff --git a/modules/structs/org.go b/modules/structs/org.go index b4c58623fd29c..7c83dcdee7e4d 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -30,8 +30,8 @@ type OrganizationPermissions struct { // CreateOrgOption options for creating an organization type CreateOrgOption struct { // required: true - UserName string `json:"username" binding:"Required"` - FullName string `json:"full_name"` + UserName string `json:"username" binding:"Required;Username;MaxSize(40)"` + FullName string `json:"full_name" binding:"MaxSize(100)"` Description string `json:"description" binding:"MaxSize(255)"` Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` Location string `json:"location" binding:"MaxSize(50)"` @@ -45,7 +45,7 @@ type CreateOrgOption struct { // EditOrgOption options for editing an organization type EditOrgOption struct { - FullName string `json:"full_name"` + FullName string `json:"full_name" binding:"MaxSize(100)"` Description string `json:"description" binding:"MaxSize(255)"` Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` Location string `json:"location" binding:"MaxSize(50)"` diff --git a/modules/templates/helper.go b/modules/templates/helper.go index f93419fe873eb..24687a460613a 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -132,7 +132,6 @@ func NewFuncMap() []template.FuncMap { // ----------------------------------------------------------------- // time / number / format "FileSize": base.FileSize, - "LocaleNumber": LocaleNumber, "CountFmt": base.FormatNumberSI, "TimeSince": timeutil.TimeSince, "TimeSinceUnix": timeutil.TimeSinceUnix, @@ -172,13 +171,6 @@ func NewFuncMap() []template.FuncMap { } return false }, - "Iterate": func(arg interface{}) (items []int64) { - count, _ := util.ToInt64(arg) - for i := int64(0); i < count; i++ { - items = append(items, i) - } - return items - }, // ----------------------------------------------------------------- // setting @@ -782,12 +774,6 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa return a } -// LocaleNumber renders a number with a Custom Element, browser will render it with a locale number -func LocaleNumber(v interface{}) template.HTML { - num, _ := util.ToInt64(v) - return template.HTML(fmt.Sprintf(`%d`, num, num)) -} - // Eval the expression and return the result, see the comment of eval.Expr for details. // To use this helper function in templates, pass each token as a separate parameter. // diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index 94dbd2f290d99..e558bf1b9f176 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -18,6 +18,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" chi "github.com/go-chi/chi/v5" @@ -34,7 +35,7 @@ func MockContext(t *testing.T, path string) *context.Context { Values: make(url.Values), }, Resp: context.NewResponse(resp), - Locale: &mockLocale{}, + Locale: &translation.MockLocale{}, } defer ctx.Close() @@ -91,20 +92,6 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) { assert.NoError(t, err) } -type mockLocale struct{} - -func (l mockLocale) Language() string { - return "en" -} - -func (l mockLocale) Tr(s string, _ ...interface{}) string { - return s -} - -func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string { - return key1 -} - type mockResponseWriter struct { httptest.ResponseRecorder size int diff --git a/modules/timeutil/since.go b/modules/timeutil/since.go index bdde54c617540..e6a2519d2120b 100644 --- a/modules/timeutil/since.go +++ b/modules/timeutil/since.go @@ -114,11 +114,25 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string { return strings.TrimPrefix(timeStr, ", ") } +func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML { + friendlyText := then.Format("2006-01-02 15:04:05 +07:00") + + // document: https://github.com/github/relative-time-element + attrs := `tense="past"` + isFuture := now.Before(then) + if isFuture { + attrs = `tense="future"` + } + + // declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip + htm := fmt.Sprintf(`%s`, + attrs, then.Format(time.RFC3339), friendlyText) + return template.HTML(htm) +} + // TimeSince renders relative time HTML given a time.Time func TimeSince(then time.Time, lang translation.Locale) template.HTML { - timestamp := then.UTC().Format(time.RFC3339) - // declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip - return template.HTML(fmt.Sprintf(`%s`, lang.Tr("on_date"), timestamp, timestamp)) + return timeSinceUnix(then, time.Now(), lang) } // TimeSinceUnix renders relative time HTML given a TimeStamp diff --git a/modules/translation/mock.go b/modules/translation/mock.go new file mode 100644 index 0000000000000..6ce66166aa13d --- /dev/null +++ b/modules/translation/mock.go @@ -0,0 +1,27 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package translation + +import "fmt" + +// MockLocale provides a mocked locale without any translations +type MockLocale struct{} + +var _ Locale = (*MockLocale)(nil) + +func (l MockLocale) Language() string { + return "en" +} + +func (l MockLocale) Tr(s string, _ ...interface{}) string { + return s +} + +func (l MockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string { + return key1 +} + +func (l MockLocale) PrettyNumber(v any) string { + return fmt.Sprint(v) +} diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 331da0f965c38..56cf1df2d438e 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -15,17 +15,20 @@ import ( "code.gitea.io/gitea/modules/translation/i18n" "golang.org/x/text/language" + "golang.org/x/text/message" + "golang.org/x/text/number" ) type contextKey struct{} -var ContextKey interface{} = &contextKey{} +var ContextKey any = &contextKey{} // Locale represents an interface to translation type Locale interface { Language() string - Tr(string, ...interface{}) string - TrN(cnt interface{}, key1, keyN string, args ...interface{}) string + Tr(string, ...any) string + TrN(cnt any, key1, keyN string, args ...any) string + PrettyNumber(v any) string } // LangType represents a lang type @@ -135,6 +138,7 @@ func Match(tags ...language.Tag) language.Tag { type locale struct { i18n.Locale Lang, LangName string // these fields are used directly in templates: .i18n.Lang + msgPrinter *message.Printer } // NewLocale return a locale @@ -147,13 +151,24 @@ func NewLocale(lang string) Locale { langName := "unknown" if l, ok := allLangMap[lang]; ok { langName = l.Name + } else if len(setting.Langs) > 0 { + lang = setting.Langs[0] + langName = setting.Names[0] } + i18nLocale, _ := i18n.GetLocale(lang) - return &locale{ + l := &locale{ Locale: i18nLocale, Lang: lang, LangName: langName, } + if langTag, err := language.Parse(lang); err != nil { + log.Error("Failed to parse language tag from name %q: %v", l.Lang, err) + l.msgPrinter = message.NewPrinter(language.English) + } else { + l.msgPrinter = message.NewPrinter(langTag) + } + return l } func (l *locale) Language() string { @@ -199,7 +214,7 @@ var trNLangRules = map[string]func(int64) int{ } // TrN returns translated message for plural text translation -func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { +func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string { var c int64 if t, ok := cnt.(int); ok { c = int64(t) @@ -223,3 +238,8 @@ func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) st } return l.Tr(keyN, args...) } + +func (l *locale) PrettyNumber(v any) string { + // TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format + return l.msgPrinter.Sprintf("%v", number.Decimal(v)) +} diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go new file mode 100644 index 0000000000000..83a40f145815a --- /dev/null +++ b/modules/translation/translation_test.go @@ -0,0 +1,27 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package translation + +import ( + "testing" + + "code.gitea.io/gitea/modules/translation/i18n" + + "github.com/stretchr/testify/assert" +) + +func TestPrettyNumber(t *testing.T) { + // TODO: make this package friendly to testing + + i18n.ResetDefaultLocales() + + allLangMap = make(map[string]*LangType) + allLangMap["id-ID"] = &LangType{Lang: "id-ID", Name: "Bahasa Indonesia"} + + l := NewLocale("id-ID") + assert.EqualValues(t, "1.000.000", l.PrettyNumber(1000000)) + + l = NewLocale("nosuch") + assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000)) +} diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 5f6bfa76bca2d..2e720709f4f08 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -110,7 +110,6 @@ never=Nikdy rss_feed=RSS kanál - [aria] navbar=Navigační lišta footer=Patička @@ -3027,8 +3026,6 @@ starred_repo=si oblíbil/a %[2]s watched_repo=začal/a sledovat %[2]s [tool] -ago=před %s -from_now=od teď %s now=nyní future=budoucí 1s=1 sekundou diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 28fc247756724..5b2eaf316f915 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -107,7 +107,6 @@ never=Niemals rss_feed=RSS Feed - [aria] [editor] @@ -2956,8 +2955,6 @@ starred_repo=markiert %[2]s watched_repo=beobachtet %[2]s [tool] -ago=vor %s -from_now=in %s now=jetzt future=Zukunft 1s=1 Sekunde diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 8b7d03dac3292..e3fe183e5950e 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -110,7 +110,6 @@ never=Ποτέ rss_feed=Ροή RSS - [aria] navbar=Γραμμή Πλοήγησης footer=Υποσέλιδο @@ -3066,8 +3065,6 @@ starred_repo=έδωσε αστέρι στο %[2]s watched_repo=άρχισε να παρακολουθεί το %[2]s [tool] -ago=%s πριν -from_now=%s από τώρα now=τώρα future=μελλοντικό 1s=1 δευτερόλεπτο diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f71ea824e979b..416e883fc771a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -112,14 +112,18 @@ never = Never rss_feed = RSS Feed -on_date = on - [aria] navbar = Navigation Bar footer = Footer footer.software = About Software footer.links = Links +[heatmap] +number_of_contributions_in_the_last_12_months = %s contributions in the last 12 months +no_contributions = No contributions +less = Less +more = More + [editor] buttons.heading.tooltip = Add heading buttons.bold.tooltip = Add bold text @@ -248,7 +252,7 @@ install_btn_confirm = Install Gitea test_git_failed = Could not test 'git' command: %v sqlite3_not_available = This Gitea version does not support SQLite3. Please download the official binary version from %s (not the 'gobuild' version). invalid_db_setting = The database settings are invalid: %v -invalid_db_table = The database table '%s' is invalid: %v +invalid_db_table = The database table "%s" is invalid: %v invalid_repo_path = The repository root path is invalid: %v invalid_app_data_path = The app data path is invalid: %v run_user_not_match = The 'run as' username is not the current username: %s -> %s @@ -314,7 +318,7 @@ repo_no_results = No matching repositories found. user_no_results = No matching users found. org_no_results = No matching organizations found. code_no_results = No source code matching your search term found. -code_search_results = Search results for '%s' +code_search_results = Search results for "%s" code_last_indexed_at = Last indexed %s relevant_repositories_tooltip = Repositories that are forks or that have no topic, no icon, and no description are hidden. relevant_repositories = Only relevant repositories are being shown, show unfiltered results. @@ -491,8 +495,8 @@ size_error = ` must be size %s.` min_size_error = ` must contain at least %s characters.` max_size_error = ` must contain at most %s characters.` email_error = ` is not a valid email address.` -url_error = `'%s' is not a valid URL.` -include_error = ` must contain substring '%s'.` +url_error = `"%s" is not a valid URL.` +include_error = ` must contain substring "%s".` glob_pattern_error = ` glob pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` @@ -517,7 +521,7 @@ team_name_been_taken = The team name is already taken. team_no_units_error = Allow access to at least one repository section. email_been_used = The email address is already used. email_invalid = The email address is invalid. -openid_been_used = The OpenID address '%s' is already used. +openid_been_used = The OpenID address "%s" is already used. username_password_incorrect = Username or password is incorrect. password_complexity = Password does not pass complexity requirements: password_lowercase_one = At least one lowercase character @@ -569,9 +573,9 @@ disabled_public_activity = This user has disabled the public visibility of the a email_visibility.limited = Your email address is visible to all authenticated users email_visibility.private = Your email address is only visible to you and administrators -form.name_reserved = The username '%s' is reserved. -form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username. -form.name_chars_not_allowed = User name '%s' contains invalid characters. +form.name_reserved = The username "%s" is reserved. +form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username. +form.name_chars_not_allowed = User name "%s" contains invalid characters. [settings] profile = Profile @@ -602,7 +606,7 @@ location = Location update_theme = Update Theme update_profile = Update Profile update_language = Update Language -update_language_not_found = Language '%s' is not available. +update_language_not_found = Language "%s" is not available. update_language_success = Language has been updated. update_profile_success = Your profile has been updated. change_username = Your username has been changed. @@ -679,7 +683,7 @@ add_new_email = Add New Email Address add_new_openid = Add New OpenID URI add_email = Add Email Address add_openid = Add OpenID URI -add_email_confirmation_sent = A confirmation email has been sent to '%s'. Please check your inbox within the next %s to confirm your email address. +add_email_confirmation_sent = A confirmation email has been sent to "%s". Please check your inbox within the next %s to confirm your email address. add_email_success = The new email address has been added. email_preference_set_success = Email preference has been set successfully. add_openid_success = The new OpenID address has been added. @@ -718,7 +722,7 @@ gpg_token_help = You can generate a signature using: gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature = Armored GPG signature key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----' -verify_gpg_key_success = GPG key '%s' has been verified. +verify_gpg_key_success = GPG key "%s" has been verified. ssh_key_verified=Verified Key ssh_key_verified_long=Key has been verified with a token and can be used to verify commits matching any activated email addresses for this user. ssh_key_verify=Verify @@ -728,15 +732,15 @@ ssh_token = Token ssh_token_help = You can generate a signature using: ssh_token_signature = Armored SSH signature key_signature_ssh_placeholder = Begins with '-----BEGIN SSH SIGNATURE-----' -verify_ssh_key_success = SSH key '%s' has been verified. +verify_ssh_key_success = SSH key "%s" has been verified. subkeys = Subkeys key_id = Key ID key_name = Key Name key_content = Content principal_content = Content -add_key_success = The SSH key '%s' has been added. -add_gpg_key_success = The GPG key '%s' has been added. -add_principal_success = The SSH certificate principal '%s' has been added. +add_key_success = The SSH key "%s" has been added. +add_gpg_key_success = The GPG key "%s" has been added. +add_principal_success = The SSH certificate principal "%s" has been added. delete_key = Remove ssh_key_deletion = Remove SSH Key gpg_key_deletion = Remove GPG Key @@ -991,8 +995,8 @@ archive.pull.nocomment = This repo is archived. You cannot comment on pull reque form.reach_limit_of_creation_1 = The owner has already reached the limit of %d repository. form.reach_limit_of_creation_n = The owner has already reached the limit of %d repositories. -form.name_reserved = The repository name '%s' is reserved. -form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. +form.name_reserved = The repository name "%s" is reserved. +form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository name. need_auth = Authorization migrate_options = Migration Options @@ -1144,7 +1148,7 @@ editor.must_be_on_a_branch = You must be on a branch to make or propose changes editor.fork_before_edit = You must fork this repository to make or propose changes to this file. editor.delete_this_file = Delete File editor.must_have_write_access = You must have write access to make or propose changes to this file. -editor.file_delete_success = File '%s' has been deleted. +editor.file_delete_success = File "%s" has been deleted. editor.name_your_file = Name your file… editor.filename_help = Add a directory by typing its name followed by a slash ('/'). Remove a directory by typing backspace at the beginning of the input field. editor.or = or @@ -1152,12 +1156,12 @@ editor.cancel_lower = Cancel editor.commit_signed_changes = Commit Signed Changes editor.commit_changes = Commit Changes editor.add_tmpl = Add '' -editor.add = Add '%s' -editor.update = Update '%s' -editor.delete = Delete '%s' +editor.add = Add %s +editor.update = Update %s +editor.delete = Delete %s editor.patch = Apply Patch editor.patching = Patching: -editor.fail_to_apply_patch = Unable to apply patch '%s' +editor.fail_to_apply_patch = Unable to apply patch "%s" editor.new_patch = New Patch editor.commit_message_desc = Add an optional extended description… editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message. @@ -1169,29 +1173,29 @@ editor.new_branch_name = Name the new branch for this commit editor.new_branch_name_desc = New branch name… editor.cancel = Cancel editor.filename_cannot_be_empty = The filename cannot be empty. -editor.filename_is_invalid = The filename is invalid: '%s'. -editor.branch_does_not_exist = Branch '%s' does not exist in this repository. -editor.branch_already_exists = Branch '%s' already exists in this repository. -editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository. -editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor -editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository. -editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository. -editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer exists in this repository. +editor.filename_is_invalid = The filename is invalid: "%s". +editor.branch_does_not_exist = Branch "%s" does not exist in this repository. +editor.branch_already_exists = Branch "%s" already exists in this repository. +editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. +editor.file_is_a_symlink = "%s" is a symbolic link. Symbolic links cannot be edited in the web editor +editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository. +editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository. +editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. -editor.file_already_exists = A file named '%s' already exists in this repository. +editor.file_already_exists = A file named "%s" already exists in this repository. editor.commit_empty_file_header = Commit an empty file editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. -editor.fail_to_update_file = Failed to update/create file '%s'. +editor.fail_to_update_file = Failed to update/create file "%s". editor.fail_to_update_file_summary = Error Message: editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks. editor.push_rejected = The change was rejected by the server. Please check Git Hooks. editor.push_rejected_summary = Full Rejection Message: editor.add_subdir = Add a directory… -editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v -editor.upload_file_is_locked = File '%s' is locked by %s. -editor.upload_files_to_dir = Upload files to '%s' -editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s'. +editor.unable_to_upload_files = Failed to upload files to "%s" with error: %v +editor.upload_file_is_locked = File "%s" is locked by %s. +editor.upload_files_to_dir = Upload files to "%s" +editor.cannot_commit_to_protected_branch = Cannot commit to protected branch "%s". editor.no_commit_to_branch = Unable to commit directly to branch because: editor.user_no_push_to_branch = User cannot push to branch editor.require_signed_commit = Branch requires a signed commit @@ -1200,7 +1204,7 @@ editor.revert = Revert %s onto: commits.desc = Browse source code change history. commits.commits = Commits -commits.no_commits = No commits in common. '%s' and '%s' have entirely different histories. +commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.nothing_to_compare = These branches are equal. commits.search = Search commits… commits.search.tooltip = You can prefix keywords with "author:", "committer:", "after:", or "before:", e.g. "revert author:Alice before:2019-01-13". @@ -1236,14 +1240,14 @@ projects.create = Create Project projects.title = Title projects.new = New Project projects.new_subheader = Coordinate, track, and update your work in one place, so projects stay transparent and on schedule. -projects.create_success = The project '%s' has been created. +projects.create_success = The project "%s" has been created. projects.deletion = Delete Project projects.deletion_desc = Deleting a project removes it from all related issues. Continue? projects.deletion_success = The project has been deleted. projects.edit = Edit Project projects.edit_subheader = Projects organize issues and track progress. projects.modify = Edit Project -projects.edit_success = Project '%s' has been updated. +projects.edit_success = Project "%s" has been updated. projects.type.none = "None" projects.type.basic_kanban = "Basic Kanban" projects.type.bug_triage = "Bug Triage" @@ -1315,7 +1319,7 @@ issues.label_templates.title = Load a predefined set of labels issues.label_templates.info = No labels exist yet. Create a label with 'New Label' or use a predefined label set: issues.label_templates.helper = Select a label set issues.label_templates.use = Use Label Set -issues.label_templates.fail_to_load_file = Failed to load label template file '%s': %v +issues.label_templates.fail_to_load_file = Failed to load label template file "%s": %v issues.add_label = added the %s label %s issues.add_labels = added the %s labels %s issues.remove_label = removed the %s label %s @@ -1559,7 +1563,7 @@ issues.review.add_review_request = "requested review from %s %s" issues.review.remove_review_request = "removed review request for %s %s" issues.review.remove_review_request_self = "refused to review %s" issues.review.pending = Pending -issues.review.pending.tooltip = This comment is not currently visible to other users. To submit your pending comments, select '%s' -> '%s/%s/%s' at the top of the page. +issues.review.pending.tooltip = This comment is not currently visible to other users. To submit your pending comments, select "%s" -> "%s/%s/%s" at the top of the page. issues.review.review = Review issues.review.reviewers = Reviewers issues.review.outdated = Outdated @@ -1727,12 +1731,12 @@ milestones.desc = Description milestones.due_date = Due Date (optional) milestones.clear = Clear milestones.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'." -milestones.create_success = The milestone '%s' has been created. +milestones.create_success = The milestone "%s" has been created. milestones.edit = Edit Milestone milestones.edit_subheader = Milestones organize issues and track progress. milestones.cancel = Cancel milestones.modify = Update Milestone -milestones.edit_success = Milestone '%s' has been updated. +milestones.edit_success = Milestone "%s" has been updated. milestones.deletion = Delete Milestone milestones.deletion_desc = Deleting a milestone removes it from all related issues. Continue? milestones.deletion_success = The milestone has been deleted. @@ -1743,7 +1747,7 @@ milestones.filter_sort.most_complete = Most complete milestones.filter_sort.most_issues = Most issues milestones.filter_sort.least_issues = Least issues -signing.will_sign = This commit will be signed with key '%s' +signing.will_sign = This commit will be signed with key "%s" signing.wont_sign.error = There was an error whilst checking if the commit could be signed signing.wont_sign.nokey = There is no key available to sign this commit signing.wont_sign.never = Commits are never signed @@ -1779,9 +1783,9 @@ wiki.file_revision = Page Revision wiki.wiki_page_revisions = Wiki Page Revisions wiki.back_to_wiki = Back to wiki page wiki.delete_page_button = Delete Page -wiki.delete_page_notice_1 = Deleting the wiki page '%s' cannot be undone. Continue? +wiki.delete_page_notice_1 = Deleting the wiki page "%s" cannot be undone. Continue? wiki.page_already_exists = A wiki page with the same name already exists. -wiki.reserved_page = The wiki page name '%s' is reserved. +wiki.reserved_page = The wiki page name "%s" is reserved. wiki.pages = Pages wiki.last_updated = Last updated %s wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'. @@ -2128,7 +2132,7 @@ settings.title = Title settings.deploy_key_content = Content settings.key_been_used = A deploy key with identical content is already in use. settings.key_name_used = A deploy key with the same name already exists. -settings.add_key_success = The deploy key '%s' has been added. +settings.add_key_success = The deploy key "%s" has been added. settings.deploy_key_deletion = Remove Deploy Key settings.deploy_key_deletion_desc = Removing a deploy key will revoke its access to this repository. Continue? settings.deploy_key_deletion_success = The deploy key has been removed. @@ -2177,9 +2181,9 @@ settings.protect_unprotected_file_patterns = "Unprotected file patterns (separat settings.protect_unprotected_file_patterns_desc = "Unprotected files that are allowed to be changed directly if user has write access, bypassing push restriction. Multiple patterns can be separated using semicolon (';'). See github.com/gobwas/glob documentation for pattern syntax. Examples: .drone.yml, /docs/**/*.txt." settings.add_protected_branch = Enable protection settings.delete_protected_branch = Disable protection -settings.update_protect_branch_success = Branch protection for rule '%s' has been updated. -settings.remove_protected_branch_success = Branch protection for rule '%s' has been removed. -settings.remove_protected_branch_failed = Removing branch protection rule '%s' failed. +settings.update_protect_branch_success = Branch protection for rule "%s" has been updated. +settings.remove_protected_branch_success = Branch protection for rule "%s" has been removed. +settings.remove_protected_branch_failed = Removing branch protection rule "%s" failed. settings.protected_branch_deletion = Delete Branch Protection settings.protected_branch_deletion_desc = Disabling branch protection allows users with write permission to push to the branch. Continue? settings.block_rejected_reviews = Block merge on rejected reviews @@ -2364,42 +2368,42 @@ release.tags_for = Tags for %s branch.name = Branch Name branch.search = Search branches -branch.already_exists = A branch named '%s' already exists. +branch.already_exists = A branch named "%s" already exists. branch.delete_head = Delete -branch.delete = Delete Branch '%s' +branch.delete = Delete Branch "%s" branch.delete_html = Delete Branch branch.delete_desc = Deleting a branch is permanent. It CANNOT be undone. Continue? -branch.deletion_success = Branch '%s' has been deleted. -branch.deletion_failed = Failed to delete branch '%s'. -branch.delete_branch_has_new_commits = Branch '%s' cannot be deleted because new commits have been added after merging. +branch.deletion_success = Branch "%s" has been deleted. +branch.deletion_failed = Failed to delete branch "%s". +branch.delete_branch_has_new_commits = Branch "%s" cannot be deleted because new commits have been added after merging. branch.create_branch = Create branch %s -branch.create_from = from '%s' -branch.create_success = Branch '%s' has been created. -branch.branch_already_exists = Branch '%s' already exists in this repository. -branch.branch_name_conflict = Branch name '%s' conflicts with the already existing branch '%s'. -branch.tag_collision = Branch '%s' cannot be created as a tag with same name already exists in the repository. +branch.create_from = from "%s" +branch.create_success = Branch "%s" has been created. +branch.branch_already_exists = Branch "%s" already exists in this repository. +branch.branch_name_conflict = Branch name "%s" conflicts with the already existing branch "%s". +branch.tag_collision = Branch "%s" cannot be created as a tag with same name already exists in the repository. branch.deleted_by = Deleted by %s -branch.restore_success = Branch '%s' has been restored. -branch.restore_failed = Failed to restore branch '%s'. -branch.protected_deletion_failed = Branch '%s' is protected. It cannot be deleted. -branch.default_deletion_failed = Branch '%s' is the default branch. It cannot be deleted. -branch.restore = Restore Branch '%s' -branch.download = Download Branch '%s' +branch.restore_success = Branch "%s" has been restored. +branch.restore_failed = Failed to restore branch "%s". +branch.protected_deletion_failed = Branch "%s" is protected. It cannot be deleted. +branch.default_deletion_failed = Branch "%s" is the default branch. It cannot be deleted. +branch.restore = Restore Branch "%s" +branch.download = Download Branch "%s" branch.included_desc = This branch is part of the default branch branch.included = Included branch.create_new_branch = Create branch from branch: branch.confirm_create_branch = Create branch branch.create_branch_operation = Create branch branch.new_branch = Create new branch -branch.new_branch_from = Create new branch from '%s' +branch.new_branch_from = Create new branch from "%s" branch.renamed = Branch %s was renamed to %s. tag.create_tag = Create tag %s tag.create_tag_operation = Create tag tag.confirm_create_tag = Create tag -tag.create_tag_from = Create new tag from '%s' +tag.create_tag_from = Create new tag from "%s" -tag.create_success = Tag '%s' has been created. +tag.create_success = Tag "%s" has been created. topic.manage_topics = Manage Topics topic.done = Done @@ -2436,8 +2440,8 @@ team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) -form.name_reserved = The organization name '%s' is reserved. -form.name_pattern_not_allowed = The pattern '%s' is not allowed in an organization name. +form.name_reserved = The organization name "%s" is reserved. +form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. settings = Settings @@ -2646,7 +2650,7 @@ users.created = Created users.last_login = Last Sign-In users.never_login = Never Signed-In users.send_register_notify = Send User Registration Notification -users.new_success = The user account '%s' has been created. +users.new_success = The user account "%s" has been created. users.edit = Edit users.auth_source = Authentication Source users.local = Local @@ -2846,7 +2850,7 @@ auths.tip.yandex = Create a new application at https://oauth.yandex.com/client/n auths.tip.mastodon = Input a custom instance URL for the mastodon instance you want to authenticate with (or use the default one) auths.edit = Edit Authentication Source auths.activated = This Authentication Source is Activated -auths.new_success = The authentication '%s' has been added. +auths.new_success = The authentication "%s" has been added. auths.update_success = The authentication source has been updated. auths.update = Update Authentication Source auths.delete = Delete Authentication Source @@ -2854,7 +2858,7 @@ auths.delete_auth_title = Delete Authentication Source auths.delete_auth_desc = Deleting an authentication source prevents users from using it to sign in. Continue? auths.still_in_used = The authentication source is still in use. Convert or delete any users using this authentication source first. auths.deletion_success = The authentication source has been deleted. -auths.login_source_exist = The authentication source '%s' already exists. +auths.login_source_exist = The authentication source "%s" already exists. auths.login_source_of_type_exist = An authentication source of this type already exists. auths.unable_to_initialize_openid = Unable to initialize OpenID Connect Provider: %s auths.invalid_openIdConnectAutoDiscoveryURL = Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://) @@ -2947,8 +2951,8 @@ config.mailer_sendmail_timeout = Sendmail Timeout config.mailer_use_dummy = Dummy config.test_email_placeholder = Email (e.g. test@example.com) config.send_test_mail = Send Testing Email -config.test_mail_failed = Failed to send a testing email to '%s': %v -config.test_mail_sent = A testing email has been sent to '%s'. +config.test_mail_failed = Failed to send a testing email to "%s": %v +config.test_mail_sent = A testing email has been sent to "%s". config.oauth_config = OAuth Configuration config.oauth_enabled = Enabled @@ -3129,8 +3133,6 @@ starred_repo = starred %[2]s watched_repo = started watching %[2]s [tool] -ago = %s ago -from_now = %s from now now = now future = future 1s = 1 second @@ -3341,7 +3343,7 @@ name = Name creation = Add Secret creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. -creation.success = The secret '%s' has been added. +creation.success = The secret "%s" has been added. creation.failed = Failed to add secret. deletion = Remove secret deletion.description = Removing a secret is permanent and cannot be undone. Continue? diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 719e4031fe3d8..00dcd046f64da 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -107,7 +107,6 @@ never=Nunca rss_feed=Fuentes RSS - [aria] [editor] @@ -2990,8 +2989,6 @@ starred_repo=destacó %[2]s watched_repo=comenzó a seguir %[2]s [tool] -ago=hace %s -from_now=desde ahora %s now=ahora future=futuro 1s=1 segundo diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index b9be06c23dc61..e4857e97db80c 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -92,7 +92,6 @@ error404=صفحه موردنظر شما یا وجود ندارد%[2]s watched_repo=شروع به تماشای %[2]s کرد [tool] -ago=%s پیش -from_now=%s از هم اکنون now=حالا future=آینده 1s=۱ ثانیه diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 0d05ffba6ccfa..ffb0896f9f97e 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -106,7 +106,6 @@ never=Ei koskaan rss_feed=RSS-syöte - [aria] [editor] @@ -1727,8 +1726,6 @@ compare_commits_general=Vertaa committeja create_branch=loi haaran %[3]s repossa %[4]s [tool] -ago=%s sitten -from_now=%s alkaen nyt now=nyt 1s=1 sekunti 1m=1 minuutti diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index fdda0d28918a8..689000709a38b 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -107,7 +107,6 @@ never=Jamais rss_feed=Flux RSS - [aria] [editor] @@ -2522,8 +2521,6 @@ publish_release=`a publié "%[4]s" à %[3] review_dismissed_reason=Raison : [tool] -ago=il y a %s -from_now=dans %s now=maintenant future=futur 1s=1 seconde diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 7e35825ad6d1d..5692f144246af 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -82,7 +82,6 @@ error404=Az elérni kívánt oldal vagy nem létezik, vagy %[3]s compare_commits=Bandingkan %d melakukan [tool] -ago=%s yang lalu -from_now=%s mulai sekarang now=sekarang future=masa depan 1s=1 detik diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index ade8d84efc52d..d4695364d4129 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -103,7 +103,6 @@ error404=Síðan sem þú ert að reyna að fá annað hvort er ekki til never=Aldrei - [aria] [editor] @@ -1319,7 +1318,6 @@ comment_pull=`gerði ummæli á sameiningarbeiðni %[3]s#%[2]s%[2]s watched_repo=ha iniziato a guardare %[2]s [tool] -ago=%s fa -from_now=%s da adesso now=ora future=futuro 1s=1 secondo diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 2976b2691f197..8bbc2a5a34feb 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -112,7 +112,6 @@ never=無し rss_feed=RSSフィード - [aria] navbar=ナビゲーションバー footer=フッター @@ -3105,8 +3104,6 @@ starred_repo=が %[2]s にスターをつけました watched_repo=が %[2]s のウォッチを開始しました [tool] -ago=%s前 -from_now=今から%s後 now=たった今 future=未来 1s=1秒 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 8d868257a3c6c..5b2bc78a0035c 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -75,7 +75,6 @@ loading=불러오는 중... - [aria] [editor] @@ -1583,8 +1582,6 @@ transfer_repo=%s에서 %s로 저장소가 전송 compare_commits=%d 커밋들 비교 [tool] -ago=%s 전 -from_now=%s 지금부터 now=현재 future=미래 1s=1 초 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index c649a2670e0ad..01b5c783e75e4 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -110,7 +110,6 @@ never=Nekad rss_feed=RSS barotne - [aria] navbar=Navigācijas josla footer=Kājene @@ -3060,8 +3059,6 @@ starred_repo=atzīmēja ar zvaigznīti %[2]s watched_repo=sāka sekot %[2]s [tool] -ago=pirms %s -from_now=pēc %s no šī brīža now=tagad future=nākotnē 1s=1 sekundes diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 068f67dc51ff6..2a1074034f046 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -107,7 +107,6 @@ never=Nooit rss_feed=RSS Feed - [aria] [editor] @@ -2753,8 +2752,6 @@ compare_commits=Vergelijk %d commits compare_commits_general=Vergelijk commits [tool] -ago=%s geleden -from_now=%s vanaf nu now=nu future=toekomst 1s=1 seconde diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 0bb732b159491..5d89b21827fc0 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -105,7 +105,6 @@ error404=Strona, do której próbujesz dotrzeć nie istnieje lu never=Nigdy - [aria] [editor] @@ -2641,8 +2640,6 @@ mirror_sync_delete=synchronizuje i usuwa odwołanie %[2]s w %[2]s watched_repo=começou a observar %[2]s [tool] -ago=%s atrás -from_now=%s a partir de agora now=agora future=futuro 1s=1 segundo diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4339b3d19ac64..85c243cabcd00 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -112,8 +112,6 @@ never=Nunca rss_feed=Fonte RSS -on_date=no dia - [aria] navbar=Barra de navegação footer=Rodapé @@ -133,6 +131,8 @@ buttons.list.task.tooltip=Adicionar uma lista de tarefas buttons.mention.tooltip=Mencionar um utilizador ou uma equipa buttons.ref.tooltip=Referenciar uma questão ou um pedido de integração buttons.switch_to_legacy.tooltip=Usar o editor clássico +buttons.enable_monospace_font=Habilitar tipo de letra mono-espaçado +buttons.disable_monospace_font=Desabilitar tipo de letra mono-espaçado [filter] string.asc=A - Z @@ -3127,8 +3127,6 @@ starred_repo=juntou %[2]s aos favoritos watched_repo=começou a vigiar %[2]s [tool] -ago=há %s -from_now=daqui a %s now=agora future=futuro 1s=1 segundo diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index b74ed1609453b..0141199138f3a 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -4,7 +4,7 @@ explore=Обзор help=Помощь logo=Логотип sign_in=Вход -sign_in_with=Войдите с помощью +sign_in_with=Войти с помощью sign_out=Выход sign_up=Регистрация link_account=Привязать аккаунт @@ -25,14 +25,14 @@ licenses=Лицензии return_to_gitea=Вернуться к Gitea username=Имя пользователя -email=Адрес эл. почты +email=Адрес электронной почты password=Пароль access_token=Токен доступа re_type=Введите пароль еще раз captcha=CAPTCHA twofa=Двухфакторная аутентификация twofa_scratch=Двухфакторный scratch-код -passcode=Пароль +passcode=Код webauthn_insert_key=Вставьте ваш ключ безопасности webauthn_sign_in=Нажмите кнопку на ключе безопасности. Если ваш ключ безопасности не имеет кнопки, вставьте его снова. @@ -41,11 +41,11 @@ webauthn_use_twofa=Используйте двухфакторный код с webauthn_error=Не удалось прочитать ваш ключ безопасности. webauthn_unsupported_browser=Ваш браузер в настоящее время не поддерживает WebAuthn. webauthn_error_unknown=Произошла неизвестная ошибка. Повторите попытку. -webauthn_error_insecure=`WebAuthn поддерживает только безопасные соединения. Для тестирования по HTTP, вы можете использовать "localhost" или "127.0.0.1"` +webauthn_error_insecure=`WebAuthn поддерживает только безопасные соединения. Для тестирования по HTTP можно использовать "localhost" или "127.0.0.1"` webauthn_error_unable_to_process=Сервер не смог обработать ваш запрос. -webauthn_error_duplicated=Представленный ключ не подходит для этого запроса. Если вы пытаетесь зарегистрировать его, убедитесь, что ключ ещё не зарегистрирован. -webauthn_error_empty=Вы должны указать имя для этого ключа. -webauthn_error_timeout=Тайм-аут достигнут до того, как ваш ключ был прочитан. Перезагрузите эту страницу и повторите попытку. +webauthn_error_duplicated=Данный ключ безопасности не разрешен для этого запроса. Пожалуйста, убедитесь, что ключ не регистрировался ранее. +webauthn_error_empty=Необходимо задать имя для этого ключа. +webauthn_error_timeout=Время истекло раньше, чем ключ был прочитан. Перезагрузите эту страницу и повторите попытку. webauthn_reload=Обновить repository=Репозиторий @@ -91,7 +91,7 @@ enabled=Включено disabled=Отключен copy=Скопировать -copy_url=Копировать URL +copy_url=Скопировать URL copy_content=Скопировать содержимое copy_branch=Скопировать имя ветки copy_success=Скопировано! @@ -106,13 +106,12 @@ step1=Шаг 1: step2=Шаг 2: error=Ошибка -error404=Страница, которую вы пытаетесь открыть, либо не существует, либо недостаточно прав для ее просмотра. +error404=Либо страница, которую вы пытаетесь открыть, не существует, либо у вас недостаточно прав для ее просмотра. never=Никогда rss_feed=RSS-лента - [aria] navbar=Панель навигации footer=Подвал @@ -120,6 +119,20 @@ footer.software=О программе footer.links=Ссылки [editor] +buttons.heading.tooltip=Добавить заголовок +buttons.bold.tooltip=Добавить жирный текст +buttons.italic.tooltip=Добавить курсив +buttons.quote.tooltip=Цитировать текст +buttons.code.tooltip=Добавить код +buttons.link.tooltip=Добавить ссылку +buttons.list.unordered.tooltip=Добавить маркированный список +buttons.list.ordered.tooltip=Добавить нумерованный список +buttons.list.task.tooltip=Добавить список задач +buttons.mention.tooltip=Упомянуть пользователя или команду +buttons.ref.tooltip=Сослаться на задачу или запрос на слияние +buttons.switch_to_legacy.tooltip=Использовать старый редактор +buttons.enable_monospace_font=Включить моноширинный шрифт +buttons.disable_monospace_font=Выключить моноширинный шрифт [filter] string.asc=A - Я @@ -128,8 +141,8 @@ string.desc=Я - А [error] occurred=Произошла ошибка report_message=Если вы уверены, что это баг Gitea, пожалуйста, поищите задачу на GitHub или создайте новую при необходимости. -missing_csrf=Некорректный запрос: CSRF токен отсутствует -invalid_csrf=Некорректный запрос: неверный CSRF токен +missing_csrf=Некорректный запрос: отсутствует токен CSRF +invalid_csrf=Некорректный запрос: неверный токен CSRF network_error=Ошибка сети [startpage] @@ -137,7 +150,7 @@ app_desc=Удобный сервис собственного хостинга install=Простой в установке install_desc=Просто запустите исполняемый файл для вашей платформы, разверните через Docker, или установите с помощью менеджера пакетов. platform=Кроссплатформенный -platform_desc=Gitea работает на любой операционной системе, которая может компилировать Go: Windows, macOS, Linux, ARM и т. д. Выбирайте, что вам больше нравится! +platform_desc=Gitea работает на любой платформе, поддерживаемой Go: Windows, macOS, Linux, ARM и т. д. Выбирайте, что вам больше нравится! lightweight=Легковесный lightweight_desc=Gitea имеет низкие системные требования и может работать на недорогом Raspberry Pi. Экономьте энергию вашей машины! license=Открытый исходный код @@ -154,7 +167,7 @@ host=Хост user=Имя пользователя password=Пароль db_name=Имя базы данных -db_helper=Для пользователей MySQL: пожалуйста, используйте движок InnoDB, и если вы используете "utf8mb4" - ваша версия InnoDB должна быть старше 5.6 . +db_helper=Для пользователей MySQL: пожалуйста, используйте хранилище InnoDB, и если вы используете "utf8mb4", версия InnoDB должна быть выше 5.6 . db_schema=Схема db_schema_helper=Оставьте пустым для значения по умолчанию ("public"). ssl_mode=SSL @@ -179,8 +192,8 @@ app_name=Название сайта app_name_helper=Здесь вы можете ввести название своей компании. repo_path=Путь до корня репозитория repo_path_helper=Все удалённые Git репозитории будут сохранены в эту директорию. -lfs_path=Корневой путь Git LFS -lfs_path_helper=В этой директории будут храниться файлы Git LFS. Оставьте пустым, чтобы отключить LFS. +lfs_path=Путь к корневому каталогу Git LFS +lfs_path_helper=В этом каталоге будут храниться файлы Git LFS. Оставьте пустым, чтобы отключить LFS. run_user=Запуск от имени пользователя run_user_helper=Введите имя пользователя операционной системы, под которым работает Gitea. Обратите внимание, что этот пользователь должен иметь доступ к корневому пути репозиториев. domain=Домен сервера @@ -287,6 +300,7 @@ search=Поиск code=Код search.type.tooltip=Тип поиска search.fuzzy=Неточный +search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу search.match=Соответствие search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу code_search_unavailable=В настоящее время поиск по коду недоступен. Обратитесь к администратору сайта. @@ -1029,6 +1043,7 @@ issues=Задачи pulls=Запросы на слияние project_board=Проекты packages=Пакеты +actions=Действия labels=Метки org_labels_desc=Метки уровня организации, которые можно использовать с всеми репозиториями< / strong> в этой организации org_labels_desc_manage=управлять @@ -1247,6 +1262,7 @@ issues.choose.blank=По умолчанию issues.choose.blank_about=Создать запрос из шаблона по умолчанию. issues.choose.ignore_invalid_templates=Некорректные шаблоны были проигнорированы issues.choose.invalid_templates=Найден(ы) %v неверный(х) шаблон(ов) +issues.choose.invalid_config=Конфигурация задачи содержит ошибки: issues.no_ref=Не указана ветка или тэг issues.create=Добавить задачу issues.new_label=Новая метка @@ -1265,9 +1281,9 @@ issues.remove_labels=удалил(а) метки %s %s issues.add_remove_labels=добавил(а) метки %s и удалил(а) %s %s issues.add_milestone_at=`добавил(а) к этапу %s %s` issues.add_project_at=`добавил(а) в %s проект %s` -issues.change_milestone_at=`поменял(а) целевой этап с %s на %s %s` +issues.change_milestone_at=`изменил(а) целевой этап с %s на %s %s` issues.change_project_at=`изменил(а) проект с %s на %s %s` -issues.remove_milestone_at=`удалил(а) из этапа %s %s` +issues.remove_milestone_at=`удалил(а) это из этапа %s %s` issues.remove_project_at=`удалил(а) это из проекта %s %s` issues.deleted_milestone=`(удалено)` issues.deleted_project=`(удалено)` @@ -1421,9 +1437,9 @@ issues.start_tracking_history=`начал(а) работать %s` issues.tracker_auto_close=Таймер будет остановлен автоматически, когда эта проблема будет закрыта issues.tracking_already_started=`Вы уже начали отслеживать время для другой задачи!` issues.stop_tracking=Остановить таймер -issues.stop_tracking_history=`перестал работать %s` +issues.stop_tracking_history=`перестал(а) работать %s` issues.cancel_tracking=Отмена -issues.cancel_tracking_history=`отменил отслеживание %s` +issues.cancel_tracking_history=`отменил(а) отслеживание %s` issues.add_time=Вручную добавить время issues.del_time=Удалить этот журнал времени issues.add_time_short=Добавить время @@ -1461,7 +1477,7 @@ issues.dependency.cancel=Отменить issues.dependency.remove=Удалить issues.dependency.remove_info=Удалить эту зависимость issues.dependency.added_dependency=`добавил(а) новую зависимость %s` -issues.dependency.removed_dependency=`убрал зависимость %s` +issues.dependency.removed_dependency=`убрал(а) зависимость %s` issues.dependency.pr_closing_blockedby=Закрытие этого запроса на слияние блокируется следующими задачами issues.dependency.issue_closing_blockedby=Закрытие этой задачи блокируется следующими задачами issues.dependency.issue_close_blocks=Эта задача блокирует закрытие следующих задач @@ -1525,6 +1541,8 @@ pulls.allow_edits_from_maintainers=Разрешить редактировани pulls.allow_edits_from_maintainers_desc=Пользователи с доступом на запись в основную ветку могут отправлять изменения и в эту ветку pulls.allow_edits_from_maintainers_err=Не удалось обновить pulls.compare_changes_desc=Сравнить две ветки и создать запрос на слияние для изменений. +pulls.has_viewed_file=Просмотрено +pulls.viewed_files_label=%[1]d из %[2]d файлов просмотрено pulls.expand_files=Показать все файлы pulls.collapse_files=Свернуть все файлы pulls.compare_base=базовая ветка @@ -1539,7 +1557,7 @@ pulls.has_pull_request=`Запрос на слияние этих веток у pulls.create=Создать запрос на слияние pulls.title_desc=хочет влить %[1]d коммит(ов) из %[2]s в %[3]s pulls.merged_title_desc=слито %[1]d коммит(ов) из %[2]s в %[3]s %[4]s -pulls.change_target_branch_at=`изменил целевую ветку с %s на %s %s` +pulls.change_target_branch_at=`изменил(а) целевую ветку с %s на %s %s` pulls.tab_conversation=Обсуждение pulls.tab_commits=Коммиты pulls.tab_files=Изменённые файлы @@ -1572,6 +1590,7 @@ pulls.can_auto_merge_desc=Этот запрос на слияние может pulls.cannot_auto_merge_desc=Этот запрос на слияние не может быть объединён автоматически. pulls.cannot_auto_merge_helper=Пожалуйста, совершите слияние вручную для урегулирования конфликтов. pulls.num_conflicting_files_1=%d конфликтующий файл +pulls.num_conflicting_files_n=%d конфликтующих файлов pulls.approve_count_1=%d одобрение pulls.reject_count_1=%d запрос на изменение pulls.reject_count_n=%d запросов на изменение @@ -1620,10 +1639,23 @@ pulls.reopened_at=`переоткрыл этот запрос на слияни pulls.merge_instruction_hint=`Вы также можете просмотреть инструкции командной строки.` pulls.merge_instruction_step1_desc=В репозитории вашего проекта посмотрите новую ветку и протестируйте изменения. pulls.merge_instruction_step2_desc=Объединить изменения и обновить на Gitea. +pulls.clear_merge_message=Очистить сообщение о слиянии +pulls.clear_merge_message_hint=Очистка сообщения о слиянии удалит только содержимое сообщения коммита, но сохранит сгенерированные git добавки, такие как "Co-Authored-By …". +pulls.auto_merge_button_when_succeed=(При успешных проверках) +pulls.auto_merge_when_succeed=Слить автоматически после прохождения всех проверок +pulls.auto_merge_newly_scheduled=Запрос был запланирован для слияния после прохождения всех проверок. +pulls.auto_merge_has_pending_schedule=%[1]s запланировал этот запрос для автоматического слияния, когда все проверки пройдены %[2]s. +pulls.auto_merge_cancel_schedule=Отменить автоматическое слияние +pulls.auto_merge_not_scheduled=Этот запрос не запланирован для автоматического слияния. +pulls.auto_merge_canceled_schedule=Автоматическое слияние для этого запроса было отменено. +pulls.auto_merge_newly_scheduled_comment=`запланировал этот запрос для автоматического слияния после прохождения всех проверок %[1]s` +pulls.auto_merge_canceled_schedule_comment=`отменил автоматическое слияние этого запроса после прохождения всех проверок %[1]s` +pulls.delete.title=Удалить этот запрос на слияние? +pulls.delete.text=Вы действительно хотите удалить этот запрос на слияние? (Это навсегда удалит всё содержимое. Возможно, лучше закрыть запрос в архивных целях.) milestones.new=Новый этап milestones.closed=Закрыт %s @@ -1669,6 +1701,7 @@ signing.wont_sign.commitssigned=Слияние не будет подписан signing.wont_sign.approved=Слияние не будет подписано, так как PR не одобрен signing.wont_sign.not_signed_in=Вы не авторизовались +ext_wiki=Доступ к внешней вики ext_wiki.desc=Ссылка на внешнюю вики. wiki=Вики @@ -1765,6 +1798,7 @@ search=Поиск search.search_repo=Поиск по репозиторию search.type.tooltip=Тип поиска search.fuzzy=Неточный +search.fuzzy.tooltip=Включать результаты, которые не полностью соответствуют поисковому запросу search.match=Соответствие search.match.tooltip=Включать только результаты, которые точно соответствуют поисковому запросу search.results=Результаты поиска "%s" в %s @@ -1798,6 +1832,7 @@ settings.mirror_sync_in_progress=Синхронизируются репозит settings.site=Сайт settings.update_settings=Обновить настройки settings.branches.update_default_branch=Обновить ветку по умолчанию +settings.branches.add_new_rule=Добавить новое правило settings.advanced_settings=Расширенные настройки settings.wiki_desc=Включить Вики для репозитория settings.use_internal_wiki=Использовать встроенную вики-систему @@ -1816,15 +1851,22 @@ settings.tracker_url_format_error=Формат URL внешнего баг-тр settings.tracker_issue_style=Формат нумерации для внешней системы учета задач settings.tracker_issue_style.numeric=Цифровой settings.tracker_issue_style.alphanumeric=Буквенноцифровой +settings.tracker_issue_style.regexp=Регулярное выражение +settings.tracker_issue_style.regexp_pattern=Шаблон регулярного выражения +settings.tracker_issue_style.regexp_pattern_desc=Вместо {index} будет использоваться первая захваченная группа. settings.tracker_url_format_desc=Вы можете использовать шаблоны {user}, {repo} и {index} для имени пользователя, репозитория и номера задачи. settings.enable_timetracker=Включить отслеживание времени settings.allow_only_contributors_to_track_time=Учитывать только участников разработки в подсчёте времени settings.pulls_desc=Включить запросы на слияние settings.pulls.ignore_whitespace=Игнорировать незначащие изменения (пробелы, табуляция) при проверке на конфликты слияния settings.pulls.enable_autodetect_manual_merge=Включить автоопределение ручного слияния (Примечание: в некоторых особых случаях могут возникнуть ошибки) +settings.pulls.allow_rebase_update=Включить обновление ветки из запроса на слияние путём rebase settings.pulls.default_delete_branch_after_merge=Удалить ветку запроса после его слияния по умолчанию settings.pulls.default_allow_edits_from_maintainers=По умолчанию разрешать редактирование сопровождающими +settings.releases_desc=Включить релизы +settings.packages_desc=Включить реестр пакетов settings.projects_desc=Включить проекты репозитория +settings.actions_desc=Включить действия репозитория settings.admin_settings=Настройки администратора settings.admin_enable_health_check=Выполнять проверки целостности этого репозитория (git fsck) settings.admin_code_indexer=Индексатор кода @@ -1886,6 +1928,7 @@ settings.confirm_delete=Удалить репозиторий settings.add_collaborator=Добавить соавтора settings.add_collaborator_success=Соавтор добавлен. settings.add_collaborator_inactive_user=Невозможно добавить неактивного пользователя как соавтора. +settings.add_collaborator_owner=Невозможно добавить владельца в качестве соавтора. settings.add_collaborator_duplicate=Соавтор уже добавлен в этот репозиторий. settings.delete_collaborator=Удалить settings.collaborator_deletion=Удалить соавтора @@ -1917,6 +1960,7 @@ settings.webhook.headers=Заголовки settings.webhook.payload=Содержимое settings.webhook.body=Тело ответа settings.webhook.replay.description=Повторить этот веб-хук. +settings.webhook.delivery.success=Событие было добавлено в очередь доставки. Может пройти несколько секунд, прежде чем оно отобразится в истории. settings.githooks_desc=Git-хуки предоставляются самим Git. Вы можете изменять файлы хуков из списка ниже, чтобы настроить собственные операции. settings.githook_edit_desc=Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведёт к отключению хука. settings.githook_name=Название хукa @@ -1981,6 +2025,7 @@ settings.event_package_desc=Пакет создан или удален в ре settings.branch_filter=Фильтр веток settings.branch_filter_desc=Белый список ветвей для событий Push, создания ветвей и удаления ветвей, указанных в виде глоб-шаблона. Если пустой или *, то все событий для всех ветвей будут зарегистрированы. Перейдите по ссылке github.com/gobwas/glob на документацию по синтаксису. Примеры: master, {master,release*}. settings.authorization_header=Заголовок Authorization +settings.authorization_header_desc=Будет включён в качестве заголовка авторизации для запросов. Примеры: %s. settings.active=Активный settings.active_helper=Информация о происходящих событиях будет отправляться на URL этого веб-хука. settings.add_hook_success=Веб-хук был добавлен. @@ -2020,6 +2065,8 @@ settings.key_been_used=Идентичный ключ развёртывания settings.key_name_used=Ключ развёртывания с таким именем уже существует. settings.add_key_success=Новый ключ развёртывания '%s' успешно добавлен. settings.deploy_key_deletion=Удалить ключ развёртывания +settings.deploy_key_deletion_desc=Удаление ключа развёртывания сделает невозможным доступ к репозиторию с его помощью. Вы уверены? +settings.deploy_key_deletion_success=Ключ развёртывания был успешно удалён. settings.branches=Ветки settings.protected_branch=Защита веток settings.protected_branch.save_rule=Сохранить правило @@ -2573,6 +2620,7 @@ repos.issues=Задачи repos.size=Размер packages.package_manage_panel=Управление пакетами +packages.total_size=Общий размер: %s packages.owner=Владелец packages.creator=Автор packages.name=Наименование @@ -2942,6 +2990,7 @@ reopen_pull_request=`переоткрыл(а) запрос на слияние < comment_issue=`прокомментировал(а) задачу %[3]s#%[2]s` comment_pull=`прокомментировал(а) запрос на слияние %[3]s#%[2]s` merge_pull_request=`принял(а) запрос на слияние %[3]s#%[2]s` +auto_merge_pull_request=`автоматически принял(а) запрос на слияние %[3]s#%[2]s` transfer_repo=передал(а) репозиторий %s %s push_tag=создал(а) тег %[3]s в %[4]s delete_tag=удалил(а) тэг %[2]s из %[3]s @@ -2962,8 +3011,6 @@ starred_repo=добавил(а) %[2]s в избранное watched_repo=начала(а) наблюдение за %[2]s [tool] -ago=%s назад -from_now=%s с этого момента now=сейчас future=в будущем 1s=1 секунду @@ -3035,11 +3082,20 @@ keywords=Ключевые слова details=Подробнее details.author=Автор details.project_site=Сайт проекта +details.repository_site=Сайт репозитория +details.documentation_site=Сайт документации details.license=Лицензия versions=Версии versions.view_all=Показать всё dependency.version=Версия +cargo.registry=Настройте этот реестр в файле конфигурации Cargo (например, ~/.cargo/config.toml): +cargo.install=Чтобы установить пакет с помощью Cargo, выполните следующую команду: +cargo.documentation=Для получения дополнительной информации о реестре Cargo смотрите документацию. +cargo.details.repository_site=Сайт репозитория +cargo.details.documentation_site=Сайт документации +chef.registry=Настройте этот реестр в своём файле ~/.chef/config.rb: chef.install=Чтобы установить пакет, выполните следующую команду: +chef.documentation=Для получения дополнительной информации о реестре Chef смотрите документацию. composer.registry=Настройте этот реестр в файле ~/.composer/config.json: composer.install=Чтобы установить пакет с помощью Composer, выполните следующую команду: composer.documentation=Для получения дополнительной информации о реестре Composer смотрите документацию. @@ -3048,6 +3104,11 @@ conan.details.repository=Репозиторий conan.registry=Настроить реестр из командной строки: conan.install=Чтобы установить пакет с помощью Conan, выполните следующую команду: conan.documentation=Для получения дополнительной информации о реестре Conan смотрите документацию. +conda.registry=Пропишите этот реестр в качестве репозитория Conda в своём файле .condarc: +conda.install=Чтобы установить пакет с помощью Conda, выполните следующую команду: +conda.documentation=Для получения дополнительной информации о реестре Conda смотрите документацию. +conda.details.repository_site=Сайт репозитория +conda.details.documentation_site=Сайт документации container.details.type=Тип образа container.details.platform=Платформа container.digest=Отпечаток: @@ -3056,11 +3117,13 @@ container.multi_arch=ОС / архитектура container.labels=Метки container.labels.key=Ключ container.labels.value=Значение +generic.download=Скачать пакет из командной строки: generic.documentation=Для получения дополнительной информации об общем реестре смотрите документацию. helm.registry=Настроить реестр из командной строки: helm.install=Чтобы установить пакет, выполните следующую команду: helm.documentation=Для получения дополнительной информации о реестре Helm смотрите документацию. maven.install2=Выполнить через командную строку: +maven.download=Чтобы скачать зависимость, запустите в командной строке: nuget.registry=Настроить реестр из командной строки: nuget.install=Чтобы установить пакет с помощью NuGet, выполните следующую команду: nuget.documentation=Для получения дополнительной информации о реестре NuGet смотрите документацию. @@ -3071,6 +3134,8 @@ npm.dependencies=Зависимости npm.dependencies.peer=Одноранговые зависимости npm.dependencies.optional=Необязательные зависимости npm.details.tag=Тег +pub.install=Чтобы установить пакет с помощью Dart, выполните следующую команду: +pub.documentation=Для получения дополнительной информации о реестре Conda смотрите документацию. pypi.requires=Требуется Python pypi.documentation=Для получения дополнительной информации о реестре PyPI смотрите документацию. rubygems.install=Чтобы установить пакет с помощью gem, выполните следующую команду: diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 99a43974ef1f7..6771d1181390c 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -92,7 +92,6 @@ error404=ඔබ ළඟා වීමට උත්සාහ කරන පිටු never=කිසි විටෙකත් - [aria] [editor] @@ -2678,8 +2677,6 @@ create_branch=නිර්මාණය කරන ලද ශාඛාව %[2]sනැරඹීමට පටන් ගත්තා [tool] -ago=%s කලින් -from_now=%s මෙතැන් සිට now=දැන් future=අනාගතය 1s=තත්පර 1 diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 9e38834b7d7af..76cc71c940a94 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -106,7 +106,6 @@ never=Nikdy rss_feed=RSS kanál - [aria] [editor] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 0d4a0036cf5db..cf3056ae3d569 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -81,7 +81,6 @@ error404=Sidan du försöker nå finns inte eller så h - [aria] [editor] @@ -2110,8 +2109,6 @@ compare_commits_general=Jämför commits mirror_sync_delete=synkade och raderade referens %[2]s%[3]s från spegel [tool] -ago=%s sedan -from_now=%s från och med nu now=nu future=framtiden 1s=1 sekund diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index a5e7061d49211..858fdea231710 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -110,7 +110,6 @@ never=Asla rss_feed=RSS Beslemesi - [aria] navbar=Gezinti Çubuğu footer=Alt Bilgi @@ -2988,8 +2987,6 @@ starred_repo=%[2]s deposuna yıldız bıraktı watched_repo=%[2]s deposunu izlemeye başladı [tool] -ago=%s önce -from_now=%s şu andan now=şimdi future=gelecek 1s=1 saniye diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 6b6aa67371d50..33a3eaeab8ec8 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -93,7 +93,6 @@ error404=Сторінка, до якої ви намагаєтеся зверн never=Ніколи - [aria] [editor] @@ -2750,8 +2749,6 @@ starred_repo=додав %[2]s у обране watched_repo=почав слідкувати за %[2] [tool] -ago=%s тому -from_now=%s з цього моменту now=зараз future=в майбутньому 1s=1 секунда diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7806a195baf7f..c9adca186fcd5 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -112,7 +112,6 @@ never=从不 rss_feed=RSS 订阅源 - [aria] navbar=导航栏 footer=页脚 @@ -3095,8 +3094,6 @@ starred_repo=点赞了 %[2]s watched_repo=开始关注 %[2]s [tool] -ago=%s前 -from_now=%s 之后 now=现在 future=将来 1s=1 秒 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 53ead462a9a85..29530d9467ced 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -51,7 +51,6 @@ enabled=已啟用 - [aria] [editor] @@ -926,8 +925,6 @@ transfer_repo=將儲存庫 %s 轉移至 %s compare_commits=比較 %d 提交 [tool] -ago=%s之前 -from_now=%s之後 now=現在 future=未來 1s=1 秒 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 6dfbe25181a50..ca3990af83ca1 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -112,7 +112,6 @@ never=從來沒有 rss_feed=RSS 摘要 - [aria] navbar=導航列 footer=頁尾 @@ -120,6 +119,8 @@ footer.software=關於軟體 footer.links=連結 [editor] +buttons.enable_monospace_font=啟用等寬字型 +buttons.disable_monospace_font=停用等寬字型 [filter] string.asc=A - Z @@ -1748,6 +1749,8 @@ wiki.create_first_page=建立第一個頁面 wiki.page=頁面 wiki.filter_page=過濾頁面 wiki.new_page=頁面 +wiki.page_title=頁面標題 +wiki.page_content=頁面內容 wiki.default_commit_message=關於此次頁面修改的說明(非必要)。 wiki.save_page=儲存頁面 wiki.last_commit_info=%s 於 %s 修改了此頁面 @@ -3105,8 +3108,6 @@ starred_repo=為 %[2]s 加上星號 watched_repo=開始關注 %[2]s [tool] -ago=%s前 -from_now=%s之後 now=現在 future=未來 1s=1 秒 diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 06bf06b4e8472..5bf5fc8c8bcca 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -651,7 +651,7 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } - if err := issue_service.NewIssue(ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) return @@ -752,7 +752,7 @@ func EditIssue(ctx *context.APIContext) { issue.Content = *form.Body } if form.Ref != nil { - err = issue_service.ChangeIssueRef(issue, ctx.Doer, *form.Ref) + err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref) if err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRef", err) return @@ -790,7 +790,7 @@ func EditIssue(ctx *context.APIContext) { oneAssignee = *form.Assignee } - err = issue_service.UpdateAssignees(issue, oneAssignee, form.Assignees, ctx.Doer) + err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) if err != nil { ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) return @@ -887,7 +887,7 @@ func DeleteIssue(ctx *context.APIContext) { return } - if err = issue_service.DeleteIssue(ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { + if err = issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 9b5ec0b3f8ed6..f4e2969d7d1c2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -534,7 +534,7 @@ func EditPullRequest(ctx *context.APIContext) { // Send an empty array ([]) to clear all assignees from the Issue. if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) { - err = issue_service.UpdateAssignees(issue, form.Assignee, form.Assignees, ctx.Doer) + err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 8f4b9dafec67d..a568cd565a768 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -706,7 +706,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions } for _, reviewer := range reviewers { - comment, err := issue_service.ReviewRequest(pr.Issue, ctx.Doer, reviewer, isAdd) + comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, reviewer, isAdd) if err != nil { ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) return @@ -750,7 +750,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions } for _, teamReviewer := range teamReviewers { - comment, err := issue_service.TeamReviewRequest(pr.Issue, ctx.Doer, teamReviewer, isAdd) + comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) if err != nil { ctx.ServerError("TeamReviewRequest", err) return diff --git a/routers/private/actions.go b/routers/private/actions.go new file mode 100644 index 0000000000000..b7e416f56a33b --- /dev/null +++ b/routers/private/actions.go @@ -0,0 +1,91 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package private + +import ( + "errors" + "fmt" + "net/http" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/util" +) + +// GenerateActionsRunnerToken generates a new runner token for a given scope +func GenerateActionsRunnerToken(ctx *context.PrivateContext) { + var genRequest private.GenerateTokenRequest + rd := ctx.Req.Body + defer rd.Close() + + if err := json.NewDecoder(rd).Decode(&genRequest); err != nil { + log.Error("%v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + return + } + + owner, repo, err := parseScope(ctx, genRequest.Scope) + if err != nil { + log.Error("%v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err.Error(), + }) + } + + token, err := actions_model.GetUnactivatedRunnerToken(ctx, owner, repo) + if errors.Is(err, util.ErrNotExist) { + token, err = actions_model.NewRunnerToken(ctx, owner, repo) + if err != nil { + err := fmt.Sprintf("error while creating runner token: %v", err) + log.Error("%v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, + }) + return + } + } else if err != nil { + err := fmt.Sprintf("could not get unactivated runner token: %v", err) + log.Error("%v", err) + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: err, + }) + return + } + + ctx.PlainText(http.StatusOK, token.Token) +} + +func parseScope(ctx *context.PrivateContext, scope string) (ownerID, repoID int64, err error) { + ownerID = 0 + repoID = 0 + if scope == "" { + return ownerID, repoID, nil + } + + ownerName, repoName, found := strings.Cut(scope, "/") + + u, err := user_model.GetUserByName(ctx, ownerName) + if err != nil { + return ownerID, repoID, err + } + + if !found { + return u.ID, repoID, nil + } + + r, err := repo_model.GetRepositoryByName(u.ID, repoName) + if err != nil { + return ownerID, repoID, err + } + repoID = r.ID + return ownerID, repoID, nil +} diff --git a/routers/private/internal.go b/routers/private/internal.go index 4acede33705d6..b4d32c37a6439 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -77,6 +77,7 @@ func Routes() *web.Route { r.Get("/manager/processes", Processes) r.Post("/mail/send", SendEmail) r.Post("/restore_repo", RestoreRepo) + r.Post("/actions/generate_actions_runner_token", GenerateActionsRunnerToken) return r } diff --git a/routers/web/base.go b/routers/web/base.go index da18a75643e68..79991d89db5ec 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -143,31 +143,29 @@ func Recovery(ctx goctx.Context) func(next http.Handler) http.Handler { "locale": lc, } - user := context.GetContextUser(req) + // TODO: this recovery handler is usually called without Gitea's web context, so we shouldn't touch that context too much + // Otherwise, the 500 page may cause new panics, eg: cache.GetContextWithData, it makes the developer&users couldn't find the original panic + user := context.GetContextUser(req) // almost always nil if user == nil { // Get user from session if logged in - do not attempt to sign-in user = auth.SessionUser(sessionStore) } - if user != nil { - store["IsSigned"] = true - store["SignedUser"] = user - store["SignedUserID"] = user.ID - store["SignedUserName"] = user.Name - store["IsAdmin"] = user.IsAdmin - } else { - store["SignedUserID"] = int64(0) - store["SignedUserName"] = "" - } httpcache.SetCacheControlInHeader(w.Header(), 0, "no-transform") w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) - if !setting.IsProd { + if !setting.IsProd || (user != nil && user.IsAdmin) { store["ErrorMsg"] = combinedErr } + + defer func() { + if err := recover(); err != nil { + log.Error("HTML render in Recovery handler panics again: %v", err) + } + }() err = rnd.HTML(w, http.StatusInternalServerError, "status/500", templates.BaseVars().Merge(store)) if err != nil { - log.Error("%v", err) + log.Error("HTML render in Recovery handler fails again: %v", err) } } }() diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 784940909a612..48875e306d29e 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -7,6 +7,7 @@ import ( "net/http" "path" "strings" + "time" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -32,5 +33,14 @@ func List(ctx *context.Context) { } func Tmpl(ctx *context.Context) { + now := time.Now() + ctx.Data["TimeNow"] = now + ctx.Data["TimePast5s"] = now.Add(-5 * time.Second) + ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second) + ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute) + ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute) + ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second) + ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second) + ctx.HTML(http.StatusOK, base.TplName("devtest"+path.Clean("/"+ctx.Params("sub")))) } diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 3546334ed6479..0e232c194c2e1 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -12,7 +12,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -23,10 +22,6 @@ import ( "code.gitea.io/gitea/modules/util" ) -const ( - tplBlame base.TplName = "repo/home" -) - type blameRow struct { RowNumber int Avatar gotemplate.HTML @@ -140,7 +135,7 @@ func RefBlame(ctx *context.Context) { renderBlame(ctx, blameParts, commitNames, previousCommits) - ctx.HTML(http.StatusOK, tplBlame) + ctx.HTML(http.StatusOK, tplRepoHome) } func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[string]*user_model.UserCommit, map[string]string) { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index c49eb762d8a66..92abe0ce25917 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -551,7 +551,11 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { ctx.ServerError("GetCompareInfo", err) return nil } - ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase + if ci.DirectComparison { + ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID + } else { + ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase + } return ci } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index d251f2043c043..fb61ec00d127c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -964,7 +964,7 @@ func DeleteIssue(ctx *context.Context) { return } - if err := issue_service.DeleteIssue(ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { + if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { ctx.ServerError("DeleteIssueByID", err) return } @@ -1132,7 +1132,7 @@ func NewIssuePost(ctx *context.Context) { Ref: form.Ref, } - if err := issue_service.NewIssue(repo, issue, labelIDs, attachments, assigneeIDs); err != nil { + if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) return @@ -2013,7 +2013,7 @@ func UpdateIssueTitle(ctx *context.Context) { return } - if err := issue_service.ChangeTitle(issue, ctx.Doer, title); err != nil { + if err := issue_service.ChangeTitle(ctx, issue, ctx.Doer, title); err != nil { ctx.ServerError("ChangeTitle", err) return } @@ -2037,7 +2037,7 @@ func UpdateIssueRef(ctx *context.Context) { ref := ctx.FormTrim("ref") - if err := issue_service.ChangeIssueRef(issue, ctx.Doer, ref); err != nil { + if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil { ctx.ServerError("ChangeRef", err) return } @@ -2161,7 +2161,7 @@ func UpdateIssueAssignee(ctx *context.Context) { for _, issue := range issues { switch action { case "clear": - if err := issue_service.DeleteNotPassedAssignee(issue, ctx.Doer, []*user_model.User{}); err != nil { + if err := issue_service.DeleteNotPassedAssignee(ctx, issue, ctx.Doer, []*user_model.User{}); err != nil { ctx.ServerError("ClearAssignees", err) return } @@ -2182,7 +2182,7 @@ func UpdateIssueAssignee(ctx *context.Context) { return } - _, _, err = issue_service.ToggleAssignee(issue, ctx.Doer, assigneeID) + _, _, err = issue_service.ToggleAssignee(ctx, issue, ctx.Doer, assigneeID) if err != nil { ctx.ServerError("ToggleAssignee", err) return @@ -2269,7 +2269,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { return } - _, err = issue_service.TeamReviewRequest(issue, ctx.Doer, team, action == "attach") + _, err = issue_service.TeamReviewRequest(ctx, issue, ctx.Doer, team, action == "attach") if err != nil { ctx.ServerError("TeamReviewRequest", err) return @@ -2307,7 +2307,7 @@ func UpdatePullReviewRequest(ctx *context.Context) { return } - _, err = issue_service.ReviewRequest(issue, ctx.Doer, reviewer, action == "attach") + _, err = issue_service.ReviewRequest(ctx, issue, ctx.Doer, reviewer, action == "attach") if err != nil { ctx.ServerError("ReviewRequest", err) return diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 9cd5fc9e40207..150050f76b90b 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -1014,7 +1014,7 @@ func Stars(ctx *context.Context) { // Forks render repository's forked users func Forks(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repos.forks") + ctx.Data["Title"] = ctx.Tr("repo.forks") page := ctx.FormInt("page") if page <= 0 { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index fe2becb7bb473..0c5c5eed7de88 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -298,7 +298,15 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Data["footerPresent"] = false } - ctx.Data["toc"] = rctx.TableOfContents + if rctx.SidebarTocNode != nil { + sb := &strings.Builder{} + err = markdown.SpecializedMarkdown().Renderer().Render(sb, nil, rctx.SidebarTocNode) + if err != nil { + log.Error("Failed to render wiki sidebar TOC: %v", err) + } else { + ctx.Data["sidebarTocContent"] = sb.String() + } + } // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount(wiki_service.DefaultBranch, pageFilename) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index a0a5dc3c4b9ba..1f77379044afe 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -107,6 +107,7 @@ func Dashboard(ctx *context.Context) { return } ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } feeds, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{ diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index d690fa4d0113d..b39ba58f12455 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -74,6 +74,7 @@ func Profile(ctx *context.Context) { return } ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) } if len(ctx.ContextUser.Description) != 0 { diff --git a/routers/web/user/search.go b/routers/web/user/search.go index c5c3aa75f084e..bdc4116e3776b 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -24,6 +24,7 @@ func Search(ctx *context.Context) { Keyword: ctx.FormTrim("q"), UID: ctx.FormInt64("uid"), Type: user_model.UserTypeIndividual, + IsActive: ctx.FormOptionalBool("active"), ListOptions: listOptions, }) if err != nil { diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 6956c25cee2fd..4ac77276ffe2e 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -526,3 +526,38 @@ func (n *actionsNotifier) NotifyPullRequestChangeTargetBranch(ctx context.Contex WithPullRequest(pr). Notify(ctx) } + +func (n *actionsNotifier) NotifyNewWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) { + ctx = withMethod(ctx, "NotifyNewWikiPage") + + newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ + Action: api.HookWikiCreated, + Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Sender: convert.ToUser(ctx, doer, nil), + Page: page, + Comment: comment, + }).Notify(ctx) +} + +func (n *actionsNotifier) NotifyEditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page, comment string) { + ctx = withMethod(ctx, "NotifyEditWikiPage") + + newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ + Action: api.HookWikiEdited, + Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Sender: convert.ToUser(ctx, doer, nil), + Page: page, + Comment: comment, + }).Notify(ctx) +} + +func (n *actionsNotifier) NotifyDeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, page string) { + ctx = withMethod(ctx, "NotifyDeleteWikiPage") + + newNotifyInput(repo, doer, webhook_module.HookEventWiki).WithPayload(&api.WikiPayload{ + Action: api.HookWikiDeleted, + Repository: convert.ToRepo(ctx, repo, perm_model.AccessModeOwner), + Sender: convert.ToUser(ctx, doer, nil), + Page: page, + }).Notify(ctx) +} diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go index ec89984499862..76445e0d6d53d 100644 --- a/services/auth/source/db/authenticate.go +++ b/services/auth/source/db/authenticate.go @@ -40,5 +40,13 @@ func Authenticate(user *user_model.User, login, password string) (*user_model.Us } } + // attempting to login as a non-user account + if user.Type != user_model.UserTypeIndividual { + return nil, user_model.ErrUserProhibitLogin{ + UID: user.ID, + Name: user.Name, + } + } + return user, nil } diff --git a/services/issue/assignee.go b/services/issue/assignee.go index e5e1456c3f12e..4d0224d4bff10 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -6,7 +6,6 @@ package issue import ( "context" - "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -18,7 +17,7 @@ import ( ) // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array -func DeleteNotPassedAssignee(issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) { +func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) { var found bool oriAssignes := make([]*user_model.User, len(issue.Assignees)) _ = copy(oriAssignes, issue.Assignees) @@ -34,7 +33,7 @@ func DeleteNotPassedAssignee(issue *issues_model.Issue, doer *user_model.User, a if !found { // This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here - if _, _, err := ToggleAssignee(issue, doer, assignee.ID); err != nil { + if _, _, err := ToggleAssignee(ctx, issue, doer, assignee.ID); err != nil { return err } } @@ -44,25 +43,25 @@ func DeleteNotPassedAssignee(issue *issues_model.Issue, doer *user_model.User, a } // ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. -func ToggleAssignee(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) { - removed, comment, err = issues_model.ToggleIssueAssignee(issue, doer, assigneeID) +func ToggleAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) { + removed, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) if err != nil { return } - assignee, err1 := user_model.GetUserByID(db.DefaultContext, assigneeID) + assignee, err1 := user_model.GetUserByID(ctx, assigneeID) if err1 != nil { err = err1 return } - notification.NotifyIssueChangeAssignee(db.DefaultContext, doer, issue, assignee, removed, comment) + notification.NotifyIssueChangeAssignee(ctx, doer, issue, assignee, removed, comment) return removed, comment, err } // ReviewRequest add or remove a review request from a user for this PR, and make comment for it. -func ReviewRequest(issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { +func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { if isAdd { comment, err = issues_model.AddReviewRequest(issue, reviewer, doer) } else { @@ -74,7 +73,7 @@ func ReviewRequest(issue *issues_model.Issue, doer, reviewer *user_model.User, i } if comment != nil { - notification.NotifyPullReviewRequest(db.DefaultContext, doer, issue, reviewer, isAdd, comment) + notification.NotifyPullReviewRequest(ctx, doer, issue, reviewer, isAdd, comment) } return comment, err @@ -229,7 +228,7 @@ func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, } // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. -func TeamReviewRequest(issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { +func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { if isAdd { comment, err = issues_model.AddTeamReviewRequest(issue, reviewer, doer) } else { @@ -245,11 +244,11 @@ func TeamReviewRequest(issue *issues_model.Issue, doer *user_model.User, reviewe } // notify all user in this team - if err = comment.LoadIssue(db.DefaultContext); err != nil { + if err = comment.LoadIssue(ctx); err != nil { return } - members, err := organization.GetTeamMembers(db.DefaultContext, &organization.SearchMembersOptions{ + members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ TeamID: reviewer.ID, }) if err != nil { @@ -261,7 +260,7 @@ func TeamReviewRequest(issue *issues_model.Issue, doer *user_model.User, reviewe continue } comment.AssigneeID = member.ID - notification.NotifyPullReviewRequest(db.DefaultContext, doer, issue, member, isAdd, comment) + notification.NotifyPullReviewRequest(ctx, doer, issue, member, isAdd, comment) } return comment, err diff --git a/services/issue/assignee_test.go b/services/issue/assignee_test.go index 114ace078edd1..43b24e1d1fc4f 100644 --- a/services/issue/assignee_test.go +++ b/services/issue/assignee_test.go @@ -31,7 +31,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) { assert.True(t, isAssigned) // Clean everyone - err = DeleteNotPassedAssignee(issue, user1, []*user_model.User{}) + err = DeleteNotPassedAssignee(db.DefaultContext, issue, user1, []*user_model.User{}) assert.NoError(t, err) assert.EqualValues(t, 0, len(issue.Assignees)) diff --git a/services/issue/comments.go b/services/issue/comments.go index 1323fb47aa440..4fe07c17b9d37 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -16,8 +16,8 @@ import ( ) // CreateComment creates comment of issue or commit. -func CreateComment(opts *issues_model.CreateCommentOptions) (comment *issues_model.Comment, err error) { - ctx, committer, err := db.TxContext(db.DefaultContext) +func CreateComment(ctx context.Context, opts *issues_model.CreateCommentOptions) (comment *issues_model.Comment, err error) { + ctx, committer, err := db.TxContext(ctx) if err != nil { return nil, err } @@ -53,7 +53,7 @@ func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue return nil } - _, err = CreateComment(&issues_model.CreateCommentOptions{ + _, err = CreateComment(db.DefaultContext, &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeCommitRef, Doer: doer, Repo: repo, @@ -66,7 +66,7 @@ func CreateRefComment(doer *user_model.User, repo *repo_model.Repository, issue // CreateIssueComment creates a plain issue comment. func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content string, attachments []string) (*issues_model.Comment, error) { - comment, err := CreateComment(&issues_model.CreateCommentOptions{ + comment, err := CreateComment(ctx, &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeComment, Doer: doer, Repo: repo, diff --git a/services/issue/issue.go b/services/issue/issue.go index b91ee4fc18b07..d4f827e99af56 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -4,6 +4,7 @@ package issue import ( + "context" "fmt" activities_model "code.gitea.io/gitea/models/activities" @@ -20,49 +21,49 @@ import ( ) // NewIssue creates new issue with labels for repository. -func NewIssue(repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { +func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { if err := issues_model.NewIssue(repo, issue, labelIDs, uuids); err != nil { return err } for _, assigneeID := range assigneeIDs { - if err := AddAssigneeIfNotAssigned(issue, issue.Poster, assigneeID); err != nil { + if err := AddAssigneeIfNotAssigned(ctx, issue, issue.Poster, assigneeID); err != nil { return err } } - mentions, err := issues_model.FindAndUpdateIssueMentions(db.DefaultContext, issue, issue.Poster, issue.Content) + mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) if err != nil { return err } - notification.NotifyNewIssue(db.DefaultContext, issue, mentions) + notification.NotifyNewIssue(ctx, issue, mentions) if len(issue.Labels) > 0 { - notification.NotifyIssueChangeLabels(db.DefaultContext, issue.Poster, issue, issue.Labels, nil) + notification.NotifyIssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil) } if issue.Milestone != nil { - notification.NotifyIssueChangeMilestone(db.DefaultContext, issue.Poster, issue, 0) + notification.NotifyIssueChangeMilestone(ctx, issue.Poster, issue, 0) } return nil } // ChangeTitle changes the title of this issue, as the given user. -func ChangeTitle(issue *issues_model.Issue, doer *user_model.User, title string) (err error) { +func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, title string) (err error) { oldTitle := issue.Title issue.Title = title - if err = issues_model.ChangeIssueTitle(issue, doer, oldTitle); err != nil { + if err = issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { return } - notification.NotifyIssueChangeTitle(db.DefaultContext, doer, issue, oldTitle) + notification.NotifyIssueChangeTitle(ctx, doer, issue, oldTitle) return nil } // ChangeIssueRef changes the branch of this issue, as the given user. -func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string) error { +func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error { oldRef := issue.Ref issue.Ref = ref @@ -70,7 +71,7 @@ func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string return err } - notification.NotifyIssueChangeRef(db.DefaultContext, doer, issue, oldRef) + notification.NotifyIssueChangeRef(ctx, doer, issue, oldRef) return nil } @@ -81,7 +82,7 @@ func ChangeIssueRef(issue *issues_model.Issue, doer *user_model.User, ref string // "assignees" (array): Logins for Users to assign to this issue. // Pass one or more user logins to replace the set of assignees on this Issue. // Send an empty array ([]) to clear all assignees from the Issue. -func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { +func UpdateAssignees(ctx context.Context, issue *issues_model.Issue, oneAssignee string, multipleAssignees []string, doer *user_model.User) (err error) { var allNewAssignees []*user_model.User // Keep the old assignee thingy for compatibility reasons @@ -102,7 +103,7 @@ func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssi // Loop through all assignees to add them for _, assigneeName := range multipleAssignees { - assignee, err := user_model.GetUserByName(db.DefaultContext, assigneeName) + assignee, err := user_model.GetUserByName(ctx, assigneeName) if err != nil { return err } @@ -111,7 +112,7 @@ func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssi } // Delete all old assignees not passed - if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil { + if err = DeleteNotPassedAssignee(ctx, issue, doer, allNewAssignees); err != nil { return err } @@ -121,7 +122,7 @@ func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssi // has access to the repo. for _, assignee := range allNewAssignees { // Extra method to prevent double adding (which would result in removing) - err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID) + err = AddAssigneeIfNotAssigned(ctx, issue, doer, assignee.ID) if err != nil { return err } @@ -131,42 +132,42 @@ func UpdateAssignees(issue *issues_model.Issue, oneAssignee string, multipleAssi } // DeleteIssue deletes an issue -func DeleteIssue(doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error { +func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue) error { // load issue before deleting it - if err := issue.LoadAttributes(gitRepo.Ctx); err != nil { + if err := issue.LoadAttributes(ctx); err != nil { return err } - if err := issue.LoadPullRequest(gitRepo.Ctx); err != nil { + if err := issue.LoadPullRequest(ctx); err != nil { return err } // delete entries in database - if err := deleteIssue(issue); err != nil { + if err := deleteIssue(ctx, issue); err != nil { return err } // delete pull request related git data - if issue.IsPull { + if issue.IsPull && gitRepo != nil { if err := gitRepo.RemoveReference(fmt.Sprintf("%s%d/head", git.PullPrefix, issue.PullRequest.Index)); err != nil { return err } } - notification.NotifyDeleteIssue(gitRepo.Ctx, doer, issue) + notification.NotifyDeleteIssue(ctx, doer, issue) return nil } // AddAssigneeIfNotAssigned adds an assignee only if he isn't already assigned to the issue. // Also checks for access of assigned user -func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (err error) { - assignee, err := user_model.GetUserByID(db.DefaultContext, assigneeID) +func AddAssigneeIfNotAssigned(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (err error) { + assignee, err := user_model.GetUserByID(ctx, assigneeID) if err != nil { return err } // Check if the user is already assigned - isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, assignee) + isAssigned, err := issues_model.IsUserAssignedToIssue(ctx, issue, assignee) if err != nil { return err } @@ -175,7 +176,7 @@ func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, return nil } - valid, err := access_model.CanBeAssigned(db.DefaultContext, assignee, issue.Repo, issue.IsPull) + valid, err := access_model.CanBeAssigned(ctx, assignee, issue.Repo, issue.IsPull) if err != nil { return err } @@ -183,7 +184,7 @@ func AddAssigneeIfNotAssigned(issue *issues_model.Issue, doer *user_model.User, return repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: issue.Repo.Name} } - _, _, err = ToggleAssignee(issue, doer, assigneeID) + _, _, err = ToggleAssignee(ctx, issue, doer, assigneeID) if err != nil { return err } @@ -206,8 +207,8 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i } // deleteIssue deletes the issue -func deleteIssue(issue *issues_model.Issue) error { - ctx, committer, err := db.TxContext(db.DefaultContext) +func deleteIssue(ctx context.Context, issue *issues_model.Issue) error { + ctx, committer, err := db.TxContext(ctx) if err != nil { return err } diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go index b67d2e2e79a25..da0e97c23c258 100644 --- a/services/issue/issue_test.go +++ b/services/issue/issue_test.go @@ -44,7 +44,7 @@ func TestIssue_DeleteIssue(t *testing.T) { ID: issueIDs[2], } - err = deleteIssue(issue) + err = deleteIssue(db.DefaultContext, issue) assert.NoError(t, err) issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1) assert.NoError(t, err) @@ -55,7 +55,7 @@ func TestIssue_DeleteIssue(t *testing.T) { assert.NoError(t, err) issue, err = issues_model.GetIssueByID(db.DefaultContext, 4) assert.NoError(t, err) - err = deleteIssue(issue) + err = deleteIssue(db.DefaultContext, issue) assert.NoError(t, err) assert.EqualValues(t, 2, len(attachments)) for i := range attachments { @@ -78,7 +78,7 @@ func TestIssue_DeleteIssue(t *testing.T) { assert.NoError(t, err) assert.False(t, left) - err = deleteIssue(issue2) + err = deleteIssue(db.DefaultContext, issue2) assert.NoError(t, err) left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) diff --git a/services/pull/comment.go b/services/pull/comment.go index 933ad09a85e9a..24dfd8af0ce00 100644 --- a/services/pull/comment.go +++ b/services/pull/comment.go @@ -90,7 +90,7 @@ func CreatePushPullComment(ctx context.Context, pusher *user_model.User, pr *iss ops.Content = string(dataJSON) - comment, err = issue_service.CreateComment(ops) + comment, err = issue_service.CreateComment(ctx, ops) return comment, err } diff --git a/services/pull/pull.go b/services/pull/pull.go index fe2f002010b1d..55dfd3c180297 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -52,7 +52,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu } for _, assigneeID := range assigneeIDs { - if err := issue_service.AddAssigneeIfNotAssigned(pull, pull.Poster, assigneeID); err != nil { + if err := issue_service.AddAssigneeIfNotAssigned(ctx, pull, pull.Poster, assigneeID); err != nil { return err } } @@ -122,7 +122,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu Content: string(dataJSON), } - _, _ = issue_service.CreateComment(ops) + _, _ = issue_service.CreateComment(ctx, ops) } return nil @@ -221,7 +221,7 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer OldRef: oldBranch, NewRef: targetBranch, } - if _, err = issue_service.CreateComment(options); err != nil { + if _, err = issue_service.CreateComment(ctx, options); err != nil { return fmt.Errorf("CreateChangeTargetBranchComment: %w", err) } diff --git a/services/pull/review.go b/services/pull/review.go index ba93b5e2f540c..6feffe4ec4e7a 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -248,7 +248,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo return nil, err } } - return issue_service.CreateComment(&issues_model.CreateCommentOptions{ + return issue_service.CreateComment(ctx, &issues_model.CreateCommentOptions{ Type: issues_model.CommentTypeCode, Doer: doer, Repo: repo, @@ -368,7 +368,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string, return } - comment, err = issue_service.CreateComment(&issues_model.CreateCommentOptions{ + comment, err = issue_service.CreateComment(ctx, &issues_model.CreateCommentOptions{ Doer: doer, Content: message, Type: issues_model.CommentTypeDismissReview, diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl index c5ab863d00d04..2fe478a07d8a0 100644 --- a/templates/devtest/gitea-ui.tmpl +++ b/templates/devtest/gitea-ui.tmpl @@ -1,12 +1,51 @@ {{template "base/head" .}} -
+
+
- - +

Tooltip

+
text with tooltip
+
text with interactive tooltip
+
- text with tooltip +

GiteaOriginUrl

+
+
- {{template "shared/combomarkdowneditor" .}} + +
+

LocaleNumber

+
{{.locale.PrettyNumber 1}}
+
{{.locale.PrettyNumber 12}}
+
{{.locale.PrettyNumber 123}}
+
{{.locale.PrettyNumber 1234}}
+
{{.locale.PrettyNumber 12345}}
+
{{.locale.PrettyNumber 123456}}
+
{{.locale.PrettyNumber 1234567}}
+
+ +
+

TimeSince

+
Now: {{TimeSince .TimeNow $.locale}}
+
5s past: {{TimeSince .TimePast5s $.locale}}
+
5s future: {{TimeSince .TimeFuture5s $.locale}}
+
2m past: {{TimeSince .TimePast2m $.locale}}
+
2m future: {{TimeSince .TimeFuture2m $.locale}}
+
1y past: {{TimeSince .TimePast1y $.locale}}
+
1y future: {{TimeSince .TimeFuture1y $.locale}}
+
+ +
+

ComboMarkdownEditor

+
ps: no JS code attached, so just a layout
+ {{template "shared/combomarkdowneditor" .}} +
+ +
{{template "base/footer" .}} diff --git a/templates/org/create.tmpl b/templates/org/create.tmpl index f734a39a93724..7e988ba0c792a 100644 --- a/templates/org/create.tmpl +++ b/templates/org/create.tmpl @@ -11,7 +11,7 @@ {{template "base/alert" .}}
- + {{.locale.Tr "org.org_name_helper"}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 833b97e347253..1caa4210e62ca 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -14,26 +14,27 @@ {{.CsrfTokenHtml}}
- +
- +
- +
- +
- +
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 9677b8eb09d9f..7c622a91b3eb7 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -84,9 +84,9 @@ {{.locale.Tr "packages.versions.view_all"}}
{{range .LatestVersions}} -
- {{.Version}} - {{$.locale.Tr "on_date"}} {{.CreatedUnix.FormatDate}} +
+ {{.Version}} + {{template "shared/datetime/short" (dict "Datetime" (.CreatedUnix.FormatDate) "Fallback" (.CreatedUnix.FormatDate))}}
{{end}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 213bab70b66e2..ae9e3a0d11870 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -13,11 +13,11 @@ @@ -46,9 +46,9 @@ {{end}} {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{$.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} {{svg "octicon-check" 16 "gt-mr-3"}} - {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + {{$.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 602461400b928..e54a72714a5c1 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -18,11 +18,11 @@
@@ -84,9 +84,9 @@ {{end}} {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{$.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} {{svg "octicon-check" 16 "gt-mr-3"}} - {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + {{$.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} {{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}} {{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix $.locale) | Safe}}{{end}} diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl index 6eb26b36c5d6d..045f513974671 100644 --- a/templates/repo/issue/openclose.tmpl +++ b/templates/repo/issue/openclose.tmpl @@ -5,10 +5,10 @@ {{else}} {{svg "octicon-issue-opened" 16 "gt-mr-3"}} {{end}} - {{LocaleNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}} + {{.locale.PrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}} {{svg "octicon-check" 16 "gt-mr-3"}} - {{LocaleNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} + {{.locale.PrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl index fb5bc4f48d91b..bbcc20dd7cbd2 100644 --- a/templates/repo/projects/list.tmpl +++ b/templates/repo/projects/list.tmpl @@ -15,11 +15,11 @@ @@ -48,9 +48,9 @@ {{end}} {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} {{svg "octicon-check" 16 "gt-mr-3"}} - {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + {{.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 12aaa0bd71723..8e1793a5bab7c 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -161,9 +161,9 @@
  • {{.Size | FileSize}} - + {{svg "octicon-info"}} - + {{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}} diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 7a4e28cffa1be..ddedfd608690c 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -71,9 +71,9 @@ {{.Size | FileSize}} - + {{svg "octicon-info"}} - + {{end}} diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl index 97fbabda412d4..9289295b1d733 100644 --- a/templates/repo/sub_menu.tmpl +++ b/templates/repo/sub_menu.tmpl @@ -4,7 +4,7 @@
    {{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
    {{svg "octicon-git-branch"}} {{.BranchesCount}} {{.locale.TrN .BranchesCount "repo.branch" "repo.branches"}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 3db8be90cb911..6fa05870bb3c2 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -66,28 +66,16 @@

    {{.FormatWarning}}

    {{end}} -
    -
    +
    +
    {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} {{.content | Safe}}
    - {{if or .sidebarPresent .toc}} -
    - {{if .toc}} + {{if or .sidebarPresent .sidebarTocContent}} +
    + {{if .sidebarTocContent}}
    -
    - -
    {{.locale.Tr "toc"}}
    -
    - {{$level := 0}} - {{range .toc}} - {{if lt $level .Level}}{{range Iterate (Eval .Level "-" $level)}}
      {{end}}{{end}} - {{if gt $level .Level}}{{range Iterate (Eval $level "-" .Level)}}
    {{end}}{{end}} - {{$level = .Level}} -
  • {{.Text}}
  • - {{end}} - {{range Iterate $level}}{{end}} -
    + {{.sidebarTocContent | Safe}}
    {{end}} {{if .sidebarPresent}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 6b9bfe4ebad45..5a207aac60e3a 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -65,11 +65,11 @@
    diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 99151597217ae..39eea2fc7517d 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -39,11 +39,11 @@
    @@ -104,9 +104,9 @@ {{end}} {{svg "octicon-issue-opened" 16 "gt-mr-3"}} - {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} + {{.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} {{svg "octicon-check" 16 "gt-mr-3"}} - {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} + {{.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} {{if .TotalTrackedTime}} {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}} {{end}} diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl index 9d58bc8fc5e51..5d42a5435bb7b 100644 --- a/templates/user/heatmap.tmpl +++ b/templates/user/heatmap.tmpl @@ -1,5 +1,11 @@ {{if .HeatmapData}} -
    +
    {{.locale.Tr "user.heatmap.loading"}}
    diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index 4427c610bfa4d..4b25f97dddb29 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -67,7 +67,7 @@ func TestAPIMergePullWIP(t *testing.T) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{Status: issues_model.PullRequestStatusMergeable}, unittest.Cond("has_merged = ?", false)) pr.LoadIssue(db.DefaultContext) - issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title) + issue_service.ChangeTitle(db.DefaultContext, pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title) // force reload pr.LoadAttributes(db.DefaultContext) diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index b2098fdd03294..a3a5422154199 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -214,7 +214,7 @@ func TestAPICreateFile(t *testing.T) { req = NewRequestWithJSON(t, "POST", url, &createFileOptions) resp = MakeRequest(t, req, http.StatusCreated) DecodeJSON(t, resp, &fileResponse) - expectedMessage := "Add '" + treePath + "'\n" + expectedMessage := "Add " + treePath + "\n" assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message) // Test trying to create a file that already exists, should fail diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 9b80dc150a664..ae28c97002db7 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -99,7 +99,7 @@ func TestAPIDeleteFile(t *testing.T) { req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fileResponse) - expectedMessage := "Delete '" + treePath + "'\n" + expectedMessage := "Delete " + treePath + "\n" assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message) // Test deleting a file with the wrong SHA diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 8e07511aafda3..177d7282cabe3 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -199,7 +199,7 @@ func TestAPIUpdateFile(t *testing.T) { req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fileResponse) - expectedMessage := "Update '" + treePath + "'\n" + expectedMessage := "Update " + treePath + "\n" assert.EqualValues(t, expectedMessage, fileResponse.Commit.Message) // Test updating a file with the wrong SHA diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index 495290ed569cb..de2a9d7d23be9 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -55,7 +55,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { // Check if master branch has been locked successfully flashCookie := session.GetCookie(gitea_context.CookieNameFlash) assert.NotNil(t, flashCookie) - assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2527master%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522master%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) // Request editor page req = NewRequest(t, "GET", "/user2/repo1/_new/master/") @@ -76,7 +76,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) // Check body for error message - assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch 'master'.") + assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".") // remove the protected branch csrf = GetCSRF(t, session, "/user2/repo1/settings/branches") @@ -95,7 +95,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { // Check if master branch has been locked successfully flashCookie = session.GetCookie(gitea_context.CookieNameFlash) assert.NotNil(t, flashCookie) - assert.EqualValues(t, "error%3DRemoving%2Bbranch%2Bprotection%2Brule%2B%25271%2527%2Bfailed.", flashCookie.Value) + assert.EqualValues(t, "error%3DRemoving%2Bbranch%2Bprotection%2Brule%2B%25221%2522%2Bfailed.", flashCookie.Value) }) } diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index 95c6d83e54a02..95323b91c62c6 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -439,7 +439,7 @@ func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFil // Check if master branch has been locked successfully flashCookie := ctx.Session.GetCookie(gitea_context.CookieNameFlash) assert.NotNil(t, flashCookie) - assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522"+url.QueryEscape(branch)+"%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) } } diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 35f7cb8ab7650..3a13fb4fcb915 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -67,7 +67,7 @@ func TestPullCreate(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body) assert.Regexp(t, "diff", resp.Body) - assert.Regexp(t, `Subject: \[PATCH\] Update 'README.md'`, resp.Body) + assert.Regexp(t, `Subject: \[PATCH\] Update README.md`, resp.Body) assert.NotRegexp(t, "diff.*diff", resp.Body) // not two diffs, just one }) } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 55cf295257a17..ee185f2238025 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -187,7 +187,7 @@ func TestPullCleanUpAfterMerge(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - assert.EqualValues(t, "Branch 'user1/repo1:feature/test' has been deleted.", resultMsg) + assert.EqualValues(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg) }) } diff --git a/web_src/css/base.css b/web_src/css/base.css index c48a36c854764..bdf601951b717 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -9,7 +9,7 @@ /* non-color variables */ --border-radius: 0.28571429rem; --opacity-disabled: 0.55; - --height-loading: 12rem; + --height-loading: 16rem; /* base colors */ --color-primary: #4183c4; --color-primary-contrast: #ffffff; diff --git a/web_src/css/editor-markdown.css b/web_src/css/editor-markdown.css index 7d6c36635dbff..eb5c5d13b8c5f 100644 --- a/web_src/css/editor-markdown.css +++ b/web_src/css/editor-markdown.css @@ -44,6 +44,31 @@ max-height: calc(100vh - 200px); } +/* use the same styles as markup/content.css */ +.combo-markdown-editor .CodeMirror-scroll .cm-header-1 { + font-size: 2em; +} + +.combo-markdown-editor .CodeMirror-scroll .cm-header-2 { + font-size: 1.5em; +} + +.combo-markdown-editor .CodeMirror-scroll .cm-header-3 { + font-size: 1.25em; +} + +.combo-markdown-editor .CodeMirror-scroll .cm-header-4 { + font-size: 1em; +} + +.combo-markdown-editor .CodeMirror-scroll .cm-header-5 { + font-size: 0.875em; +} + +.combo-markdown-editor .CodeMirror-scroll .cm-header-6 { + font-size: 0.85em; +} + text-expander { display: block; position: relative; diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css index 8d64bd751b879..beb93e1e86d83 100644 --- a/web_src/css/helpers.css +++ b/web_src/css/helpers.css @@ -25,7 +25,8 @@ .gt-overflow-x-scroll { overflow-x: scroll !important; } .gt-cursor-default { cursor: default !important; } .gt-items-start { align-items: flex-start !important; } -.gt-whitespace-pre { white-space: pre !important } +.gt-whitespace-pre { white-space: pre !important; } +.gt-invisible { visibility: hidden !important; } .gt-mono { font-family: var(--fonts-monospace) !important; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 90f8c7091e9a6..d0f11e8e76500 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -542,7 +542,7 @@ .markup-block-error { display: block !important; /* override fomantic .ui.form .error.message {display: none} */ - border: 1px solid var(--color-error-border) !important; + border: none !important; margin-bottom: 0 !important; border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; diff --git a/web_src/css/repository.css b/web_src/css/repository.css index 5c903856287e7..05e50548d9bae 100644 --- a/web_src/css/repository.css +++ b/web_src/css/repository.css @@ -3261,14 +3261,15 @@ td.blob-excerpt { display: none; } -.wiki-content-toc > ul > li { - margin-bottom: 4px; -} - .wiki-content-toc ul { margin: 0; list-style: none; - padding-left: 1em; + padding: 5px 0 5px 1em; +} + +.wiki-content-toc ul ul { + border-left: 1px var(--color-secondary); + border-left-style: dashed; } /* fomantic's last-child selector does not work with hidden last child */ diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index 98ffce44b5579..7834ebe82ccac 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -1,7 +1,7 @@