diff --git a/models/repo/mirror.go b/models/repo/mirror.go index bd83d244245d9..8f96e8cee1851 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -19,12 +19,6 @@ import ( // ErrMirrorNotExist mirror does not exist error var ErrMirrorNotExist = errors.New("Mirror does not exist") -// RemoteMirrorer defines base methods for pull/push mirrors. -type RemoteMirrorer interface { - GetRepository() *Repository - GetRemoteName() string -} - // Mirror represents mirror information of a repository. type Mirror struct { ID int64 `xorm:"pk autoincr"` diff --git a/modules/base/tool.go b/modules/base/tool.go index 47ce125853fdf..a981fd6c57dc9 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/dustin/go-humanize" ) @@ -143,9 +144,9 @@ func FileSize(s int64) string { } // PrettyNumber produces a string form of the given number in base 10 with -// commas after every three orders of magnitud -func PrettyNumber(v int64) string { - return humanize.Comma(v) +// commas after every three orders of magnitude +func PrettyNumber(i interface{}) string { + return humanize.Comma(util.NumberIntoInt64(i)) } // Subtract deals with subtraction of all types of number. diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 5280827e8ae35..6685168bacd88 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -117,6 +117,7 @@ func TestFileSize(t *testing.T) { func TestPrettyNumber(t *testing.T) { assert.Equal(t, "23,342,432", PrettyNumber(23342432)) + assert.Equal(t, "23,342,432", PrettyNumber(int32(23342432))) assert.Equal(t, "0", PrettyNumber(0)) assert.Equal(t, "-100,000", PrettyNumber(-100000)) } diff --git a/modules/git/remote.go b/modules/git/remote.go index b2a2e6d7ab41a..cbb4ac6126b86 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -6,11 +6,12 @@ package git import ( "context" - "net/url" + + giturl "code.gitea.io/gitea/modules/git/url" ) -// GetRemoteAddress returns the url of a specific remote of the repository. -func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.URL, error) { +// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name +func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { var cmd *Command if CheckGitVersionAtLeast("2.7") == nil { cmd = NewCommand(ctx, "remote", "get-url", remoteName) @@ -20,11 +21,20 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.UR result, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) if err != nil { - return nil, err + return "", err } if len(result) > 0 { result = result[:len(result)-1] } - return url.Parse(result) + return result, nil +} + +// GetRemoteURL returns the url of a specific remote of the repository. +func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.GitURL, error) { + addr, err := GetRemoteAddress(ctx, repoPath, remoteName) + if err != nil { + return nil, err + } + return giturl.Parse(addr) } diff --git a/modules/git/url/url.go b/modules/git/url/url.go new file mode 100644 index 0000000000000..b41cfab7efb29 --- /dev/null +++ b/modules/git/url/url.go @@ -0,0 +1,90 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package url + +import ( + "fmt" + stdurl "net/url" + "strings" +) + +// ErrWrongURLFormat represents an error with wrong url format +type ErrWrongURLFormat struct { + URL string +} + +func (err ErrWrongURLFormat) Error() string { + return fmt.Sprintf("git URL %s format is wrong", err.URL) +} + +// GitURL represents a git URL +type GitURL struct { + *stdurl.URL + extraMark int // 0 no extra 1 scp 2 file path with no prefix +} + +// String returns the URL's string +func (u *GitURL) String() string { + switch u.extraMark { + case 0: + return u.URL.String() + case 1: + return fmt.Sprintf("%s@%s:%s", u.User.Username(), u.Host, u.Path) + case 2: + return u.Path + default: + return "" + } +} + +// Parse parse all kinds of git URL +func Parse(remote string) (*GitURL, error) { + if strings.Contains(remote, "://") { + u, err := stdurl.Parse(remote) + if err != nil { + return nil, err + } + return &GitURL{URL: u}, nil + } else if strings.Contains(remote, "@") && strings.Contains(remote, ":") { + url := stdurl.URL{ + Scheme: "ssh", + } + squareBrackets := false + lastIndex := -1 + FOR: + for i := 0; i < len(remote); i++ { + switch remote[i] { + case '@': + url.User = stdurl.User(remote[:i]) + lastIndex = i + 1 + case ':': + if !squareBrackets { + url.Host = strings.ReplaceAll(remote[lastIndex:i], "%25", "%") + if len(remote) <= i+1 { + return nil, ErrWrongURLFormat{URL: remote} + } + url.Path = remote[i+1:] + break FOR + } + case '[': + squareBrackets = true + case ']': + squareBrackets = false + } + } + return &GitURL{ + URL: &url, + extraMark: 1, + }, nil + } + + return &GitURL{ + URL: &stdurl.URL{ + Scheme: "file", + Path: remote, + }, + extraMark: 2, + }, nil +} diff --git a/modules/git/url/url_test.go b/modules/git/url/url_test.go new file mode 100644 index 0000000000000..611bef8672740 --- /dev/null +++ b/modules/git/url/url_test.go @@ -0,0 +1,167 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package url + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGitURLs(t *testing.T) { + kases := []struct { + kase string + expected *GitURL + }{ + { + kase: "git@127.0.0.1:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "127.0.0.1", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@[fe80:14fc:cec5:c174:d88%2510]:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[fe80:14fc:cec5:c174:d88%10]", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@[::1]:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[::1]", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "git@github.com:go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "github.com", + Path: "go-gitea/gitea.git", + }, + extraMark: 1, + }, + }, + { + kase: "ssh://git@github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "ssh://git@[::1]/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "ssh", + User: url.User("git"), + Host: "[::1]", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "/repositories/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "file", + Path: "/repositories/go-gitea/gitea.git", + }, + extraMark: 2, + }, + }, + { + kase: "file:///repositories/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "file", + Path: "/repositories/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://git:git@github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "github.com", + User: url.UserPassword("git", "git"), + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + { + kase: "https://[fe80:14fc:cec5:c174:d88%2510]:20/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "https", + Host: "[fe80:14fc:cec5:c174:d88%10]:20", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + + { + kase: "git://github.com/go-gitea/gitea.git", + expected: &GitURL{ + URL: &url.URL{ + Scheme: "git", + Host: "github.com", + Path: "/go-gitea/gitea.git", + }, + extraMark: 0, + }, + }, + } + + for _, kase := range kases { + t.Run(kase.kase, func(t *testing.T) { + u, err := Parse(kase.kase) + assert.NoError(t, err) + assert.EqualValues(t, kase.expected.extraMark, u.extraMark) + assert.EqualValues(t, *kase.expected, *u) + }) + } +} diff --git a/modules/graceful/manager_unix.go b/modules/graceful/manager_unix.go index 9d3816e9c2a76..ba1dbd38f6a08 100644 --- a/modules/graceful/manager_unix.go +++ b/modules/graceful/manager_unix.go @@ -157,6 +157,7 @@ func (g *Manager) handleSignals(ctx context.Context) { case <-ctx.Done(): log.Warn("PID: %d. Background context for manager closed - %v - Shutting down...", pid, ctx.Err()) g.DoGracefulShutdown() + return } } } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index ef7b70c09f8ac..c0be5c1fa56ea 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -32,6 +32,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" + giturl "code.gitea.io/gitea/modules/git/url" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -97,18 +98,19 @@ func NewFuncMap() []template.FuncMap { "CustomEmojis": func() map[string]string { return setting.UI.CustomEmojisMap }, - "Safe": Safe, - "SafeJS": SafeJS, - "JSEscape": JSEscape, - "Str2html": Str2html, - "TimeSince": timeutil.TimeSince, - "TimeSinceUnix": timeutil.TimeSinceUnix, - "RawTimeSince": timeutil.RawTimeSince, - "FileSize": base.FileSize, - "PrettyNumber": base.PrettyNumber, - "Subtract": base.Subtract, - "EntryIcon": base.EntryIcon, - "MigrationIcon": MigrationIcon, + "Safe": Safe, + "SafeJS": SafeJS, + "JSEscape": JSEscape, + "Str2html": Str2html, + "TimeSince": timeutil.TimeSince, + "TimeSinceUnix": timeutil.TimeSinceUnix, + "RawTimeSince": timeutil.RawTimeSince, + "FileSize": base.FileSize, + "PrettyNumber": base.PrettyNumber, + "JsPrettyNumber": JsPrettyNumber, + "Subtract": base.Subtract, + "EntryIcon": base.EntryIcon, + "MigrationIcon": MigrationIcon, "Add": func(a ...int) int { sum := 0 for _, val := range a { @@ -971,21 +973,44 @@ type remoteAddress struct { Password string } -func mirrorRemoteAddress(ctx context.Context, m repo_model.RemoteMirrorer) remoteAddress { +func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress { a := remoteAddress{} + if !m.IsMirror { + return a + } + + remoteURL := m.OriginalURL + if remoteURL == "" { + var err error + remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) + if err != nil { + log.Error("GetRemoteURL %v", err) + return a + } + } - u, err := git.GetRemoteAddress(ctx, m.GetRepository().RepoPath(), m.GetRemoteName()) + u, err := giturl.Parse(remoteURL) if err != nil { - log.Error("GetRemoteAddress %v", err) + log.Error("giturl.Parse %v", err) return a } - if u.User != nil { - a.Username = u.User.Username() - a.Password, _ = u.User.Password() + if u.Scheme != "ssh" && u.Scheme != "file" { + if u.User != nil { + a.Username = u.User.Username() + a.Password, _ = u.User.Password() + } + u.User = nil } - u.User = nil a.Address = u.String() return a } + +// JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent +// JS will replace the number with locale-specific separators, based on the user's selected language +func JsPrettyNumber(i interface{}) template.HTML { + num := util.NumberIntoInt64(i) + + return template.HTML(`` + base.PrettyNumber(num) + ``) +} diff --git a/modules/util/util.go b/modules/util/util.go index 1017117874816..be60fe4b4bbdf 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -224,3 +224,21 @@ func Dedent(s string) string { } return strings.TrimSpace(s) } + +// NumberIntoInt64 transform a given int into int64. +func NumberIntoInt64(number interface{}) int64 { + var value int64 + switch v := number.(type) { + case int: + value = int64(v) + case int8: + value = int64(v) + case int16: + value = int64(v) + case int32: + value = int64(v) + case int64: + value = v + } + return value +} diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index c9dc4a8f59a01..88a3920f6e67c 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -128,7 +128,7 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl case binding.ERR_EMAIL: data["ErrorMsg"] = trName + l.Tr("form.email_error") case binding.ERR_URL: - data["ErrorMsg"] = trName + l.Tr("form.url_error") + data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message) case binding.ERR_INCLUDE: data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) case validation.ErrGlobPattern: diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c4ad71471700d..347022fbdb39b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -443,7 +443,7 @@ 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 = ` is not a valid URL.` +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.` @@ -1259,8 +1259,6 @@ issues.change_ref_at = `changed reference from %s to issues.remove_ref_at = `removed reference %s %s` issues.add_ref_at = `added reference %s %s` issues.delete_branch_at = `deleted branch %s %s` -issues.open_tab = %d Open -issues.close_tab = %d Closed issues.filter_label = Label issues.filter_label_exclude = `Use alt + click/enter to exclude labels` issues.filter_label_no_select = All labels @@ -1568,14 +1566,7 @@ pulls.squash_merge_pull_request = Create squash commit pulls.merge_manually = Manually merged pulls.merge_commit_id = The merge commit ID pulls.require_signed_wont_sign = The branch requires signed commits but this merge will not be signed -pulls.merge_pull_request_now = Merge Pull Request Now -pulls.rebase_merge_pull_request_now = Rebase and Merge Now -pulls.rebase_merge_commit_pull_request_now = Rebase and Merge Now (--no-ff) -pulls.squash_merge_pull_request_now = Squash and Merge Now -pulls.merge_pull_request_on_status_success = Merge Pull Request When All Checks Succeed -pulls.rebase_merge_pull_request_on_status_success = Rebase and Merge When All Checks Succeed -pulls.rebase_merge_commit_pull_request_on_status_success = Rebase and Merge (--no-ff) When All Checks Succeed -pulls.squash_merge_pull_request_on_status_success = Squash and Merge When All Checks Succeed + pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.merge_conflict = Merge Failed: There was a conflict whilst merging. Hint: Try a different strategy pulls.merge_conflict_summary = Error Message @@ -1606,18 +1597,20 @@ pulls.reopened_at = `reopened this pull request %[2] pulls.merge_instruction_hint = `You can also view command line instructions.` pulls.merge_instruction_step1_desc = From your project repository, check out a new branch and test the changes. pulls.merge_instruction_step2_desc = Merge the changes and update on Gitea. -pulls.merge_on_status_success = The pull request was scheduled to merge when all checks succeed. -pulls.merge_on_status_success_already_scheduled = This pull request is already scheduled to merge when all checks succeed. -pulls.pr_has_pending_merge_on_success = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. -pulls.merge_pull_on_success_cancel = Cancel auto merge -pulls.pull_request_not_scheduled = This pull request is not scheduled to auto merge. -pulls.pull_request_schedule_canceled = The auto merge was canceled for this pull request. -pulls.pull_request_scheduled_auto_merge = `scheduled this pull request to auto merge when all checks succeed %[1]s` -pulls.pull_request_canceled_scheduled_auto_merge = `canceled auto merging this pull request when all checks succeed %[1]s` + +pulls.auto_merge_button_when_succeed = (When checks succeed) +pulls.auto_merge_when_succeed = Auto merge when all checks succeed +pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed. +pulls.auto_merge_has_pending_schedule = %[1]s scheduled this pull request to auto merge when all checks succeed %[2]s. + +pulls.auto_merge_cancel_schedule = Cancel auto merge +pulls.auto_merge_not_scheduled = This pull request is not scheduled to auto merge. +pulls.auto_merge_canceled_schedule = The auto merge was canceled for this pull request. + +pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto merge when all checks succeed %[1]s` +pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s` milestones.new = New Milestone -milestones.open_tab = %d Open -milestones.close_tab = %d Closed milestones.closed = Closed %s milestones.update_ago = Updated %s ago milestones.no_due_date = No due date diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 24e3a564805b8..887f675810243 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -2,6 +2,7 @@ home=ホーム dashboard=ダッシュボード explore=エクスプローラー help=ヘルプ +logo=ロゴ sign_in=サインイン sign_in_with=こちらでサインイン sign_out=サインアウト @@ -716,6 +717,9 @@ generate_token_success=あなたの新しいトークンを生成しました。 generate_token_name_duplicate=アプリケーション名 %s は既に使われています。他の名前を使用してください。 delete_token=削除 access_token_deletion=アクセストークンの削除 +access_token_deletion_cancel_action=キャンセル +access_token_deletion_confirm_action=削除 +access_token_deletion_desc=トークンを削除すると、それを使用しているアプリケーションは、アカウントへのアクセスができなくなります。これは元に戻せません。続行しますか? delete_token_success=トークンを削除しました。 削除したトークンを使用しているアプリケーションは、今後あなたのアカウントにアクセスできません。 manage_oauth2_applications=OAuth2アプリケーションの管理 @@ -858,6 +862,7 @@ default_branch=デフォルトブランチ default_branch_helper=デフォルトブランチはプルリクエストとコードコミットのベースブランチとなります。 mirror_prune=Prune mirror_prune_desc=不要になった古いリモートトラッキング参照を削除 +mirror_interval=ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 自動的な同期を無効にする場合は0。(最小間隔: %s) mirror_interval_invalid=ミラー間隔が不正です。 mirror_address=クローンするURL mirror_address_desc=必要な資格情報は「認証」セクションに設定してください。 @@ -1806,6 +1811,9 @@ 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=コントリビューターだけタイムトラッキングする @@ -2281,6 +2289,8 @@ topic.done=完了 topic.count_prompt=選択できるのは25トピックまでです。 topic.format_prompt=トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。 +find_file.go_to_file=ファイルへ移動 +find_file.no_matching=一致するファイルが見つかりません error.csv.too_large=このファイルは大きすぎるため表示できません。 error.csv.unexpected=このファイルは %d 行目の %d 文字目に予期しない文字が含まれているため表示できません。 diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 7656f0c52b855..14a8d11dc3b1f 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -566,6 +566,7 @@ comment_type_group_branch=Ramo comment_type_group_time_tracking=Contagem de tempo comment_type_group_deadline=Prazo comment_type_group_dependency=Dependência +comment_type_group_lock=Estado do bloqueio comment_type_group_review_request=Pedido de revisão comment_type_group_pull_request_push=Cometimentos adicionados comment_type_group_project=Planeamento @@ -1810,6 +1811,9 @@ settings.tracker_url_format_error=O formato do URL do gestor de questões extern settings.tracker_issue_style=Formato dos números do gestor de questões externo settings.tracker_issue_style.numeric=Numérico settings.tracker_issue_style.alphanumeric=Alfanumérico +settings.tracker_issue_style.regexp=Expressão Regular +settings.tracker_issue_style.regexp_pattern=Padrão da expressão regular +settings.tracker_issue_style.regexp_pattern_desc=O primeiro grupo capturado será usado no lugar de {index}. settings.tracker_url_format_desc=Use os marcadores {user}, {repo} e {index} para o nome de utilizador, nome do repositório e índice das questões. settings.enable_timetracker=Habilitar a contagem de tempo settings.allow_only_contributors_to_track_time=Permitir a contagem de tempo somente aos contribuidores diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 2cc448282b484..1b00a2bb472fb 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -717,6 +717,9 @@ generate_token_success=新令牌生成成功。请拷贝因为令牌将只会显 generate_token_name_duplicate=%s 已被用作应用程序名称。请使用一个新的名称。 delete_token=删除令牌 access_token_deletion=删除 Access Token +access_token_deletion_cancel_action=取消 +access_token_deletion_confirm_action=刪除 +access_token_deletion_desc=删除令牌将撤销程序对您账户的访问权限。此操作无法撤消。是否继续? delete_token_success=令牌已经被删除。使用该令牌的应用将不再能够访问你的账号。 manage_oauth2_applications=管理 OAuth2 应用程序 @@ -1808,6 +1811,9 @@ settings.tracker_url_format_error=外部工单链接无效 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=仅允许成员跟踪时间 @@ -2283,6 +2289,8 @@ topic.done=保存 topic.count_prompt=您最多选择25个主题 topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符 +find_file.go_to_file=转到文件 +find_file.no_matching=没有找到匹配的文件 error.csv.too_large=无法渲染此文件,因为它太大了。 error.csv.unexpected=无法渲染此文件,因为它包含了意外字符,其位于第 %d 行和第 %d 列。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 8ce92f03c98c5..2cf099fda49b6 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1789,6 +1789,9 @@ 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=只讓貢獻者追蹤時間 @@ -3000,4 +3003,6 @@ error.no_unit_allowed_repo=您未被允許存取此儲存庫的任何區域。 error.unit_not_allowed=您未被允許訪問此儲存庫區域 [packages] +dependency.id=ID +dependency.version=版本 diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index 38cdbd49735d0..393f8ed3d9316 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -128,7 +128,7 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN case addrErr.IsProtocolInvalid: ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form) + ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form) @@ -139,11 +139,11 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form) default: log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form) + ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) } } else { log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error"), tpl, form) + ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) } } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 8df4ccc607862..d698f1c49a79a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" + pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -36,6 +37,7 @@ import ( "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/utils" asymkey_service "code.gitea.io/gitea/services/asymkey" + "code.gitea.io/gitea/services/automerge" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/gitdiff" pull_service "code.gitea.io/gitea/services/pull" @@ -966,6 +968,22 @@ func MergePullRequest(ctx *context.Context) { message += "\n\n" + form.MergeMessageField } + if form.MergeWhenChecksSucceed { + // delete all scheduled auto merges + _ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID) + // schedule auto merge + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + if err != nil { + ctx.ServerError("ScheduleAutoMerge", err) + return + } else if scheduled { + // nothing more to do ... + ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index)) + return + } + } + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) @@ -1070,6 +1088,26 @@ func MergePullRequest(ctx *context.Context) { ctx.Redirect(issue.Link()) } +// CancelAutoMergePullRequest cancels a scheduled pr +func CancelAutoMergePullRequest(ctx *context.Context) { + issue := checkPullInfo(ctx) + if ctx.Written() { + return + } + + if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil { + if db.IsErrNotExist(err) { + ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) + return + } + ctx.ServerError("RemoveScheduledAutoMerge", err) + return + } + ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule")) + ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index)) +} + func stopTimerIfAvailable(user *user_model.User, issue *models.Issue) error { if models.StopwatchExists(user.ID, issue.ID) { if err := models.CreateOrStopIssueStopwatch(user, issue); err != nil { diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index f49ef6e85d56a..fae62c102077c 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -57,8 +57,9 @@ const ( tplProtectedBranch base.TplName = "repo/settings/protected_branch" ) -// Settings show a repository's settings page -func Settings(ctx *context.Context) { +// SettingsCtxData is a middleware that sets all the general context data for the +// settings template. +func SettingsCtxData(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate @@ -94,15 +95,16 @@ func Settings(ctx *context.Context) { return } ctx.Data["PushMirrors"] = pushMirrors +} +// Settings show a repository's settings page +func Settings(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsOptions) } // SettingsPost response for changes of a repository func SettingsPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) - ctx.Data["Title"] = ctx.Tr("repo.settings") - ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled @@ -215,22 +217,24 @@ func SettingsPost(ctx *context.Context) { return } - u, _ := git.GetRemoteAddress(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName()) + u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Mirror.GetRemoteName()) + if err != nil { + ctx.Data["Err_MirrorAddress"] = true + handleSettingRemoteAddrError(ctx, err, form) + return + } if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() { form.MirrorPassword, _ = u.User.Password() } - address, err := forms.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword) - if err == nil { - err = migrations.IsMigrateURLAllowed(address, ctx.Doer) - } + err = migrations.IsMigrateURLAllowed(u.String(), ctx.Doer) if err != nil { ctx.Data["Err_MirrorAddress"] = true handleSettingRemoteAddrError(ctx, err, form) return } - if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, address); err != nil { + if err := mirror_service.UpdateAddress(ctx, ctx.Repo.Mirror, u.String()); err != nil { ctx.ServerError("UpdateAddress", err) return } @@ -825,7 +829,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R case addrErr.IsProtocolInvalid: ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error"), tplSettingsOptions, form) + ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form) diff --git a/routers/web/web.go b/routers/web/web.go index bf4c4662afd7c..ad005f74df4d7 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -126,7 +126,7 @@ func Routes() *web.Route { routes.Route("/avatars/*", "GET, HEAD", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.Route("/repo-avatars/*", "GET, HEAD", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) - // for health check - doeesn't need to be passed through gzip handler + // for health check - doesn't need to be passed through gzip handler routes.Head("/", func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) }) @@ -730,8 +730,10 @@ func RegisterRoutes(m *web.Route) { m.Group("/{username}/{reponame}", func() { m.Group("/settings", func() { - m.Combo("").Get(repo.Settings). - Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost) + m.Group("", func() { + m.Combo("").Get(repo.Settings). + Post(bindIgnErr(forms.RepoSettingForm{}), repo.SettingsPost) + }, repo.SettingsCtxData) m.Post("/avatar", bindIgnErr(forms.AvatarForm{}), repo.SettingsAvatar) m.Post("/avatar/delete", repo.SettingsDeleteAvatar) @@ -1127,6 +1129,7 @@ func RegisterRoutes(m *web.Route) { m.Get(".patch", repo.DownloadPullPatch) m.Get("/commits", context.RepoRef(), repo.ViewPullCommits) m.Post("/merge", context.RepoMustNotBeArchived(), bindIgnErr(forms.MergePullRequestForm{}), repo.MergePullRequest) + m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest) m.Post("/update", repo.UpdatePullRequest) m.Post("/set_allow_maintainer_edit", bindIgnErr(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits) m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 738a77d2bbc99..23ac1abe3c286 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -101,7 +101,7 @@ func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, err strings.HasPrefix(remoteAddr, "git://") { u, err := url.Parse(remoteAddr) if err != nil { - return "", &models.ErrInvalidCloneAddr{IsURLError: true} + return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr} } if len(authUsername)+len(authPassword) > 0 { u.User = url.UserPassword(authUsername, authPassword) diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 700f06af35dc0..ce76733bd51a8 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -44,7 +44,7 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error { // Remote address can be HTTP/HTTPS/Git URL or local path. u, err := url.Parse(remoteURL) if err != nil { - return &models.ErrInvalidCloneAddr{IsURLError: true} + return &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteURL} } if u.Scheme == "file" || u.Scheme == "" { diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index caa81f0fe9852..f4c527bbdc226 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -210,9 +210,10 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo } gitArgs = append(gitArgs, m.GetRemoteName()) - remoteAddr, remoteErr := git.GetRemoteAddress(ctx, repoPath, m.GetRemoteName()) + remoteURL, remoteErr := git.GetRemoteURL(ctx, repoPath, m.GetRemoteName()) if remoteErr != nil { log.Error("SyncMirrors [repo: %-v]: GetRemoteAddress Error %v", m.Repo, remoteErr) + return nil, false } stdoutBuilder := strings.Builder{} @@ -291,7 +292,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo if m.LFS && setting.LFS.StartServer { log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo) - endpoint := lfs.DetermineEndpoint(remoteAddr.String(), m.LFSEndpoint) + endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint) lfsClient := lfs.NewClient(endpoint, nil) if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil { log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err) diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go index 138ebb737b235..2927bed72b279 100644 --- a/services/mirror/mirror_push.go +++ b/services/mirror/mirror_push.go @@ -131,7 +131,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second performPush := func(path string) error { - remoteAddr, err := git.GetRemoteAddress(ctx, path, m.RemoteName) + remoteURL, err := git.GetRemoteURL(ctx, path, m.RemoteName) if err != nil { log.Error("GetRemoteAddress(%s) Error %v", path, err) return errors.New("Unexpected error") @@ -147,7 +147,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error { } defer gitRepo.Close() - endpoint := lfs.DetermineEndpoint(remoteAddr.String(), "") + endpoint := lfs.DetermineEndpoint(remoteURL.String(), "") lfsClient := lfs.NewClient(endpoint, nil) if err := pushAllLFSObjects(ctx, gitRepo, lfsClient); err != nil { return util.SanitizeErrorCredentialURLs(err) diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2d963d67c8974..cfac37cd115b0 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -37,7 +37,9 @@ {{end}} - {{if .IsMirror}}
{{$.i18n.Tr "repo.mirror_from"}} {{if .SanitizedOriginalURL}}{{.SanitizedOriginalURL}}{{else}}{{(MirrorRemoteAddress $.Context $.Mirror).Address}}{{end}}
{{end}} + {{if .IsMirror}} + {{$address := MirrorRemoteAddress $.Context . $.Mirror.GetRemoteName}} +
{{$.i18n.Tr "repo.mirror_from"}} {{$address.Address}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{.BaseRepo.FullName}}
{{end}} {{if .IsGenerated}}
{{$.i18n.Tr "repo.generated_from"}} {{.TemplateRepo.FullName}}
{{end}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index b6b1c76d787a1..235044cb17bf2 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -18,11 +18,11 @@ @@ -83,8 +83,10 @@ {{end}} {{end}} - {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}} - {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}} + {{svg "octicon-issue-opened" 16 "mr-3"}} + {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} + {{svg "octicon-check" 16 "mr-3"}} + {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}} {{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}} {{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{end}} diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl index 050660522a5b6..ae99f091b14ed 100644 --- a/templates/repo/issue/openclose.tmpl +++ b/templates/repo/issue/openclose.tmpl @@ -1,10 +1,14 @@ diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 235f4c8fc2662..0258a9f9691c0 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -843,8 +843,8 @@ {{svg "octicon-git-merge" 16}} {{.Poster.GetDisplayName}} - {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.pull_request_scheduled_auto_merge" $createdStr | Safe}} - {{else}}{{$.i18n.Tr "repo.pulls.pull_request_canceled_scheduled_auto_merge" $createdStr | Safe}}{{end}} + {{if eq .Type 34}}{{$.i18n.Tr "repo.pulls.auto_merge_newly_scheduled_comment" $createdStr | Safe}} + {{else}}{{$.i18n.Tr "repo.pulls.auto_merge_canceled_schedule_comment" $createdStr | Safe}}{{end}} {{end}} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index c764138fa0474..d2282f07f6e31 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -251,8 +251,14 @@ {{$.i18n.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason) }} {{end}} + {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not .RequiredStatusCheckState.IsSuccess))}} - {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + + {{/* admin can merge without checks, writer can merge when checkes succeed */}} + {{$canMergeNow := and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} + {{/* admin and writer both can make an auto merge schedule */}} + + {{if $canMergeNow}} {{if $notAllOverridableChecksOk}}
{{svg "octicon-dot-fill"}} @@ -277,7 +283,6 @@ {{end}} {{end}} - {{$canAutoMerge = true}} {{if (gt .Issue.PullRequest.CommitsBehind 0)}}
@@ -317,112 +322,111 @@
{{end}} - {{if and (or $.IsRepoAdmin (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} - {{if .AllowMerge}} - {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} - {{$approvers := .Issue.PullRequest.GetApprovers}} - {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{if .AllowMerge}} {{/* user is allowed to merge */}} + {{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}} + {{$approvers := .Issue.PullRequest.GetApprovers}} + {{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}} + {{$hasPendingPullRequestMergeTip := ""}} + {{if .HasPendingPullRequestMerge}} + {{$createdPRMergeStr := TimeSinceUnix .PendingPullRequestMerge.CreatedUnix $.i18n.Lang}} + {{$hasPendingPullRequestMergeTip = $.i18n.Tr "repo.pulls.auto_merge_has_pending_schedule" .PendingPullRequestMerge.Doer.Name $createdPRMergeStr}} + {{end}} +
+ + const generalHideAutoMerge = mergeForm.canMergeNow && mergeForm.allOverridableChecksOk; // if this PR can be merged now, then hide the auto merge + mergeForm['mergeStyles'] = [ + { + 'name': 'merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebase}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_pull_request"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'rebase-merge', + 'allowed': {{$prUnit.PullRequestsConfig.AllowRebaseMerge}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}, + 'mergeTitleFieldText': defaultMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'squash', + 'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}, + 'mergeTitleFieldText': defaultSquashMergeTitle, + 'mergeMessageFieldText': defaultMergeMessage, + 'hideAutoMerge': generalHideAutoMerge, + }, + { + 'name': 'manually-merged', + 'allowed': {{and $prUnit.PullRequestsConfig.AllowManualMerge $.IsRepoAdmin}}, + 'textDoMerge': {{$.i18n.Tr "repo.pulls.merge_manually"}}, + 'hideMergeMessageTexts': true, + 'hideAutoMerge': true, + } + ]; + window.config.pageData.pullRequestMergeForm = mergeForm; + })(); + -
+
- {{if .ShowMergeInstructions}} -
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}
- - {{end}} - {{else}} -
-
- {{svg "octicon-x"}} - {{$.i18n.Tr "repo.pulls.no_merge_desc"}} -
-
- {{svg "octicon-info"}} - {{$.i18n.Tr "repo.pulls.no_merge_helper"}} -
+ {{if .ShowMergeInstructions}} + {{template "repo/issue/view_content/pull_merge_instruction" (dict "i18n" .i18n "Issue" .Issue)}} {{end}} {{else}} + {{/* no merge style was set in repo setting: not or ($prUnit.PullRequestsConfig.AllowMerge ...) */}}
+
+ {{svg "octicon-x"}} + {{$.i18n.Tr "repo.pulls.no_merge_desc"}} +
{{svg "octicon-info"}} - {{$.i18n.Tr "repo.pulls.no_merge_access"}} + {{$.i18n.Tr "repo.pulls.no_merge_helper"}}
- {{end}} - {{end}} + {{end}} {{/* end if the repo was set to use any merge style */}} + {{else}} + {{/* user is not allowed to merge */}} +
+
+ {{svg "octicon-info"}} + {{$.i18n.Tr "repo.pulls.no_merge_access"}} +
+ {{end}} {{/* end if user is allowed to merge or not */}} {{else}} {{/* Merge conflict without specific file. Suggest manual merge, only if all reviews and status checks OK. */}} {{if .IsBlockedByApprovals}} diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl new file mode 100644 index 0000000000000..0ed70860f3ee7 --- /dev/null +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -0,0 +1,19 @@ +
{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}
+ diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl index a8c51d668e247..7a2366da0b76e 100644 --- a/templates/repo/projects/list.tmpl +++ b/templates/repo/projects/list.tmpl @@ -14,12 +14,12 @@ {{template "base/alert" .}} @@ -47,8 +47,10 @@ {{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.closed" $closedDate|Str2html}} {{end}} - {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}} - {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}} + {{svg "octicon-issue-opened" 16 "mr-3"}} + {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} + {{svg "octicon-check" 16 "mr-3"}} + {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}}
{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 67a98aff43cda..68cbd4de2c18f 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -91,7 +91,7 @@ {{if .Repository.IsMirror}} - {{(MirrorRemoteAddress $.Context .Mirror).Address}} + {{(MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName).Address}} {{$.i18n.Tr "repo.settings.mirror_settings.direction.pull"}} {{.Mirror.UpdatedUnix.AsTime}} @@ -119,7 +119,7 @@ - {{$address := MirrorRemoteAddress $.Context .Mirror}} + {{$address := MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName}}
@@ -168,7 +168,7 @@ {{range .PushMirrors}} - {{$address := MirrorRemoteAddress $.Context .}} + {{$address := MirrorRemoteAddress $.Context $.Repository .GetRemoteName}} {{$address.Address}} {{$.i18n.Tr "repo.settings.mirror_settings.direction.push"}} {{if .LastUpdateUnix}}{{.LastUpdateUnix.AsTime}}{{else}}{{$.i18n.Tr "never"}}{{end}} {{if .LastError}}
{{$.i18n.Tr "error"}}
{{end}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index c5efd3d2d4f66..fe5cb7b170584 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,6 +1,6 @@

-
+
{{if .ReadmeInList}} {{svg "octicon-book" 16 "mr-3"}} {{.FileName}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index ffaf5bb4ee01c..bd7d54b6705d9 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -61,11 +61,11 @@
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 385f5c529b3a9..738438423f32c 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -38,12 +38,12 @@ @@ -103,9 +103,13 @@ {{end}} {{end}} - {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.milestones.open_tab" .NumOpenIssues}} - {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.milestones.close_tab" .NumClosedIssues}} - {{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}} + {{svg "octicon-issue-opened" 16 "mr-3"}} + {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}} + {{svg "octicon-check" 16 "mr-3"}} + {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}} + {{if .TotalTrackedTime}} + {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}} + {{end}}
{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 40398a65cba74..75fbceb8007a7 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -1,9 +1,23 @@ @@ -68,6 +115,7 @@ export default { mergeTitleFieldValue: '', mergeMessageFieldValue: '', deleteBranchAfterMerge: false, + autoMergeWhenSucceed: false, mergeStyle: '', mergeStyleDetail: { // dummy only, these values will come from one of the mergeForm.mergeStyles @@ -82,6 +130,13 @@ export default { showActionForm: false, }), + computed: { + mergeButtonStyleClass() { + if (this.mergeForm.allOverridableChecksOk) return 'green'; + return this.autoMergeWhenSucceed ? 'blue' : 'red'; + } + }, + watch: { mergeStyle(val) { this.mergeStyleDetail = this.mergeForm.mergeStyles.find((e) => e.name === val); @@ -90,7 +145,7 @@ export default { created() { this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); - this.mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name; + this.switchMergeStyle(this.mergeForm.mergeStyles.find((e) => e.allowed)?.name, !this.mergeForm.canMergeNow); }, mounted() { @@ -111,7 +166,11 @@ export default { this.deleteBranchAfterMerge = this.mergeForm.defaultDeleteBranchAfterMerge; this.mergeTitleFieldValue = this.mergeStyleDetail.mergeTitleFieldText; this.mergeMessageFieldValue = this.mergeStyleDetail.mergeMessageFieldText; - } + }, + switchMergeStyle(name, autoMerge = false) { + this.mergeStyle = name; + this.autoMergeWhenSucceed = autoMerge; + }, }, }; @@ -124,4 +183,59 @@ export default { .ui.checkbox label { cursor: pointer; } + +/* make the dropdown list left-aligned */ +.ui.merge-button { + position: relative; +} +.ui.merge-button .ui.dropdown { + position: static; +} +.ui.merge-button > .ui.dropdown:last-child > .menu:not(.left) { + left: 0; + right: auto; +} +.ui.merge-button .ui.dropdown .menu > .item { + display: flex; + align-items: stretch; + padding: 0 !important; /* polluted by semantic.css: .ui.dropdown .menu > .item { !important } */ +} + +/* merge style list item */ +.action-text { + padding: 0.8rem; + flex: 1 +} + +.auto-merge-small { + width: 40px; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} +.auto-merge-small .auto-merge-tip { + display: none; + left: 38px; + top: -1px; + bottom: -1px; + position: absolute; + align-items: center; + color: var(--color-info-text); + background-color: var(--color-info-bg); + border: 1px solid var(--color-info-border); + border-left: none; + padding-right: 1rem; +} + +.auto-merge-small:hover { + color: var(--color-info-text); + background-color: var(--color-info-bg); + border: 1px solid var(--color-info-border); +} + +.auto-merge-small:hover .auto-merge-tip { + display: flex; +} + diff --git a/web_src/js/features/formatting.js b/web_src/js/features/formatting.js new file mode 100644 index 0000000000000..a7ee7ec3cf36f --- /dev/null +++ b/web_src/js/features/formatting.js @@ -0,0 +1,14 @@ +import {prettyNumber} from '../utils.js'; + +const {lang} = document.documentElement; + +export function initFormattingReplacements() { + // replace english formatted numbers with locale-specific separators + for (const el of document.getElementsByClassName('js-pretty-number')) { + const num = Number(el.getAttribute('data-value')); + const formatted = prettyNumber(num, lang); + if (formatted && formatted !== el.textContent) { + el.textContent = formatted; + } + } +} diff --git a/web_src/js/index.js b/web_src/js/index.js index b6a1aee7796f6..0568da64aec51 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -84,6 +84,11 @@ import {initRepoBranchButton} from './features/repo-branch.js'; import {initCommonOrganization} from './features/common-organization.js'; import {initRepoWikiForm} from './features/repo-wiki.js'; import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; +import {initFormattingReplacements} from './features/formatting.js'; + +// Run time-critical code as soon as possible. This is safe to do because this +// script appears at the end of and rendered HTML is accessible at that point. +initFormattingReplacements(); // Silence fomantic's error logging when tabs are used without a target content element $.fn.tab.settings.silent = true; diff --git a/web_src/js/svg.js b/web_src/js/svg.js index 926f0a5d05d80..9c39852c303ef 100644 --- a/web_src/js/svg.js +++ b/web_src/js/svg.js @@ -1,6 +1,7 @@ import octiconChevronDown from '../../public/img/svg/octicon-chevron-down.svg'; import octiconChevronRight from '../../public/img/svg/octicon-chevron-right.svg'; import octiconCopy from '../../public/img/svg/octicon-copy.svg'; +import octiconClock from '../../public/img/svg/octicon-clock.svg'; import octiconGitMerge from '../../public/img/svg/octicon-git-merge.svg'; import octiconGitPullRequest from '../../public/img/svg/octicon-git-pull-request.svg'; import octiconIssueClosed from '../../public/img/svg/octicon-issue-closed.svg'; @@ -23,6 +24,7 @@ export const svgs = { 'octicon-chevron-down': octiconChevronDown, 'octicon-chevron-right': octiconChevronRight, 'octicon-copy': octiconCopy, + 'octicon-clock': octiconClock, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, 'octicon-issue-closed': octiconIssueClosed, diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 67f8f1cc9865d..f01f2d3b22447 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -90,3 +90,10 @@ export function strSubMatch(full, sub) { } return res; } + +// pretty-print a number using locale-specific separators, e.g. 1200 -> 1,200 +export function prettyNumber(num, locale = 'en-US') { + if (typeof num !== 'number') return ''; + const {format} = new Intl.NumberFormat(locale); + return format(num); +} diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index acf3f1ece3e09..ba5335e3e43bf 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -1,5 +1,5 @@ import { - basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch, + basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch, prettyNumber, } from './utils.js'; test('basename', () => { @@ -85,7 +85,6 @@ test('parseIssueHref', () => { expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined}); }); - test('strSubMatch', () => { expect(strSubMatch('abc', '')).toEqual(['abc']); expect(strSubMatch('abc', 'a')).toEqual(['', 'a', 'bc']); @@ -98,3 +97,14 @@ test('strSubMatch', () => { expect(strSubMatch('aabbcc', 'abc')).toEqual(['', 'a', 'a', 'b', 'b', 'c', 'c']); expect(strSubMatch('the/directory', 'hedir')).toEqual(['t', 'he', '/', 'dir', 'ectory']); }); + +test('prettyNumber', () => { + expect(prettyNumber()).toEqual(''); + expect(prettyNumber(null)).toEqual(''); + expect(prettyNumber(undefined)).toEqual(''); + expect(prettyNumber('1200')).toEqual(''); + expect(prettyNumber(12345678, 'en-US')).toEqual('12,345,678'); + expect(prettyNumber(12345678, 'de-DE')).toEqual('12.345.678'); + expect(prettyNumber(12345678, 'be-BE')).toEqual('12 345 678'); + expect(prettyNumber(12345678, 'hi-IN')).toEqual('1,23,45,678'); +}); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index c029cb9485050..4d7f69e3b3593 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -2003,14 +2003,6 @@ table th[data-sortt-desc] { margin-right: 0 !important; } -/* limit width of all direct dropdown menu children */ -/* https://github.com/go-gitea/gitea/pull/10835 */ -.dropdown:not(.selection) > .menu:not(.review-box) > *:not(.header) { - max-width: 300px; - overflow-x: hidden; - text-overflow: ellipsis; -} - .ui.dropdown .menu .item { border-radius: 0; } diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index d73cb903304a8..2686c0d280f3b 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -1055,10 +1055,6 @@ .merge-section { background-color: var(--color-box-body); - .item { - padding: .25rem 0; - } - .item-section { display: flex; align-items: center; @@ -3052,9 +3048,10 @@ td.blob-excerpt { } .file-header { + align-items: center; display: flex; justify-content: space-between; - align-items: center; + overflow-x: scroll; padding: 8px 12px !important; } @@ -3066,6 +3063,7 @@ td.blob-excerpt { .file-info-entry { display: flex; align-items: center; + width: max-content; } .file-info-entry + .file-info-entry {