{{template "repo/header" .}}
- {{$class := ""}} - {{if .Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} -
+

{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}

{{if not $.PageIsWiki}} @@ -142,125 +128,59 @@ {{end}} {{template "repo/commit_load_branches_and_tags" .}}
-
-
- {{if .Author}} - {{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}} - {{if .Author.FullName}} - {{.Author.FullName}} - {{else}} - {{.Commit.Author.Name}} - {{end}} + +
+
+ {{if .Author}} + {{ctx.AvatarUtils.Avatar .Author 20}} + {{if .Author.FullName}} + {{.Author.FullName}} {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}} - {{.Commit.Author.Name}} + {{.Commit.Author.Name}} {{end}} - {{DateUtils.TimeSince .Commit.Author.When}} - {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} - {{ctx.Locale.Tr "repo.diff.committed_by"}} - {{if ne .Verification.CommittingUser.ID 0}} - {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}} - {{.Commit.Committer.Name}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}} - {{.Commit.Committer.Name}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}} + {{.Commit.Author.Name}} + {{end}} +
+ + {{DateUtils.TimeSince .Commit.Author.When}} + +
+ {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} + {{ctx.Locale.Tr "repo.diff.committed_by"}} + {{if ne .Verification.CommittingUser.ID 0}} + {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}} + {{.Commit.Committer.Name}} + {{else}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}} + {{.Commit.Committer.Name}} + {{end}} + {{end}} +
+ + {{if .Verification}} + {{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}} + {{end}} + +
+ +
+ {{if .Parents}} +
+ {{ctx.Locale.Tr "repo.diff.parent"}} + {{range .Parents}} + {{ShortSha .}} {{end}} - {{end}} -
-
- {{if .Parents}} -
- {{ctx.Locale.Tr "repo.diff.parent"}} - {{range .Parents}} - {{if $.PageIsWiki}} - {{ShortSha .}} - {{else}} - {{ShortSha .}} - {{end}} - {{end}} -
- {{end}} -
- {{ctx.Locale.Tr "repo.diff.commit"}} - {{ShortSha .CommitID}}
-
-
- {{if .Commit.Signature}} -
-
- {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "gitea-lock" 16 "tw-mr-2"}} - {{if eq .Verification.TrustStatus "trusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{else if eq .Verification.TrustStatus "untrusted"}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: - {{else}} - {{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: - {{end}} - {{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{else}} - {{svg "gitea-lock-cog" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.signed_by"}}: - {{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}} - {{.Verification.SigningUser.GetDisplayName}} - {{end}} - {{else}} - {{svg "gitea-unlock" 16 "tw-mr-2"}} - {{ctx.Locale.Tr .Verification.Reason}} - {{end}} -
-
- {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{else if .Verification.Warning}} - {{svg "octicon-unverified" 16 "tw-mr-2"}} - {{if .Verification.SigningSSHKey}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{else}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{else}} - {{if .Verification.SigningKey}} - {{if ne .Verification.SigningKey.KeyID ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.gpg_key_id"}}: - {{.Verification.SigningKey.PaddedKeyID}} - {{end}} - {{end}} - {{if .Verification.SigningSSHKey}} - {{if ne .Verification.SigningSSHKey.Fingerprint ""}} - {{svg "octicon-verified" 16 "tw-mr-2"}} - {{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}: - {{.Verification.SigningSSHKey.Fingerprint}} - {{end}} - {{end}} - {{end}} + {{end}} +
+ {{ctx.Locale.Tr "repo.diff.commit"}} + {{ShortSha .CommitID}}
- {{end}} +
+ {{if .NoteRendered}}
{{svg "octicon-note" 16 "tw-mr-2"}} @@ -276,12 +196,13 @@ {{else}} {{.NoteCommit.Author.Name}} {{end}} - {{DateUtils.TimeSince .NoteCommit.Author.When}} + {{DateUtils.TimeSince .NoteCommit.Author.When}}
{{.NoteRendered | SanitizeHTML}}
{{end}} + {{template "repo/diff/box" .}}
diff --git a/templates/repo/commit_sign_badge.tmpl b/templates/repo/commit_sign_badge.tmpl new file mode 100644 index 0000000000..aa68e9dd23 --- /dev/null +++ b/templates/repo/commit_sign_badge.tmpl @@ -0,0 +1,78 @@ +{{/* Template attributes: +* Commit +* CommitBaseLink +* CommitSignVerification +If you'd like to modify this template, you could test it on the devtest page. +ATTENTION: this template could be re-rendered many times (on the graph and commit list page), +so this template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. +*/}} +{{- $commit := $.Commit -}} +{{- $commitBaseLink := $.CommitBaseLink -}} +{{- $verification := $.CommitSignVerification -}} + +{{- $extraClass := "" -}} +{{- $verified := false -}} +{{- $signingUser := NIL -}} +{{- $signingEmail := "" -}} +{{- $msgReasonPrefix := "" -}} +{{- $msgReason := "" -}} +{{- $msgSigningKey := "" -}} + +{{- if $verification -}} + {{- $signingUser = $verification.SigningUser -}} + {{- $signingEmail = $verification.SigningEmail -}} + {{- $extraClass = print $extraClass " commit-is-signed" -}} + {{- if $verification.Verified -}} + {{- /* reason is "{name} / {key-id}" */ -}} + {{- $msgReason = $verification.Reason -}} + {{- $verified = true -}} + {{- if eq $verification.TrustStatus "trusted" -}} + {{- $extraClass = print $extraClass " sign-trusted" -}} + {{- else if eq $verification.TrustStatus "untrusted" -}} + {{- $extraClass = print $extraClass " sign-untrusted" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}} + {{- else -}} + {{- $extraClass = print $extraClass " sign-unmatched" -}} + {{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}} + {{- end -}} + {{- else -}} + {{- if $verification.Warning -}} + {{- $extraClass = print $extraClass " sign-warning" -}} + {{- end -}} + {{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}} + {{- end -}} + + {{- if $msgReasonPrefix -}} + {{- $msgReason = print $msgReasonPrefix ": " $msgReason -}} + {{- end -}} + + {{- if $verification.SigningSSHKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}} + {{- else if $verification.SigningKey -}} + {{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}} + {{- end -}} +{{- end -}} + +{{- if $commit -}} + + {{- ShortSha $commit.ID.String -}} +{{- end -}} + + {{- if $verified -}} + {{- if and $signingUser $signingUser.ID -}} + {{svg "gitea-lock"}} + {{ctx.AvatarUtils.Avatar $signingUser 16}} + {{- else -}} + {{svg "gitea-lock-cog"}} + {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}} + {{- end -}} + {{- else -}} + {{svg "gitea-unlock"}} + {{- end -}} + + +{{- if $commit -}} + +{{- end -}} + +{{- /* This template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. */ -}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 50b754cc23..329dc45149 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -28,33 +28,15 @@
- {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - {{$commitShaLink := ""}} + {{$commitBaseLink := ""}} {{if $.PageIsWiki}} - {{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}} {{else if $.PageIsPullCommits}} - {{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}} {{else if $.Reponame}} - {{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}} + {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} - - {{ShortSha .ID.String}} - {{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}} - + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 0657eaba1d..2acf7c58b8 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -3,7 +3,7 @@ {{range .comment.Commits}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}} -
+
{{/*singular-commit*/}} {{svg "octicon-git-commit"}} {{if .User}} {{ctx.AvatarUtils.Avatar .User 20}} @@ -11,7 +11,8 @@ {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} {{end}} - {{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}} + {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} + {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}} @@ -21,29 +22,9 @@ {{end}} - + {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{$class := "ui sha label"}} - {{if .Signature}} - {{$class = (print $class " isSigned")}} - {{if .Verification.Verified}} - {{if eq .Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq .Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if .Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - - {{ShortSha .ID.String}} - {{if .Signature}} - {{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}} - {{end}} - + {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
{{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/diff/section_unified.tmpl b/templates/repo/diff/section_unified.tmpl index a06cd2ddd1..cb612bc27c 100644 --- a/templates/repo/diff/section_unified.tmpl +++ b/templates/repo/diff/section_unified.tmpl @@ -1,5 +1,7 @@ {{$file := .file}} -{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}} +{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}} +{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}} +{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index f1d0e62330..6af0ba1f0f 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -5,33 +5,13 @@ {{if $commit.OnlyRelation}} {{else}} - - {{$class := "ui sha label"}} - {{if $commit.Commit.Signature}} - {{$class = (print $class " isSigned")}} - {{if $commit.Verification.Verified}} - {{if eq $commit.Verification.TrustStatus "trusted"}} - {{$class = (print $class " isVerified")}} - {{else if eq $commit.Verification.TrustStatus "untrusted"}} - {{$class = (print $class " isVerifiedUntrusted")}} - {{else}} - {{$class = (print $class " isVerifiedUnmatched")}} - {{end}} - {{else if $commit.Verification.Warning}} - {{$class = (print $class " isWarning")}} - {{end}} - {{end}} - - {{ShortSha $commit.Commit.ID.String}} - {{- if $commit.Commit.Signature -}} - {{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}} - {{- end -}} - - - + {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} + + {{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}} - + + {{range $commit.Refs}} {{$refGroup := .RefGroup}} {{if eq $refGroup "pull"}} @@ -56,7 +36,8 @@ {{end}} {{end}} - + + {{$userName := $commit.Commit.Author.Name}} {{if $commit.User}} {{if and $commit.User.FullName DefaultShowFullName}} @@ -69,7 +50,8 @@ {{$userName}} {{end}} - {{DateUtils.FullTime $commit.Date}} + + {{DateUtils.FullTime $commit.Date}} {{end}} {{end}} diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index 34a5df8f77..b176b4190c 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -1,3 +1,4 @@ +
{{if not .LatestCommit}} … {{else}} @@ -14,13 +15,11 @@ {{.LatestCommit.Author.Name}} {{end}} {{end}} - - {{ShortSha .LatestCommit.ID.String}} - {{if .LatestCommit.Signature}} - {{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}} - {{end}} - + + {{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}} + {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} + {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}} {{if IsMultilineCommitMessage .LatestCommit.Message}} @@ -29,3 +28,4 @@ {{end}} {{end}} +
diff --git a/templates/repo/shabox_badge.tmpl b/templates/repo/shabox_badge.tmpl deleted file mode 100644 index 36fc9e04b1..0000000000 --- a/templates/repo/shabox_badge.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -
- {{if .verification.Verified}} -
- {{if ne .verification.SigningUser.ID 0}} - {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar .verification.SigningUser 16 "signature"}} - {{else}} - {{svg "gitea-lock-cog"}} - {{ctx.AvatarUtils.AvatarByEmail .verification.SigningEmail "" 16 "signature"}} - {{end}} -
- {{else}} - {{svg "gitea-unlock"}} - {{end}} -
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 86366ae053..0a43e3db54 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -12,9 +12,7 @@ {{if not .ReadmeInList}}
-
- {{template "repo/latest_commit" .}} -
+ {{template "repo/latest_commit" .}} {{if .LatestCommit}} {{if .LatestCommit.Committer}}
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 2d555e4c2e..01bb70e06f 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,7 +1,7 @@ {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
-
{{template "repo/latest_commit" .}}
+ {{template "repo/latest_commit" .}}
{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
{{if .HasParentPath}} diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index bb65d9e04a..fc066e06d3 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -30,7 +30,7 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) } @@ -46,7 +46,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -64,7 +64,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { doc = NewHTMLParser(t, resp.Body) // Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger) - sel := doc.doc.Find("#commits-table tbody tr td.message .tippy-target .commit-status") + sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status") assert.Equal(t, 1, sel.Length()) for _, class := range classes { assert.True(t, sel.HasClass(class)) @@ -140,7 +140,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -175,7 +175,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc := NewHTMLParser(t, resp.Body) // Get first commit URL - commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href") + commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href") assert.True(t, exists) assert.NotEmpty(t, commitURL) @@ -200,6 +200,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { doc = NewHTMLParser(t, resp.Body) // Check that the data-tippy="commit-statuses" (for trigger) and commit-status (svg) are present - sel := doc.doc.Find("#commits-table tbody tr td.message [data-tippy=\"commit-statuses\"] .commit-status") + sel := doc.doc.Find("#commits-table .message [data-tippy=\"commit-statuses\"] .commit-status") assert.Equal(t, 1, sel.Length()) } diff --git a/web_src/css/base.css b/web_src/css/base.css index 04f3678f3a..e67d51bb4d 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -742,15 +742,10 @@ input:-webkit-autofill:active, font-family: var(--fonts-monospace); font-size: 13px; font-weight: var(--font-weight-normal); - margin: 0 6px; - padding: 5px 10px; + padding: 3px 5px; flex-shrink: 0; } -.ui .sha.label .shortsha { - display: inline-block; /* not sure whether it is still needed */ -} - .ui .button.truncate { display: inline-block; max-width: 100%; diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index f8f7e35cdc..1ed541a695 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -57,6 +57,12 @@ white-space: nowrap; display: flex; align-items: center; + gap: 0.25em; +} + +#git-graph-container li .ui.label.commit-id-short { + padding-top: 2px; + padding-bottom: 2px; } #git-graph-container li .node-relation { @@ -112,17 +118,6 @@ text-overflow: ellipsis; } -#git-graph-container #rev-list .sha.label { - padding-top: 5px; - padding-bottom: 3px; -} - -#git-graph-container #rev-list .sha.label .ui.detail.icon.button { - padding-top: 3px; - margin-top: -5px; - padding-bottom: 1px; -} - #git-graph-container #graph-raw-list { margin: 0; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 6fdc9ec2a8..dcc4161c59 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -120,15 +120,7 @@ td .commit-summary { align-items: center; overflow: hidden; text-overflow: ellipsis; -} - -@media (max-width: 767.98px) { - .latest-commit .sha { - display: none; - } - .latest-commit .commit-summary { - margin-left: 8px; - } + gap: 0.25em; } .repo-path { @@ -605,15 +597,6 @@ td .commit-summary { margin-right: 0.25em; } -.singular-commit { - display: flex; - align-items: center; -} - -.singular-commit .badge { - height: 30px !important; -} - .repository.view.issue .comment-list .timeline-item.event > .commit-status-link { float: right; margin-right: 8px; @@ -936,14 +919,6 @@ td .commit-summary { width: 200px; } -.repository #commits-table thead .shatd { - text-align: center; -} - -.repository #commits-table td.sha .sha.label { - margin: 0; -} - .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { background-color: var(--color-light) !important; } @@ -1440,12 +1415,6 @@ td .commit-summary { padding-top: 15px; } -.commit-header-row { - min-height: 50px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - .commit-header-buttons { display: flex; gap: 4px; @@ -2128,18 +2097,6 @@ tbody.commit-list { .repository.view.issue .comment-list .timeline .comment-header-right .role-label { display: none; } - .commit-header-row .ui.horizontal.list { - width: 100%; - overflow-x: auto; - margin-top: 2px; - } - .commit-header-row .ui.horizontal.list .item { - align-items: center; - display: flex; - } - .commit-header-row .author { - padding: 3px 0; - } .commit-header h3 { flex-basis: auto !important; margin-bottom: 0.5rem !important; diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css index e757030419..834fdd95d1 100644 --- a/web_src/css/repo/commit-sign.css +++ b/web_src/css/repo/commit-sign.css @@ -1,272 +1,60 @@ - -.repository .ui.attached.isSigned.isWarning { - border-left: 1px solid var(--color-error-border); - border-right: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.top, -.repository .ui.attached.isSigned.isWarning.message { - border-top: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isWarning.message { - box-shadow: none; - background-color: var(--color-error-bg); - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning.message .ui.text { - color: var(--color-error-text); -} - -.repository .ui.attached.isSigned.isWarning:last-child, -.repository .ui.attached.isSigned.isWarning.bottom { - border-bottom: 1px solid var(--color-error-border); -} - -.repository .ui.attached.isSigned.isVerified { - border-left: 1px solid var(--color-success-border); - border-right: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.top, -.repository .ui.attached.isSigned.isVerified.message { - border-top: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerified.message { - box-shadow: none; - background-color: var(--color-success-bg); - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified.message .pull-right { - color: var(--color-text); -} - -.repository .ui.attached.isSigned.isVerified.message .ui.text { - color: var(--color-success-text); -} - -.repository .ui.attached.isSigned.isVerified:last-child, -.repository .ui.attached.isSigned.isVerified.bottom { - border-bottom: 1px solid var(--color-success-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted, -.repository .ui.attached.isSigned.isVerifiedUnmatched { - border-left: 1px solid var(--color-warning-border); - border-right: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.top, -.repository .ui.attached.isSigned.isVerifiedUnmatched.top, -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - border-top: 1px solid var(--color-warning-border); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message { - box-shadow: none; - background-color: var(--color-warning-bg); - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text, -.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text { - color: var(--color-warning-text); -} - -.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child, -.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child, -.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom, -.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom { - border-bottom: 1px solid var(--color-warning-border); -} - -.repository #commits-table td.sha .sha.label, -.repository #repo-files-table .sha.label, -.repository #repo-file-commit-box .sha.label, -.repository #rev-list .sha.label, -.repository .timeline-item.commits-list .singular-commit .sha.label { +.ui.label.commit-id-short, +.ui.label.commit-sign-badge { border: 1px solid var(--color-light-border); + font-size: 13px; + font-weight: var(--font-weight-normal); + padding: 3px 5px; + flex-shrink: 0; } -.repository #commits-table td.sha .sha.label .detail.icon, -.repository #repo-files-table .sha.label .detail.icon, -.repository #repo-file-commit-box .sha.label .detail.icon, -.repository #rev-list .sha.label .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon { - background: var(--color-light); - margin: -6px -10px -4px 0; - padding: 5px 4px 5px 6px; - border-left: 1px solid var(--color-light-border); - border-top: 0; - border-right: 0; - border-bottom: 0; - border-top-left-radius: 0; - border-bottom-left-radius: 0; +.ui.label.commit-id-short { + font-family: var(--fonts-monospace); } -.repository #commits-table td.sha .sha.label .detail.icon .svg, -.repository #repo-files-table .sha.label .detail.icon .svg, -.repository #repo-file-commit-box .sha.label .detail.icon .svg, -.repository #rev-list .sha.label .detail.icon .svg, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg { - margin: 0 0.25em 0 0; -} - -.repository #commits-table td.sha .sha.label .detail.icon > div, -.repository #repo-files-table .sha.label .detail.icon > div, -.repository #repo-file-commit-box .sha.label .detail.icon > div, -.repository #rev-list .sha.label .detail.icon > div, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div { - display: flex; - align-items: center; -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning, -.repository #repo-files-table .sha.label.isSigned.isWarning, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning, -.repository #rev-list .sha.label.isSigned.isWarning, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon, -.repository #rev-list .sha.label.isSigned.isWarning .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon { - border-left: 1px solid var(--color-red-badge); - color: var(--color-red-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover, -.repository #repo-files-table .sha.label.isSigned.isWarning:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover, -.repository #rev-list .sha.label.isSigned.isWarning:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified, -.repository #repo-files-table .sha.label.isSigned.isVerified, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified, -.repository #rev-list .sha.label.isSigned.isVerified, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerified .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon { - border-left: 1px solid var(--color-green-badge); - color: var(--color-green-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover, -.repository #repo-files-table .sha.label.isSigned.isVerified:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover, -.repository #rev-list .sha.label.isSigned.isVerified:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover { - background: var(--color-green-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon { - border-left: 1px solid var(--color-yellow-badge); - color: var(--color-yellow-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover { - background: var(--color-yellow-badge-hover-bg) !important; -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon { - border-left: 1px solid var(--color-orange-badge); - color: var(--color-orange-badge); -} - -.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover, -.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover { - background: var(--color-orange-badge-hover-bg) !important; -} - -.singular-commit .shabox .sha.label { +.ui.label.commit-id-short > .commit-sign-badge { margin: 0; - border: 1px solid var(--color-light-border); + padding: 0; + border: 0 !important; + border-radius: 0; + background: transparent; } -.singular-commit .shabox .sha.label.isSigned.isWarning { - border: 1px solid var(--color-red-badge); - background: var(--color-red-badge-bg); +.ui.label.commit-id-short > .commit-sign-badge:hover { + background: transparent !important; } -.singular-commit .shabox .sha.label.isSigned.isWarning:hover { - background: var(--color-red-badge-hover-bg) !important; +.commit-is-signed.sign-trusted { + border: 1px solid var(--color-green-badge) !important; + background: var(--color-green-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerified { - border: 1px solid var(--color-green-badge); - background: var(--color-green-badge-bg); -} - -.singular-commit .shabox .sha.label.isSigned.isVerified:hover { +.commit-is-signed.sign-trusted:hover { background: var(--color-green-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted { - border: 1px solid var(--color-yellow-badge); - background: var(--color-yellow-badge-bg); +.commit-is-signed.sign-untrusted { + border: 1px solid var(--color-yellow-badge) !important; + background: var(--color-yellow-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover { +.commit-is-signed.sign-untrusted:hover { background: var(--color-yellow-badge-hover-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched { - border: 1px solid var(--color-orange-badge); - background: var(--color-orange-badge-bg); +.commit-is-signed.sign-unmatched { + border: 1px solid var(--color-orange-badge) !important; + background: var(--color-orange-badge-bg) !important; } -.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover { +.commit-is-signed.sign-unmatched:hover { background: var(--color-orange-badge-hover-bg) !important; } + +.commit-is-signed.sign-warning { + border: 1px solid var(--color-red-badge) !important; + background: var(--color-red-badge-bg) !important; +} + +.commit-is-signed.sign-warning:hover { + background: var(--color-red-badge-hover-bg) !important; +} From 14ed553fae6ab7851e8c605e7d15f5114c155180 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 28 Dec 2024 00:30:57 +0000 Subject: [PATCH 07/55] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-PT.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 9dad4698eb..b59a3cd9b6 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1922,8 +1922,8 @@ pulls.close=Encerrar pedido de integração pulls.closed_at=`fechou este pedido de integração %[2]s` pulls.reopened_at=`reabriu este pedido de integração %[2]s` pulls.cmd_instruction_hint=`Ver instruções para a linha de comandos.` -pulls.cmd_instruction_checkout_title=Conferir -pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações. +pulls.cmd_instruction_checkout_title=Checkout +pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações. pulls.cmd_instruction_merge_title=Integrar pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea. pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado @@ -1946,6 +1946,8 @@ pulls.delete.title=Eliminar este pedido de integração? pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo. pulls.recently_pushed_new_branches=Enviou para o ramo %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrás de %[2]s +pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações pulls.upstream_diverging_merge=Sincronizar derivação From 254314be5f355ab5dd5533fde6e9d4f62f1620a4 Mon Sep 17 00:00:00 2001 From: metiftikci Date: Sat, 28 Dec 2024 03:58:19 +0300 Subject: [PATCH 08/55] fix scoped label ui when contains emoji (#33007) ### Before ![old_label](https://github.com/user-attachments/assets/2211f711-613a-4ed4-90fd-8ff6ab0700f5) ### After ![new_label](https://github.com/user-attachments/assets/ecbc89da-7f77-44d0-8ce9-ba51b67421e5) --- web_src/css/repo.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/css/repo.css b/web_src/css/repo.css index dcc4161c59..3ab8acb07f 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1591,7 +1591,7 @@ td .commit-summary { align-items: center; } -.labels-list .label { +.labels-list .label, .scope-parent > .label { padding: 0 6px; min-height: 20px; line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ From e435b1900a00dd98d65aa1f7668fe41e4df83044 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Dec 2024 11:31:46 +0800 Subject: [PATCH 09/55] Refactor arch route handlers (#32993) --- modules/web/{route.go => router.go} | 153 +++--------------- modules/web/router_combo.go | 41 +++++ modules/web/router_path.go | 135 ++++++++++++++++ modules/web/{route_test.go => router_test.go} | 82 +++++++--- routers/api/packages/api.go | 39 +---- 5 files changed, 260 insertions(+), 190 deletions(-) rename modules/web/{route.go => router.go} (66%) create mode 100644 modules/web/router_combo.go create mode 100644 modules/web/router_path.go rename modules/web/{route_test.go => router_test.go} (68%) diff --git a/modules/web/route.go b/modules/web/router.go similarity index 66% rename from modules/web/route.go rename to modules/web/router.go index 729ac46cdc..da06b955b1 100644 --- a/modules/web/route.go +++ b/modules/web/router.go @@ -4,18 +4,14 @@ package web import ( - "fmt" "net/http" "net/url" "reflect" - "regexp" "strings" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" @@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any { // Router defines a route based on chi's router type Router struct { - chiRouter chi.Router + chiRouter *chi.Mux curGroupPrefix string curMiddlewares []any } @@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool { return r.Kind() == reflect.Func && r.IsNil() } -func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { - handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1) - for _, m := range r.curMiddlewares { +func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) { + handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1) + for _, m := range curMiddlewares { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) } } - for _, m := range h { + if len(h) == 0 { + panic("no endpoint handler provided") + } + for i, m := range h { if !isNilOrFuncNil(m) { handlerProviders = append(handlerProviders, toHandlerProvider(m)) + } else if i == len(h)-1 { + panic("endpoint handler can't be nil") } } middlewares := handlerProviders[:len(handlerProviders)-1] @@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha // Methods adds the same handlers for multiple http "methods" (separated by ","). // If any method is invalid, the lower level router will panic. func (r *Router) Methods(methods, pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { methods := strings.Split(methods, ",") @@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) { // Any delegate requests for all methods func (r *Router) Any(pattern string, h ...any) { - middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) + middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc) } @@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) { r.chiRouter.NotFound(h) } -type pathProcessorParam struct { - name string - captureGroup int -} - -type PathProcessor struct { - methods container.Set[string] - re *regexp.Regexp - params []pathProcessorParam -} - func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) { normalized := false normalizedPath := req.URL.EscapedPath() @@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques next.ServeHTTP(resp, req) } -func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool { - if !p.methods.Contains(chiCtx.RouteMethod) { - return false - } - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] - if pathMatches == nil { - return false - } - var paramMatches [][]int - for i := 2; i < len(pathMatches); { - paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) - pmIdx := len(paramMatches) - 1 - end := pathMatches[i+1] - i += 2 - for ; i < len(pathMatches); i += 2 { - if pathMatches[i] >= end { - break - } - paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) - } - } - for i, pm := range paramMatches { - groupIdx := p.params[i].captureGroup * 2 - chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) - } - return true -} - -func NewPathProcessor(methods, pattern string) *PathProcessor { - p := &PathProcessor{methods: make(container.Set[string])} - for _, method := range strings.Split(methods, ",") { - p.methods.Add(strings.TrimSpace(method)) - } - re := []byte{'^'} - lastEnd := 0 - for lastEnd < len(pattern) { - start := strings.IndexByte(pattern[lastEnd:], '<') - if start == -1 { - re = append(re, pattern[lastEnd:]...) - break - } - end := strings.IndexByte(pattern[lastEnd+start:], '>') - if end == -1 { - panic(fmt.Sprintf("invalid pattern: %s", pattern)) - } - re = append(re, pattern[lastEnd:lastEnd+start]...) - partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") - lastEnd += start + end + 1 - - // TODO: it could support to specify a "capture group" for the name, for example: "/" - // it is not used so no need to implement it now - param := pathProcessorParam{} - if partExp == "*" { - re = append(re, "(.*?)/?"...) - if lastEnd < len(pattern) { - if pattern[lastEnd] == '/' { - lastEnd++ - } - } - } else { - partExp = util.IfZero(partExp, "[^/]+") - re = append(re, '(') - re = append(re, partExp...) - re = append(re, ')') - } - param.name = partName - p.params = append(p.params, param) - } - re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) - return p -} - // Combo delegates requests to Combo func (r *Router) Combo(pattern string, h ...any) *Combo { return &Combo{r, pattern, h} } -// Combo represents a tiny group routes with same pattern -type Combo struct { - r *Router - pattern string - h []any -} - -// Get delegates Get method -func (c *Combo) Get(h ...any) *Combo { - c.r.Get(c.pattern, append(c.h, h...)...) - return c -} - -// Post delegates Post method -func (c *Combo) Post(h ...any) *Combo { - c.r.Post(c.pattern, append(c.h, h...)...) - return c -} - -// Delete delegates Delete method -func (c *Combo) Delete(h ...any) *Combo { - c.r.Delete(c.pattern, append(c.h, h...)...) - return c -} - -// Put delegates Put method -func (c *Combo) Put(h ...any) *Combo { - c.r.Put(c.pattern, append(c.h, h...)...) - return c -} - -// Patch delegates Patch method -func (c *Combo) Patch(h ...any) *Combo { - c.r.Patch(c.pattern, append(c.h, h...)...) - return c +// PathGroup creates a group of paths which could be matched by regexp. +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) { + g := &RouterPathGroup{r: r, pathParam: "*"} + fn(g) + r.Any(pattern, append(h, g.ServeHTTP)...) } diff --git a/modules/web/router_combo.go b/modules/web/router_combo.go new file mode 100644 index 0000000000..4478689027 --- /dev/null +++ b/modules/web/router_combo.go @@ -0,0 +1,41 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +// Combo represents a tiny group routes with same pattern +type Combo struct { + r *Router + pattern string + h []any +} + +// Get delegates Get method +func (c *Combo) Get(h ...any) *Combo { + c.r.Get(c.pattern, append(c.h, h...)...) + return c +} + +// Post delegates Post method +func (c *Combo) Post(h ...any) *Combo { + c.r.Post(c.pattern, append(c.h, h...)...) + return c +} + +// Delete delegates Delete method +func (c *Combo) Delete(h ...any) *Combo { + c.r.Delete(c.pattern, append(c.h, h...)...) + return c +} + +// Put delegates Put method +func (c *Combo) Put(h ...any) *Combo { + c.r.Put(c.pattern, append(c.h, h...)...) + return c +} + +// Patch delegates Patch method +func (c *Combo) Patch(h ...any) *Combo { + c.r.Patch(c.pattern, append(c.h, h...)...) + return c +} diff --git a/modules/web/router_path.go b/modules/web/router_path.go new file mode 100644 index 0000000000..39082c0724 --- /dev/null +++ b/modules/web/router_path.go @@ -0,0 +1,135 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package web + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/chi/v5" +) + +type RouterPathGroup struct { + r *Router + pathParam string + matchers []*routerPathMatcher +} + +func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + chiCtx := chi.RouteContext(req.Context()) + path := chiCtx.URLParam(g.pathParam) + for _, m := range g.matchers { + if m.matchPath(chiCtx, path) { + handler := m.handlerFunc + for i := len(m.middlewares) - 1; i >= 0; i-- { + handler = m.middlewares[i](handler).ServeHTTP + } + handler(resp, req) + return + } + } + g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) +} + +// MatchPath matches the request method, and uses regexp to match the path. +// The pattern uses "<...>" to define path parameters, for example: "/" (different from chi router) +// It is only designed to resolve some special cases which chi router can't handle. +// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). +func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) +} + +type routerPathParam struct { + name string + captureGroup int +} + +type routerPathMatcher struct { + methods container.Set[string] + re *regexp.Regexp + params []routerPathParam + middlewares []func(http.Handler) http.Handler + handlerFunc http.HandlerFunc +} + +func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool { + if !p.methods.Contains(chiCtx.RouteMethod) { + return false + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...] + if pathMatches == nil { + return false + } + var paramMatches [][]int + for i := 2; i < len(pathMatches); { + paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]}) + pmIdx := len(paramMatches) - 1 + end := pathMatches[i+1] + i += 2 + for ; i < len(pathMatches); i += 2 { + if pathMatches[i] >= end { + break + } + paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1]) + } + } + for i, pm := range paramMatches { + groupIdx := p.params[i].captureGroup * 2 + chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]]) + } + return true +} + +func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) + p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} + for _, method := range strings.Split(methods, ",") { + p.methods.Add(strings.TrimSpace(method)) + } + re := []byte{'^'} + lastEnd := 0 + for lastEnd < len(pattern) { + start := strings.IndexByte(pattern[lastEnd:], '<') + if start == -1 { + re = append(re, pattern[lastEnd:]...) + break + } + end := strings.IndexByte(pattern[lastEnd+start:], '>') + if end == -1 { + panic(fmt.Sprintf("invalid pattern: %s", pattern)) + } + re = append(re, pattern[lastEnd:lastEnd+start]...) + partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") + lastEnd += start + end + 1 + + // TODO: it could support to specify a "capture group" for the name, for example: "/" + // it is not used so no need to implement it now + param := routerPathParam{} + if partExp == "*" { + re = append(re, "(.*?)/?"...) + if lastEnd < len(pattern) && pattern[lastEnd] == '/' { + lastEnd++ // the "*" pattern is able to handle the last slash, so skip it + } + } else { + partExp = util.IfZero(partExp, "[^/]+") + re = append(re, '(') + re = append(re, partExp...) + re = append(re, ')') + } + param.name = partName + p.params = append(p.params, param) + } + re = append(re, '$') + reStr := string(re) + p.re = regexp.MustCompile(reStr) + return p +} diff --git a/modules/web/route_test.go b/modules/web/router_test.go similarity index 68% rename from modules/web/route_test.go rename to modules/web/router_test.go index ca3134b546..bdcf623b95 100644 --- a/modules/web/route_test.go +++ b/modules/web/router_test.go @@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string { } m[key] = pathParams.Values[i] } - return m + return util.Iif(len(m) == 0, nil, m) } func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := NewPathProcessor("GET", pattern) - assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) + p := newRouterPathMatcher("GET", pattern, http.NotFound) + assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } + + // the "<...>" is intentionally designed to distinguish from chi's path parameters, because: + // 1. their behaviors are totally different, we do not want to mislead developers + // 2. we can write regexp in "" easily and parse it easily testProcess("//", "/a/b", map[string]string{"p1": "a", "p2": "b"}) testProcess("/", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path testProcess("/", "/", map[string]string{"p1": ""}) @@ -67,24 +71,31 @@ func TestRouter(t *testing.T) { } } + stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { + mark := util.OptionalArg(optMark, "") + return func(resp http.ResponseWriter, req *http.Request) { + if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { + h(stop)(resp, req) + resp.WriteHeader(http.StatusOK) + } + } + } + r := NewRouter() + r.NotFound(h("not-found:/")) r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called r.Group("/{username}/{reponame}", func() { r.Get("/{type:issues|pulls}", h("list-issues-b")) r.Group("", func() { r.Get("/{type:issues|pulls}/{index}", h("view-issue")) - }, func(resp http.ResponseWriter, req *http.Request) { - if stop := req.FormValue("stop"); stop != "" { - h(stop)(resp, req) - resp.WriteHeader(http.StatusOK) - } - }) + }, stopMark()) r.Group("/issues/{index}", func() { r.Post("/update", h("update-issue")) }) }) m := NewRouter() + m.NotFound(h("not-found:/api/v1")) r.Mount("/api/v1", m) m.Group("/repos", func() { m.Group("/{username}/{reponame}", func() { @@ -96,11 +107,14 @@ func TestRouter(t *testing.T) { m.Patch("", h()) m.Delete("", h()) }) + m.PathGroup("/*", func(g *RouterPathGroup) { + g.MatchPath("GET", `//`, stopMark("s2"), h("match-path")) + }, stopMark("s1")) }) }) }) - testRoute := func(methodPath string, expected resultStruct) { + testRoute := func(t *testing.T, methodPath string, expected resultStruct) { t.Run(methodPath, func(t *testing.T) { res = resultStruct{} methodPathFields := strings.Fields(methodPath) @@ -111,24 +125,24 @@ func TestRouter(t *testing.T) { }) } - t.Run("Root Router", func(t *testing.T) { - testRoute("GET /the-user/the-repo/other", resultStruct{}) - testRoute("GET /the-user/the-repo/pulls", resultStruct{ + t.Run("RootRouter", func(t *testing.T) { + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, handlerMark: "list-issues-b", }) - testRoute("GET /the-user/the-repo/issues/123", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "view-issue", }) - testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ + testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, handlerMark: "hijack", }) - testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{ + testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, handlerMark: "update-issue", @@ -136,31 +150,57 @@ func TestRouter(t *testing.T) { }) t.Run("Sub Router", func(t *testing.T) { - testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ + testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "POST", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, }) - testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "PATCH", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) - testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ + testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{ method: "DELETE", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"}, }) }) + + t.Run("MatchPath", func(t *testing.T) { + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "match-path", + }) + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMark: "not-found:/api/v1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMark: "s1", + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMark: "s2", + }) + }) } func TestRouteNormalizePath(t *testing.T) { diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index b50fbd638e..8c06836ff8 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -37,8 +37,6 @@ import ( "code.gitea.io/gitea/routers/api/packages/vagrant" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/context" - - "github.com/go-chi/chi/v5" ) func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { @@ -140,39 +138,10 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - - reqPutRepository := web.NewPathProcessor("PUT", "/") - reqGetRepoArchFile := web.NewPathProcessor("HEAD,GET", "///") - reqDeleteRepoNameVerArch := web.NewPathProcessor("DELETE", "////") - - r.Any("*", func(ctx *context.Context) { - chiCtx := chi.RouteContext(ctx.Req.Context()) - path := ctx.PathParam("*") - - if reqPutRepository.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.UploadPackageFile(ctx) - return - } - - if reqGetRepoArchFile.ProcessRequestPath(chiCtx, path) { - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if reqDeleteRepoNameVerArch.ProcessRequestPath(chiCtx, path) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - arch.DeletePackageVersion(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("*", func(g *web.RouterPathGroup) { + g.MatchPath("PUT", "/", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "///", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "////", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { From e69da2cd07da8637d8fa5802b78882598a545299 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 27 Dec 2024 20:04:07 -0800 Subject: [PATCH 10/55] Fix bug on activities (#33008) A repository with no issue will display a random number on activities page. This is caused by wrong usage of `And` and `Or`. ![9cdbbf81d50aa5d9bd16604e0dab5eb0](https://github.com/user-attachments/assets/828cebdc-bd35-4716-a58c-c1b43ddf8bf0) --- models/activities/repo_activity.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3ffad035b7..3ccdbd47d3 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "xorm.io/builder" "xorm.io/xorm" ) @@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) * func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). And("issue.is_pull = ?", false). - And("issue.created_unix >= ?", fromTime.Unix()). - Or("issue.closed_unix >= ?", fromTime.Unix()) + And(builder.Or( + builder.Gte{"issue.created_unix": fromTime.Unix()}, + builder.Gte{"issue.closed_unix": fromTime.Unix()}, + )) return sess } From 3d3ece36d2b9d09ddc5b277a4f3a9f0639adc0e1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Dec 2024 19:26:16 +0800 Subject: [PATCH 11/55] Refactor comment history and fix content edit (#33018) And fix a regression bug for comment content editing. Now 11 "import jquery" files left --- templates/repo/diff/box.tmpl | 4 +- web_src/js/features/repo-diff.ts | 2 + web_src/js/features/repo-issue-content.ts | 69 ++++++++++++----------- web_src/js/features/repo-issue-edit.ts | 3 + 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 0dfb1fd5a1..9733e5f980 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -235,7 +235,7 @@ {{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}} {{end}} {{if (not .DiffNotAvailable)}} diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 58e0d88092..2b405abb9b 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -38,6 +38,8 @@ function initRepoDiffFileViewToggle() { } function initRepoDiffConversationForm() { + // FIXME: there could be various different form in a conversation-holder (for example: reply form, edit form). + // This listener is for "reply form" only, it should clearly distinguish different forms in the future. addDelegatedEventListener(document, 'submit', '.conversation-holder form', async (form, e) => { e.preventDefault(); const textArea = form.querySelector('textarea'); diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts index 88672cc255..2279c26beb 100644 --- a/web_src/js/features/repo-issue-content.ts +++ b/web_src/js/features/repo-issue-content.ts @@ -1,20 +1,17 @@ -import $ from 'jquery'; import {svg} from '../svg.ts'; import {showErrorToast} from '../modules/toast.ts'; import {GET, POST} from '../modules/fetch.ts'; -import {showElem} from '../utils/dom.ts'; +import {createElementFromHTML, showElem} from '../utils/dom.ts'; import {parseIssuePageInfo} from '../utils.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; -let i18nTextEdited; -let i18nTextOptions; -let i18nTextDeleteFromHistory; -let i18nTextDeleteFromHistoryConfirm; +let i18nTextEdited: string; +let i18nTextOptions: string; +let i18nTextDeleteFromHistory: string; +let i18nTextDeleteFromHistoryConfirm: string; -function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleHtml) { - let $dialog = $('.content-history-detail-dialog'); - if ($dialog.length) return; - - $dialog = $(` +function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) { + const elDetailDialog = createElementFromHTML(` `); - $dialog.appendTo($('body')); - $dialog.find('.dialog-header-options').dropdown({ + document.body.append(elDetailDialog); + const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options'); + const $fomanticDialog = fomanticQuery(elDetailDialog); + const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown); + $fomanticDropdownOptions.dropdown({ showOnFocus: false, allowReselection: true, async onChange(_value, _text, $item) { @@ -46,7 +46,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH const resp = await response.json(); if (resp.ok) { - $dialog.modal('hide'); + $fomanticDialog.modal('hide'); } else { showErrorToast(resp.message); } @@ -60,10 +60,10 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH } }, onHide() { - $(this).dropdown('clear', true); + $fomanticDropdownOptions.dropdown('clear', true); }, }); - $dialog.modal({ + $fomanticDialog.modal({ async onShow() { try { const params = new URLSearchParams(); @@ -74,25 +74,25 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH const response = await GET(url); const resp = await response.json(); - const commentDiffData = $dialog.find('.comment-diff-data')[0]; - commentDiffData?.classList.remove('is-loading'); + const commentDiffData = elDetailDialog.querySelector('.comment-diff-data'); + commentDiffData.classList.remove('is-loading'); commentDiffData.innerHTML = resp.diffHtml; // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden. if (resp.canSoftDelete) { - showElem($dialog.find('.dialog-header-options')); + showElem(elOptionsDropdown); } } catch (error) { console.error('Error:', error); } }, onHidden() { - $dialog.remove(); + $fomanticDialog.remove(); }, }).modal('show'); } -function showContentHistoryMenu(issueBaseUrl, $item, commentId) { - const $headerLeft = $item.find('.comment-header-left'); +function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, commentId: string) { + const elHeaderLeft = elCommentItem.querySelector('.comment-header-left'); const menuHtml = `
`; - $headerLeft.find(`.content-history-menu`).remove(); - $headerLeft.append($(menuHtml)); - $headerLeft.find('.dropdown').dropdown({ + elHeaderLeft.querySelector(`.ui.dropdown.content-history-menu`)?.remove(); // remove the old one if exists + elHeaderLeft.append(createElementFromHTML(menuHtml)); + + const elDropdown = elHeaderLeft.querySelector('.ui.dropdown.content-history-menu'); + const $fomanticDropdown = fomanticQuery(elDropdown); + $fomanticDropdown.dropdown({ action: 'hide', apiSettings: { cache: false, @@ -110,7 +113,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) { }, saveRemoteData: false, onHide() { - $(this).dropdown('change values', null); + $fomanticDropdown.dropdown('change values', null); }, onChange(value, itemHtml, $item) { if (value && !$item.find('[data-history-is-deleted=1]').length) { @@ -124,9 +127,9 @@ export async function initRepoIssueContentHistory() { const issuePageInfo = parseIssuePageInfo(); if (!issuePageInfo.issueNumber) return; - const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content - const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments - if (!$itemIssue.length && !$comments.length) return; + const elIssueDescription = document.querySelector('.repository.issue .timeline-item.comment.first'); // issue(PR) main content + const elComments = document.querySelectorAll('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments + if (!elIssueDescription && !elComments.length) return; const issueBaseUrl = `${issuePageInfo.repoLink}/issues/${issuePageInfo.issueNumber}`; @@ -139,13 +142,13 @@ export async function initRepoIssueContentHistory() { i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm; i18nTextOptions = resp.i18n.textOptions; - if (resp.editedHistoryCountMap[0] && $itemIssue.length) { - showContentHistoryMenu(issueBaseUrl, $itemIssue, '0'); + if (resp.editedHistoryCountMap[0] && elIssueDescription) { + showContentHistoryMenu(issueBaseUrl, elIssueDescription, '0'); } for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) { if (commentId === '0') continue; - const $itemComment = $(`#issuecomment-${commentId}`); - showContentHistoryMenu(issueBaseUrl, $itemComment, commentId); + const elIssueComment = document.querySelector(`#issuecomment-${commentId}`); + if (elIssueComment) showContentHistoryMenu(issueBaseUrl, elIssueComment, commentId); } } catch (error) { console.error('Error:', error); diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index cf4c223e03..38dfea4743 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -30,6 +30,9 @@ async function tryOnEditContent(e) { const saveAndRefresh = async (e) => { e.preventDefault(); + // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers" + // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler) + e.stopPropagation(); renderContent.classList.add('is-loading'); showElem(renderContent); hideElem(editContentZone); From a92f5057ae962c0a503b6f66c2615b8cbde9a76a Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Dec 2024 19:51:38 +0800 Subject: [PATCH 12/55] Fix and/or comment some legacy CSS problems (#33015) --- templates/repo/branch/list.tmpl | 6 ++++-- templates/repo/settings/options.tmpl | 4 ++-- web_src/css/base.css | 3 ++- web_src/css/explore.css | 20 +------------------- web_src/css/index.css | 3 ++- web_src/css/modules/navbar.css | 4 ++-- 6 files changed, 13 insertions(+), 27 deletions(-) diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 5484024ff8..cb504e2b75 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -29,7 +29,8 @@

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

- + {{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} + {{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
- {{if or .TotalTrackedTime .Assignees .NumComments}} -
- {{if .TotalTrackedTime}} -
- {{svg "octicon-clock" 16}} - {{.TotalTrackedTime | Sec2Time}} -
- {{end}} - {{if .Assignees}} -
- {{range .Assignees}} - - {{ctx.AvatarUtils.Avatar . 20}} - - {{end}} -
- {{end}} - {{if .NumComments}} - - {{end}} + {{if .TotalTrackedTime}} +
+ {{svg "octicon-clock" 16}} + {{.TotalTrackedTime | Sec2Time}}
{{end}}
@@ -152,6 +132,26 @@ {{end}}
+ {{if or .Assignees .NumComments}} +
+ {{if .Assignees}} +
+ {{range .Assignees}} + + {{ctx.AvatarUtils.Avatar . 20}} + + {{end}} +
+ {{end}} + {{if .NumComments}} + + {{end}} +
+ {{end}}
{{end}} {{if .IssueIndexerUnavailable}} From e95b946f6d763afdb2b68e1f5d7c8b3e518d1986 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 29 Dec 2024 00:36:47 +0000 Subject: [PATCH 15/55] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 222 +++++++++++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 8139970bd7..5f2f16bfdf 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -93,6 +93,7 @@ remove_all=Alle entfernen remove_label_str=Element "%s " entfernen edit=Bearbeiten view=Anzeigen +test=Test enabled=Aktiviert disabled=Deaktiviert @@ -103,6 +104,7 @@ copy_url=URL kopieren copy_hash=Hash kopieren copy_content=Inhalt kopieren copy_branch=Branchnamen kopieren +copy_path=Pfad kopieren copy_success=Kopiert! copy_error=Kopieren fehlgeschlagen copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden @@ -143,6 +145,7 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert +readme=Readme filter=Filter filter.clear=Filter leeren @@ -158,12 +161,15 @@ filter.public=Öffentlich filter.private=Privat no_results_found=Es wurden keine Ergebnisse gefunden. +internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s [search] search=Suche ... type_tooltip=Suchmodus fuzzy=Ähnlich fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind +exact=Exakt +exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen repo_kind=Repositories durchsuchen ... user_kind=Benutzer durchsuchen ... org_kind=Organisationen durchsuchen ... @@ -174,9 +180,13 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei package_kind=Pakete durchsuchen ... project_kind=Projekte durchsuchen ... branch_kind=Branches durchsuchen ... +tag_kind=Tags durchsuchen... +tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen. commit_kind=Commits durchsuchen ... runner_kind=Runner durchsuchen ... no_results=Es wurden keine passenden Ergebnisse gefunden. +issue_kind=Issues durchsuchen ... +pull_kind=Pull-Requests durchsuchen... keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator. [aria] @@ -201,7 +211,10 @@ buttons.link.tooltip=Link hinzufügen buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen +buttons.table.add.tooltip=Tabelle hinzufügen buttons.table.add.insert=Hinzufügen +buttons.table.rows=Zeilen +buttons.table.cols=Spalten buttons.mention.tooltip=Benutzer oder Team erwähnen buttons.ref.tooltip=Issue oder Pull-Request referenzieren buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden @@ -214,16 +227,20 @@ string.desc=Z–A [error] occurred=Ein Fehler ist aufgetreten +report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf GitHub nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport. not_found=Das Ziel konnte nicht gefunden werden. network_error=Netzwerkfehler [startpage] app_desc=Ein einfacher, selbst gehosteter Git-Service install=Einfach zu installieren +install_desc=Starte einfach die Anwendung für deine Plattform oder nutze Docker. Es existieren auch paketierte Versionen. platform=Plattformübergreifend +platform_desc=Gitea läuft überall, wo Go kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt! lightweight=Leichtgewicht lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden! license=Quelloffen +license_desc=Hol dir den Code unter %[2]s! Leiste deinen Beitrag bei der Verbesserung dieses Projekts. Trau dich! [install] install=Installation @@ -337,6 +354,7 @@ enable_update_checker=Aktualisierungsprüfung aktivieren enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen. env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: +config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben [home] nav_menu=Navigationsmenü @@ -377,6 +395,8 @@ relevant_repositories=Es werden nur relevante Repositories angezeigt, Liste gestohlener Passwörter, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern. password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben. +signin_passkey=Mit einem Passkey anmelden +back_to_sign_in=Zurück zum Anmelden [mail] view_it_on=Auf %s ansehen @@ -459,6 +486,7 @@ activate_email=Bestätige deine E-Mail-Adresse activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse activate_email.text=Bitte klicke innerhalb von %s auf folgenden Link, um dein Konto zu aktivieren: +register_notify=Willkommen bei %s register_notify.title=%[1]s, willkommen bei %[2]s register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s! register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden. @@ -560,6 +588,8 @@ lang_select_error=Wähle eine Sprache aus der Liste aus. username_been_taken=Der Benutzername ist bereits vergeben. username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern. +change_username_disabled=Ändern des Benutzernamens ist deaktiviert. +change_full_name_disabled=Ändern des vollständigen Namens ist deaktiviert. username_has_not_been_changed=Benutzername wurde nicht geändert repo_name_been_taken=Der Repository-Name wird schon verwendet. repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden. @@ -609,6 +639,7 @@ org_still_own_repo=Diese Organisation besitzt noch ein oder mehrere Repositories org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst. target_branch_not_exist=Der Ziel-Branch existiert nicht. +target_ref_not_exist=Zielreferenz existiert nicht %s admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte. @@ -685,6 +716,8 @@ public_profile=Öffentliches Profil biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden) location_placeholder=Teile Deinen ungefähren Standort mit anderen profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet. +password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. +password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. full_name=Vollständiger Name website=Webseite location=Standort @@ -702,6 +735,7 @@ cancel=Abbrechen language=Sprache ui=Theme hidden_comment_types=Ausgeblendeter Kommentartypen +hidden_comment_types_description=Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von "Label" alle "{user} hat {label} hinzugefügt/entfernt"-Kommentare. hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert comment_type_group_reference=Verweis auf Mitglieder @@ -733,6 +767,7 @@ uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB). update_avatar_success=Dein Profilbild wurde geändert. update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert. +cropper_prompt=Sie können das Bild vor dem Speichern bearbeiten. Das bearbeitete Bild wird als PNG-Datei gespeichert. change_password=Passwort aktualisieren old_password=Aktuelles Passwort @@ -748,6 +783,8 @@ manage_themes=Standard-Theme auswählen manage_openid=OpenID-Adressen verwalten email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. theme_desc=Dies wird dein Standard-Theme auf der Seite sein. +theme_colorblindness_help=Hilfe zum Theme für Farbenblinde +theme_colorblindness_prompt=Gitea erhält aktuell einfache Unterstützung für Farbenblinde durch einige Themes, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Theme-Dateien vorgenommen werden. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -872,8 +909,9 @@ repo_and_org_access=Repository- und Organisationszugriff permissions_public_only=Nur öffentlich permissions_access_all=Alle (öffentlich, privat und begrenzt) select_permissions=Berechtigungen auswählen +permission_not_set=Nicht festgelegt permission_no_access=Kein Zugriff -permission_read=Gelesen +permission_read=Lesen permission_write=Lesen und Schreiben access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden API-Routen. Lies die Dokumentation für mehr Informationen. at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen @@ -891,6 +929,7 @@ create_oauth2_application_success=Du hast erfolgreich eine neue OAuth2-Anwendung update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert. oauth2_application_name=Name der Anwendung oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte. +oauth2_skip_secondary_authorization=Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. Dies kann ein Sicherheitsrisiko darstellen. oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI. save_application=Speichern oauth2_client_id=Client-ID @@ -901,6 +940,7 @@ oauth2_client_secret_hint=Das Secret wird nach dem Verlassen oder Aktualisieren oauth2_application_edit=Bearbeiten oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz. oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren? +oauth2_application_locked=Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation. authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt. @@ -909,20 +949,26 @@ revoke_oauth2_grant=Autorisierung widerrufen revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher? revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. +twofa_desc=Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter ("TOTP") zu erhalten. twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung eingeschaltet. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren +twofa_scratch_token_regenerate=Einweg-Wiederherstellungsschlüssel neu generieren +twofa_scratch_token_regenerated=Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt. twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren. twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren? +regenerate_scratch_token_desc=Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen. twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert. scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App: or_enter_secret=Oder gib das Secret ein: %s then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein: passcode_invalid=Die PIN ist falsch. Probiere es erneut. +twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen Einweg-Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird. twofa_failed_get_secret=Fehler beim Abrufen des Secrets. +webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "WebAuthn" unterstützen. webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen @@ -988,6 +1034,8 @@ fork_to_different_account=Fork in ein anderes Konto erstellen fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden. fork_branch=Branch, der zum Fork geklont werden soll all_branches=Alle Branches +view_all_branches=Alle Branches anzeigen +view_all_tags=Alle Tags anzeigen fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind. fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest. use_template=Dieses Template verwenden @@ -999,6 +1047,8 @@ generate_repo=Repository erstellen generate_from=Erstelle aus repo_desc=Beschreibung repo_desc_helper=Gib eine kurze Beschreibung an (optional) +repo_no_desc=Keine Beschreibung vorhanden +repo_lang=Sprachen repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus. repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore. issue_labels=Issue Label @@ -1006,6 +1056,7 @@ issue_labels_helper=Wähle ein Issue-Label-Set. license=Lizenz license_helper=Wähle eine Lizenz aus. license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe eine Lizenz wählen. +multiple_licenses=Mehrere Lizenzen object_format=Objektformat object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel. readme=README @@ -1059,13 +1110,16 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht blame_prior=Blame vor dieser Änderung anzeigen blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden ignoriert. Klicke hier, um das zu umgehen und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. +user_search_tooltip=Zeigt maximal 30 Benutzer tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s transfer.accept=Übertragung Akzeptieren +transfer.accept_desc=`Übertragung nach "%s"` transfer.reject=Übertragung Ablehnen +transfer.reject_desc=Übertragung nach "%s " abbrechen transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen. transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen. @@ -1140,6 +1194,11 @@ migrate.gogs.description=Daten von notabug.org oder anderen Gogs Instanzen migri migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren. migrate.codebase.description=Daten von codebasehq.com migrieren. migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren. +migrate.codecommit.description=Daten von AWS CodeCommit migrieren. +migrate.codecommit.aws_access_key_id=AWS Access Key ID +migrate.codecommit.aws_secret_access_key=AWS Secret Access Key +migrate.codecommit.https_git_credentials_username=HTTPS-Git-Nutzername +migrate.codecommit.https_git_credentials_password=HTTPS-Git-Passwort migrate.migrating_git=Git-Daten werden migriert migrate.migrating_topics=Themen werden migriert migrate.migrating_milestones=Meilensteine werden migriert @@ -1200,6 +1259,7 @@ releases=Releases tag=Tag released_this=hat released tagged_this=hat getaggt +file.title=%s in %s file_raw=Originalformat file_history=Verlauf file_view_source=Quelltext anzeigen @@ -1207,12 +1267,16 @@ file_view_rendered=Ansicht rendern file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. +file_is_empty=Die Datei ist leer. +code_preview_line_from_to=Zeilen %[1]d bis %[2]d in %[3]s +code_preview_line_in=Zeile %[1]d in %[2]s invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen` invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen` ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.` invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen` ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen` +ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden` escape_control_characters=Escapen unescape_control_characters=Unescapen @@ -1260,6 +1324,7 @@ editor.or=oder editor.cancel_lower=Abbrechen editor.commit_signed_changes=Committe signierte Änderungen editor.commit_changes=Änderungen committen +editor.add_tmpl='{filename}' hinzufügen editor.add=%s hinzugefügt editor.update=%s aktualisiert editor.delete=%s gelöscht @@ -1343,6 +1408,7 @@ commitstatus.success=Erfolg ext_issues=Zugriff auf Externe Issues ext_issues.desc=Link zu externem Issuetracker. +projects.desc=Verwalte Issues und Pull-Requests in Projekten. projects.description=Beschreibung (optional) projects.description_placeholder=Beschreibung projects.create=Projekt erstellen @@ -1402,7 +1468,9 @@ issues.new.clear_milestone=Meilenstein entfernen issues.new.assignees=Zuständig issues.new.clear_assignees=Zuständige entfernen issues.new.no_assignees=Niemand zuständig +issues.new.no_reviewers=Keine Reviewer issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. +issues.edit.already_changed=Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.choose.get_started=Los geht's issues.choose.open_external_link=Öffnen @@ -1429,6 +1497,7 @@ issues.remove_labels=hat die Labels %s %s entfernt issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt issues.add_milestone_at=`hat diesen Issue %[2]s zum %[1]s Meilenstein hinzugefügt` issues.add_project_at=`hat dieses zum %s projekt %s hinzugefügt` +issues.move_to_column_of_project=`hat dies zu %s in %s %s verschoben` issues.change_milestone_at=`hat den Meilenstein %[3]s von %[1]s zu %[2]s geändert` issues.change_project_at=`hat das Projekt %[3]s von %[1]s zu %[2]s geändert` issues.remove_milestone_at=`hat dieses Issue %[2]s vom %[1]s Meilenstein entfernt` @@ -1460,6 +1529,8 @@ issues.filter_assignee=Zuständig issues.filter_assginee_no_select=Alle Zuständigen issues.filter_assginee_no_assignee=Niemand zuständig issues.filter_poster=Autor +issues.filter_user_placeholder=Benutzer suchen +issues.filter_user_no_select=Alle Benutzer issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen @@ -1513,7 +1584,9 @@ issues.no_content=Keine Beschreibung angegeben. issues.close=Issue schließen issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged +issues.close_comment_issue=Kommentieren und schließen issues.reopen_issue=Wieder öffnen +issues.reopen_comment_issue=Kommentieren und wieder öffnen issues.create_comment=Kommentieren issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.closed_at=`hat diesen Issue %[2]s geschlossen` @@ -1602,12 +1675,25 @@ issues.delete.title=Dieses Issue löschen? issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst) issues.tracker=Zeiterfassung +issues.timetracker_timer_start=Timer starten +issues.timetracker_timer_stop=Timer stoppen +issues.timetracker_timer_discard=Timer verwerfen +issues.timetracker_timer_manually_add=Zeit hinzufügen +issues.time_estimate_set=Geschätzte Zeit festlegen +issues.time_estimate_display=Schätzung: %s +issues.change_time_estimate_at=Zeitschätzung geändert zu %s %s +issues.remove_time_estimate_at=Zeitschätzung %s entfernt +issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig +issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in diesem Issue gestartet!` +issues.stop_tracking_history=hat für %s gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen +issues.add_time_history=hat %s gearbeitete Zeit hinzugefügt %s issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` +issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden issues.add_time_minutes=Minuten issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben. @@ -1667,6 +1753,7 @@ issues.dependency.add_error_dep_not_same_repo=Beide Issues müssen sich im selbe issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen. issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen. issues.review.approve=hat die Änderungen %s genehmigt +issues.review.comment=hat %s überprüft issues.review.dismissed=verwarf %ss Review %s issues.review.dismissed_label=Verworfen issues.review.left_comment=hat einen Kommentar hinterlassen @@ -1692,6 +1779,11 @@ issues.review.resolve_conversation=Diskussion als "erledigt" markieren issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren issues.review.resolved_by=markierte diese Unterhaltung als gelöst issues.review.commented=Kommentieren +issues.review.official=Genehmigt +issues.review.requested=Prüfung ausstehend +issues.review.rejected=Änderungen angefordert +issues.review.stale=Aktualisiert seit der Genehmigung +issues.review.unofficial=Ungezählte Genehmigung issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden. issues.reference_issue.body=Beschreibung issues.content_history.deleted=gelöscht @@ -1708,6 +1800,8 @@ compare.compare_head=vergleichen pulls.desc=Pull-Requests und Code-Reviews aktivieren. pulls.new=Neuer Pull-Request pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. +pulls.new.must_collaborator=Du musst Mitarbeiter sein, um Pull-Requests zu erstellen. +pulls.edit.already_changed=Änderungen zum Pull-Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisieren die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden pulls.view=Pull-Request ansehen pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben @@ -1763,6 +1857,8 @@ pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich. pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen. pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen. +pulls.blocked_by_approvals=Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt. +pulls.blocked_by_approvals_whitelisted=Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste. pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden. pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen. pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist. @@ -1804,7 +1900,9 @@ pulls.unrelated_histories=Merge fehlgeschlagen: Der Head des Merges und die Basi pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut. pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut. pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern. +pulls.push_rejected=Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository. pulls.push_rejected_summary=Vollständige Ablehnungsmeldung +pulls.push_rejected_no_message=Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.` pulls.status_checking=Einige Prüfungen sind noch ausstehend pulls.status_checks_success=Alle Prüfungen waren erfolgreich @@ -1828,6 +1926,7 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen. pulls.cmd_instruction_merge_title=Mergen pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren. +pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten. @@ -1847,9 +1946,15 @@ pulls.delete.title=Diesen Pull-Request löschen? pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren) pulls.recently_pushed_new_branches=Du hast auf den Branch %[1]s %[2]s gepusht +pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %[2]s +pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s +pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen +pulls.upstream_diverging_merge=Fork synchronisieren pull.deleted_branch=(gelöscht):%s +pull.agit_documentation=Dokumentation zu AGit durchschauen +comments.edit.already_changed=Änderungen zum Kommentar konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden milestones.new=Neuer Meilenstein milestones.closed=Geschlossen %s @@ -1858,6 +1963,7 @@ milestones.no_due_date=Kein Fälligkeitsdatum milestones.open=Öffnen milestones.close=Schließen milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen. +milestones.completeness=%d%% abgeschlossen milestones.create=Meilenstein erstellen milestones.title=Titel milestones.desc=Beschreibung @@ -2042,12 +2148,14 @@ settings.push_mirror_sync_in_progress=Aktuell werden Änderungen auf %s gepusht. settings.site=Webseite settings.update_settings=Einstellungen speichern settings.update_mirror_settings=Mirror-Einstellungen aktualisieren +settings.branches.switch_default_branch=Standardbranch wechseln settings.branches.update_default_branch=Standardbranch aktualisieren settings.branches.add_new_rule=Neue Regel hinzufügen settings.advanced_settings=Erweiterte Einstellungen settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch +settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer: settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen. settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki-URL @@ -2078,6 +2186,7 @@ settings.pulls.default_delete_branch_after_merge=Standardmäßig bei Pull-Reques settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository Packages Registry aktivieren +settings.projects_desc=Projekte aktivieren settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen) settings.projects_mode_repo=Nur Repo-Projekte settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte @@ -2117,6 +2226,7 @@ settings.transfer_in_progress=Es gibt derzeit eine laufende Übertragung. Bitte settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert). +settings.transfer_notices_4=- Wenn das Repository einer Organisation gehört und du es an eine andere Organisation oder eine andere Person überträgst, verlierst du die Verlinkungen zwischen den Issues des Repositorys und dem Projektboard der Organisation. settings.transfer_owner=Neuer Besitzer settings.transfer_perform=Übertragung durchführen settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"` @@ -2216,6 +2326,7 @@ settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelösc settings.event_release=Release settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht. settings.event_push=Push +settings.event_force_push=Force Push settings.event_push_desc=Git push in ein Repository. settings.event_repository=Repository settings.event_repository_desc=Repository erstellt oder gelöscht. @@ -2252,6 +2363,7 @@ settings.event_pull_request_merge=Pull-Request-Merge settings.event_package=Paket settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht. settings.branch_filter=Branch-Filter +settings.branch_filter_desc=Whitelist für Branches für Push-, Erzeugungs- und Löschevents, als Glob-Pattern beschrieben. Es werden Events für alle Branches gemeldet, falls das Pattern * ist, oder falls es leer ist. Siehe die %[2]s Dokumentation für die Syntax (Englisch). Beispiele: master, {master,release*}. settings.authorization_header=Authorization-Header settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s. settings.active=Aktiv @@ -2300,22 +2412,50 @@ settings.branches=Branches settings.protected_branch=Branch-Schutz settings.protected_branch.save_rule=Regel speichern settings.protected_branch.delete_rule=Regel löschen +settings.protected_branch_can_push=Push erlauben? +settings.protected_branch_can_push_yes=Du kannst pushen +settings.protected_branch_can_push_no=Du kannst nicht pushen +settings.branch_protection=Branch-Schutz für Branch '%s' settings.protect_this_branch=Branch-Schutz aktivieren settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein. settings.protect_disable_push=Push deaktivieren settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben. +settings.protect_disable_force_push=Force-Push deaktivieren +settings.protect_disable_force_push_desc=Force-Push auf diesen Branch nicht erlauben. settings.protect_enable_push=Push aktivieren settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push). +settings.protect_enable_force_push_all=Force-Push aktivieren +settings.protect_enable_force_push_all_desc=Jeder mit Push-Zugriff wird in diesen Branch force-pushen können. +settings.protect_enable_force_push_allowlist=Force-Push beschränkt auf Genehmigungsliste +settings.protect_enable_force_push_allowlist_desc=Nur Benutzer oder Teams auf der Genehmigungsliste mit Push-Zugriff werden in diesen Branch force-pushen können. settings.protect_enable_merge=Merge aktivieren settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen. +settings.protect_whitelist_committers=Genehmigungsliste für eingeschränkten Push +settings.protect_whitelist_committers_desc=Jeder, der auf der Genehmigungsliste steht, darf in diesen Branch pushen (aber kein Force-Push). +settings.protect_whitelist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Pushen. +settings.protect_whitelist_users=Nutzer, die pushen dürfen: +settings.protect_whitelist_teams=Teams, die pushen dürfen: +settings.protect_force_push_allowlist_users=Erlaubte Benutzer für Force-Push: +settings.protect_force_push_allowlist_teams=Erlaubte Teams für Force-Push: +settings.protect_force_push_allowlist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Force-Push. +settings.protect_merge_whitelist_committers=Merge-Genehmigungsliste aktivieren +settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen. +settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: +settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.protect_check_status_contexts=Statusprüfungen aktivieren settings.protect_status_check_patterns=Statuscheck-Muster: settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein. +settings.protect_check_status_contexts_desc=Vor dem Mergen müssen Statusprüfungen bestanden werden. Wähle aus, welche Statusprüfungen erfolgreich durchgeführt werden müssen, bevor Branches in einen anderen gemergt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Commits zuerst auf einen anderen Branch gepusht werden, dann nach bestandener Statusprüfung gemergt oder direkt auf einen Branch gepusht werden, der dieser Regel entspricht. Wenn kein Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein. settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden settings.protect_status_check_matched=Übereinstimmung settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s". settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster. settings.protect_required_approvals=Erforderliche Zustimmungen: +settings.protect_required_approvals_desc=Erlaube das Mergen des Pull-Requests nur mit genügend Genehmigungen. +settings.protect_approvals_whitelist_enabled=Genehmigungen auf Benutzer oder Teams auf der Genehmigungsliste beschränken +settings.protect_approvals_whitelist_enabled_desc=Nur Bewertungen von Benutzern auf der Genehmigungsliste oder Teams zählen zu den erforderlichen Genehmigungen. Gibt es keine Genehmigungsliste, so zählen Reviews von jedem mit Schreibzugriff zu den erforderlichen Genehmigungen. +settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer: +settings.protect_approvals_whitelist_teams=Freigeschaltete Teams: settings.dismiss_stale_approvals=Entferne alte Genehmigungen settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt. settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren @@ -2323,12 +2463,18 @@ settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits ert settings.require_signed_commits=Signierte Commits erforderlich settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind. settings.protect_branch_name_pattern=Muster für geschützte Branchnamen +settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe die Dokumentation für die Pattern-Syntax. Beispiele: main, release/** settings.protect_patterns=Muster settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt): +settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Pattern-Syntax. Beispiele: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt): +settings.protect_unprotected_file_patterns_desc=Ungeschützte Dateien, die direkt geändert werden dürfen, wenn der Benutzer Schreibzugriff hat, können die Push-Beschränkung umgehen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe %[2]s Dokumentation zur Mustersyntax. Beispiele: .drone.yml, /docs/**/*.txt. +settings.add_protected_branch=Schutz aktivieren +settings.delete_protected_branch=Schutz deaktivieren settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert. settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert. settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen. +settings.protected_branch_deletion=Branch-Schutz deaktivieren settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt. @@ -2336,8 +2482,11 @@ settings.block_on_official_review_requests=Mergen bei offiziellen Review-Anfrage settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt. settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist. +settings.block_admin_merge_override=Administratoren müssen die Schutzregeln für Branches befolgen +settings.block_admin_merge_override_desc=Administratoren müssen die Schutzregeln für Branches befolgen und können sie nicht umgehen. settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: settings.merge_style_desc=Merge-Styles +settings.default_merge_style_desc=Standard-Mergeverhalten für Pull-Requests settings.choose_branch=Branch wählen… settings.no_protected_branch=Es gibt keine geschützten Branches. settings.edit_protected_branch=Bearbeiten @@ -2353,12 +2502,25 @@ settings.tags.protection.allowed.teams=Erlaubte Teams settings.tags.protection.allowed.noone=Niemand settings.tags.protection.create=Tag schützen settings.tags.protection.none=Es gibt keine geschützten Tags. +settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im Guide für geschützte Tags (Englisch). settings.bot_token=Bot-Token settings.chat_id=Chat-ID settings.thread_id=Thread-ID settings.matrix.homeserver_url=Homeserver-URL settings.matrix.room_id=Raum-ID settings.matrix.message_type=Nachrichtentyp +settings.visibility.private.button=Auf privat setzen +settings.visibility.private.text=Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen. +settings.visibility.private.bullet_title=Das Ändern der Sichtbarkeit auf privat wird: +settings.visibility.private.bullet_one=Das Repository nur für zugelassene Mitglieder sichtbar machen. +settings.visibility.private.bullet_two=Kann die Beziehung zwischen ihm und Forks, Beobachternund Sternen entfernen. +settings.visibility.public.button=Auf öffentlich setzen +settings.visibility.public.text=Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar. +settings.visibility.public.bullet_title=Das Ändern der Sichtbarkeit auf öffentlich wird: +settings.visibility.public.bullet_one=Das Repository für jeden sichtbar machen. +settings.visibility.success=Die Sichtbarkeit des Repositorys wurde geändert. +settings.visibility.error=Beim Versuch, die Sichtbarkeit des Repositorys zu ändern, ist ein Fehler aufgetreten. +settings.visibility.fork_error=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar. settings.archive.button=Repo archivieren settings.archive.header=Dieses Repo archivieren settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. @@ -2470,6 +2632,7 @@ release.new_release=Neues Release release.draft=Entwurf release.prerelease=Pre-Release release.stable=Stabil +release.latest=Aktuell release.compare=Vergleichen release.edit=bearbeiten release.ahead.commits=%d Commits @@ -2554,6 +2717,7 @@ tag.create_success=Tag "%s" wurde erstellt. topic.manage_topics=Themen verwalten topic.done=Fertig +topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig. find_file.go_to_file=Datei suchen @@ -2651,6 +2815,7 @@ teams.leave.detail=%s verlassen? teams.can_create_org_repo=Repositories erstellen teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository. teams.none_access=Kein Zugriff +teams.none_access_helper=Mitglieder können keine anderen Aktionen für diese Einheit anzeigen oder durchführen. Dies hat keine Wirkung auf öffentliche Repositories. teams.general_access=Allgemeiner Zugriff teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt. teams.read_access=Lesen @@ -2697,6 +2862,7 @@ teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. [admin] +maintenance=Wartung dashboard=Dashboard self_check=Selbstprüfung identity_access=Identität & Zugriff @@ -2718,7 +2884,9 @@ last_page=Letzte total=Gesamt: %d settings=Administratoreinstellungen +dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im Blog. dashboard.statistic=Übersicht +dashboard.maintenance_operations=Wartungsoperationen dashboard.system_status=System-Status dashboard.operation_name=Name der Operation dashboard.operation_switch=Wechseln @@ -2759,6 +2927,7 @@ dashboard.reinit_missing_repos=Alle Git-Repositories neu einlesen, für die Eint dashboard.sync_external_users=Externe Benutzerdaten synchronisieren dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen dashboard.cleanup_packages=Veraltete Pakete löschen +dashboard.cleanup_actions=Abgelaufene Ressourcen von Actions bereinigen dashboard.server_uptime=Server-Uptime dashboard.current_goroutine=Aktuelle Goroutinen dashboard.current_memory_usage=Aktuelle Speichernutzung @@ -2788,12 +2957,19 @@ dashboard.total_gc_time=Gesamte GC-Pause dashboard.total_gc_pause=Gesamte GC-Pause dashboard.last_gc_pause=Letzte GC-Pause dashboard.gc_times=Anzahl GC +dashboard.delete_old_actions=Alle alten Aktionen aus der Datenbank löschen +dashboard.delete_old_actions.started=Löschen aller alten Aktionen in der Datenbank gestartet. dashboard.update_checker=Update-Checker dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen +dashboard.stop_zombie_tasks=Zombie-Aufgaben stoppen +dashboard.stop_endless_tasks=Endlose Aktionen stoppen +dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen +dashboard.start_schedule_tasks=Terminierte Aufgaben starten dashboard.sync_branch.started=Synchronisierung der Branches gestartet dashboard.sync_tag.started=Tag-Synchronisierung gestartet dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen +dashboard.sync_repo_licenses=Repo-Lizenzen synchronisieren users.user_manage_panel=Benutzerkontenverwaltung users.new_account=Benutzerkonto erstellen @@ -2865,6 +3041,10 @@ emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: % emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet. emails.change_email_header=E-Mail-Eigenschaften aktualisieren emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest? +emails.delete=E-Mail löschen +emails.delete_desc=Willst du diese E-Mail-Adresse wirklich löschen? +emails.deletion_success=Die E-Mail-Adresse wurde gelöscht. +emails.delete_primary_email_error=Du kannst die primäre E-Mail-Adresse nicht löschen. orgs.org_manage_panel=Organisationsverwaltung orgs.name=Name @@ -2897,10 +3077,12 @@ packages.size=Größe packages.published=Veröffentlicht defaulthooks=Standard-Webhooks +defaulthooks.desc=Webhooks senden automatisch eine HTTP-POST-Anfrage an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks sind die Standardwerte, die in alle neuen Repositories kopiert werden. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). defaulthooks.add_webhook=Standard-Webhook hinzufügen defaulthooks.update_webhook=Standard-Webhook aktualisieren systemhooks=System-Webhooks +systemhooks.desc=Webhooks senden automatisch HTTP-POST-Anfragen an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks werden auf alle Repositories des Systems übertragen, beachte daher mögliche Performance-Einbrüche. Mehr Infos findest du in der Webhooks-Anleitung (auf Englisch). systemhooks.add_webhook=System-Webhook hinzufügen systemhooks.update_webhook=System-Webhook aktualisieren @@ -2995,7 +3177,18 @@ auths.tips=Tipps auths.tips.oauth2.general=OAuth2-Authentifizierung auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten: auths.tip.oauth2_provider=OAuth2-Anbieter +auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz +auths.tip.dropbox=Erstelle eine neue App auf %s +auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu +auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung +auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s +auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s +auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL "https://{server}/.well-known/openid-configuration", um die Endpunkte zu spezifizieren +auths.tip.twitter=Gehe zu %s, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist +auths.tip.discord=Erstelle unter %s eine neue Anwendung +auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s +auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem "Yandex.Passport API" Bereich: "Zugriff auf E-Mail-Adresse", "Zugriff auf Benutzeravatar" und "Zugriff auf Benutzername, Vor- und Nachname, Geschlecht"` auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) auths.edit=Authentifikationsquelle bearbeiten auths.activated=Diese Authentifikationsquelle ist aktiviert @@ -3111,6 +3304,10 @@ config.cache_adapter=Cache-Adapter config.cache_interval=Cache-Intervall config.cache_conn=Cache-Anbindung config.cache_item_ttl=Cache Item-TTL +config.cache_test=Cache testen +config.cache_test_failed=Fehler beim Prüfen des Caches: %v. +config.cache_test_slow=Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s. +config.cache_test_succeeded=Cache-Test erfolgreich, Antwort in %s erhalten. config.session_config=Session-Konfiguration config.session_provider=Session-Provider @@ -3157,6 +3354,7 @@ monitor.next=Nächste Ausführung monitor.previous=Letzte Ausführung monitor.execute_times=Ausführungen monitor.process=Laufende Prozesse +monitor.stacktrace=Stacktraces monitor.processes_count=%d Prozesse monitor.download_diagnosis_report=Diagnosebericht herunterladen monitor.desc=Beschreibung @@ -3164,6 +3362,8 @@ monitor.start=Startzeit monitor.execute_time=Ausführungszeit monitor.last_execution_result=Ergebnis monitor.process.cancel=Prozess abbrechen +monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen +monitor.process.cancel_notices=Abbrechen: %s? monitor.process.children=Subprozesse monitor.queues=Warteschlangen @@ -3202,11 +3402,13 @@ notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. self_check.no_problem_found=Bisher wurde kein Problem festgestellt. +self_check.startup_warnings=Warnungen beim Start: self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren. self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen. self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben. self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden. +self_check.location_origin_mismatch=Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header "Host" und "X-Forwarded-Proto" korrekt gesetzt sind. [action] create_repo=hat das Repository %s erstellt @@ -3234,6 +3436,7 @@ mirror_sync_create=neue Referenz %[3]s bei % mirror_sync_delete=hat die Referenz des Mirrors %[2]s in %[3]s synchronisiert und gelöscht approve_pull_request=`hat %[3]s#%[2]s approved` reject_pull_request=`schlug Änderungen für %[3]s#%[2]s vor` +publish_release=`veröffentlichte Release "%[4]s" in %[3]s` review_dismissed=`verwarf das Review von %[4]s in %[3]s#%[2]s` review_dismissed_reason=Grund: create_branch=legte den Branch %[3]s in %[4]s an @@ -3262,6 +3465,8 @@ raw_minutes=Minuten [dropzone] default_message=Zum Hochladen hier klicken oder Datei ablegen. +invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden. +file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB). remove_file=Datei entfernen [notification] @@ -3298,6 +3503,7 @@ error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bere title=Pakete desc=Repository-Pakete verwalten. empty=Noch keine Pakete vorhanden. +no_metadata=Keine Metadaten. empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der Dokumentation. empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den Paketeinstellungen und verlinke es mit diesem Repo. registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der Dokumentation nach. @@ -3332,6 +3538,8 @@ alpine.repository=Repository-Informationen alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architekturen +arch.registry=Server mit gebrauchtem Repository und Architektur zu /etc/pacman.conf hinzufügen: +arch.install=Paket mit pacman synchronisieren: arch.repository=Repository-Informationen arch.repository.repositories=Repositories arch.repository.architectures=Architekturen @@ -3382,6 +3590,7 @@ npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl au npm.install2=oder füge es zur package.json-Datei hinzu: npm.dependencies=Abhängigkeiten npm.dependencies.development=Entwicklungsabhängigkeiten +npm.dependencies.bundle=Gebündelte Abhängigkeiten npm.dependencies.peer=Peer Abhängigkeiten npm.dependencies.optional=Optionale Abhängigkeiten npm.details.tag=Tag @@ -3513,6 +3722,7 @@ runners.status.active=Aktiv runners.status.offline=Offline runners.version=Version runners.reset_registration_token=Registrierungs-Token zurücksetzen +runners.reset_registration_token_confirm=Möchtest du den aktuellen Token invalidieren und einen neuen generieren? runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt runs.all_workflows=Alle Workflows @@ -3522,6 +3732,7 @@ runs.pushed_by=gepusht von runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten. +runs.no_job=Der Workflow muss mindestens einen Job enthalten runs.actor=Initiator runs.status=Status runs.actors_no_select=Alle Initiatoren @@ -3532,12 +3743,18 @@ runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der Dokumentation. runs.no_runs=Der Workflow hat noch keine Ausführungen. runs.empty_commit_message=(leere Commit-Nachricht) +runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren. workflow.disable=Workflow deaktivieren workflow.disable_success=Workflow '%s' erfolgreich deaktiviert. workflow.enable=Workflow aktivieren workflow.enable_success=Workflow '%s' erfolgreich aktiviert. workflow.disabled=Workflow ist deaktiviert. +workflow.run=Workflow ausführen +workflow.not_found=Workflow '%s' wurde nicht gefunden. +workflow.run_success=Workflow '%s' erfolgreich ausgeführt. +workflow.from_ref=Nutze Workflow von +workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger. need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich. @@ -3557,8 +3774,11 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt. variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.success=Die Variable wurde bearbeitet. +logs.always_auto_scroll=Autoscroll für Logs immer aktivieren +logs.always_expand_running=Laufende Logs immer erweitern [projects] +deleted.display_name=Gelöschtes Projekt type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt type-3.display_name=Organisationsprojekt From 0ed160ffea91ac1903bd33866fa93c24e3ace65c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Dec 2024 09:05:56 +0800 Subject: [PATCH 16/55] Refactor tests (#33021) 1. fix incorrect tests, for example: BeanExists doesn't do assert and shouldn't be used 2. remove unnecessary test functions 3. introduce DumpQueryResult to help to see the database rows during test (at least I need it) ``` ====== DumpQueryResult: SELECT * FROM action_runner_token ====== - # row[0] id: 1 token: xeiWBL5kuTYxGPynHCqQdoeYmJAeG3IzGXCYTrDX owner_id: 0 ... ``` --- models/activities/user_heatmap_test.go | 8 ++- models/auth/webauthn_test.go | 6 +-- models/issues/issue_user_test.go | 6 +-- models/issues/label_test.go | 2 +- models/organization/org_user_test.go | 2 +- models/unittest/fixtures.go | 20 +++---- models/unittest/reflection.go | 4 +- models/unittest/testdb.go | 5 -- models/unittest/unit_tests.go | 73 ++++++++++++++++++-------- routers/web/repo/issue_label_test.go | 9 ++-- services/org/user_test.go | 2 +- tests/integration/auth_ldap_test.go | 2 +- tests/integration/pull_review_test.go | 8 +-- tests/integration/signin_test.go | 4 +- 14 files changed, 83 insertions(+), 68 deletions(-) diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index a039fd3613..380045d3c5 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -64,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { for _, tc := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID}) - doer := &user_model.User{ID: tc.doerID} - _, err := unittest.LoadBeanIfExists(doer) - assert.NoError(t, err) - if tc.doerID == 0 { - doer = nil + var doer *user_model.User + if tc.doerID != 0 { + doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID}) } // get the action for comparison diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go index f1cf398adf..654427e974 100644 --- a/models/auth/webauthn_test.go +++ b/models/auth/webauthn_test.go @@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 1 assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1}) } func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { @@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1}) cred.SignCount = 0xffffffff assert.NoError(t, cred.UpdateSignCount(db.DefaultContext)) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) } func TestCreateCredential(t *testing.T) { @@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) { assert.Equal(t, "WebAuthn Created Credential", res.Name) assert.Equal(t, []byte("Test"), res.CredentialID) - unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) + unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) } diff --git a/models/issues/issue_user_test.go b/models/issues/issue_user_test.go index ce47adb53a..7c21aa15ee 100644 --- a/models/issues/issue_user_test.go +++ b/models/issues/issue_user_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_NewIssueUsers(t *testing.T) { @@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) { } // artificially insert new issue - unittest.AssertSuccessfulInsert(t, newIssue) - - assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) + require.NoError(t, db.Insert(db.DefaultContext, newIssue)) + require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue)) // issue_user table should now have entries for new issue unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID}) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 1d4b6f4684..185fa11bbc 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) { expectedNumIssues := label.NumIssues expectedNumClosedIssues := label.NumClosedIssues - if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) { + if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil { expectedNumIssues-- if issue.IsClosed { expectedNumClosedIssues-- diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index 55abb63203..c5110b2a34 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -131,7 +131,7 @@ func TestAddOrgUser(t *testing.T) { testSuccess := func(orgID, userID int64, isPublic bool) { org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID}) expectedNumMembers := org.NumMembers - if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil { expectedNumMembers++ } assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID)) diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go index 4dde5410d6..517584fdc2 100644 --- a/models/unittest/fixtures.go +++ b/models/unittest/fixtures.go @@ -1,12 +1,10 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package unittest import ( "fmt" - "os" "time" "code.gitea.io/gitea/models/db" @@ -37,7 +35,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { } else { fixtureOptionFiles = testfixtures.Files(opts.Files...) } - dialect := "unknown" + var dialect string switch e.Dialect().URI().DBType { case schemas.POSTGRES: dialect = "postgres" @@ -48,8 +46,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { case schemas.SQLITE: dialect = "sqlite3" default: - fmt.Println("Unsupported RDBMS for integration tests") - os.Exit(1) + return fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType) } loaderOptions := []func(loader *testfixtures.Loader) error{ testfixtures.Database(e.DB().DB), @@ -69,9 +66,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { // register the dummy hash algorithm function used in the test fixtures _ = hash.Register("dummy", hash.NewDummyHasher) - setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - return err } @@ -87,7 +82,7 @@ func LoadFixtures(engine ...*xorm.Engine) error { time.Sleep(200 * time.Millisecond) } if err != nil { - fmt.Printf("LoadFixtures failed after retries: %v\n", err) + return fmt.Errorf("LoadFixtures failed after retries: %w", err) } // Now if we're running postgres we need to tell it to update the sequences if e.Dialect().URI().DBType == schemas.POSTGRES { @@ -108,21 +103,18 @@ func LoadFixtures(engine ...*xorm.Engine) error { AND T.relname = PGT.tablename ORDER BY S.relname;`) if err != nil { - fmt.Printf("Failed to generate sequence update: %v\n", err) - return err + return fmt.Errorf("failed to generate sequence update: %w", err) } for _, r := range results { for _, value := range r { _, err = e.Exec(value) if err != nil { - fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err) - return err + return fmt.Errorf("failed to update sequence: %s, error: %w", value, err) } } } } _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - - return err + return nil } diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index 141fc66b99..bc96a05973 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -4,7 +4,7 @@ package unittest import ( - "log" + "fmt" "reflect" ) @@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value { } f := v.FieldByName(field) if !f.IsValid() { - log.Panicf("can not read %s for %v", field, v) + panic(fmt.Errorf("can not read %s for %v", field, v)) } return f } diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 12f3c25676..3fb53541df 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -34,11 +34,6 @@ var ( fixturesDir string ) -// FixturesDir returns the fixture directory -func FixturesDir() string { - return fixturesDir -} - func fatalTestError(fmtStr string, args ...any) { _, _ = fmt.Fprintf(os.Stderr, fmtStr, args...) os.Exit(1) diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 4ac858e04e..1c5595aef8 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -4,13 +4,17 @@ package unittest import ( + "fmt" "math" + "os" + "strings" "code.gitea.io/gitea/models/db" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "xorm.io/builder" + "xorm.io/xorm" ) // Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons. @@ -51,22 +55,23 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine { return e.OrderBy(orderBy) } -// LoadBeanIfExists loads beans from fixture database if exist -func LoadBeanIfExists(bean any, conditions ...any) (bool, error) { +func getBeanIfExists(bean any, conditions ...any) (bool, error) { e := db.GetEngine(db.DefaultContext) return whereOrderConditions(e, conditions).Get(bean) } -// BeanExists for testing, check if a bean exists -func BeanExists(t assert.TestingT, bean any, conditions ...any) bool { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - return exists +func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) { + exists, err := getBeanIfExists(bean, conditions...) + require.NoError(t, err) + if exists { + return bean + } + return ret } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) require.NoError(t, err) require.True(t, exists, "Expected to find %+v (of type %T, with conditions %+v), but did not", @@ -112,25 +117,11 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int { // AssertNotExistsBean assert that a bean does not exist in the test database func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) + exists, err := getBeanIfExists(bean, conditions...) assert.NoError(t, err) assert.False(t, exists) } -// AssertExistsIf asserts that a bean exists or does not exist, depending on -// what is expected. -func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) { - exists, err := LoadBeanIfExists(bean, conditions...) - assert.NoError(t, err) - assert.Equal(t, expected, exists) -} - -// AssertSuccessfulInsert assert that beans is successfully inserted -func AssertSuccessfulInsert(t assert.TestingT, beans ...any) { - err := db.Insert(db.DefaultContext, beans...) - assert.NoError(t, err) -} - // AssertCount assert the count of a bean func AssertCount(t assert.TestingT, bean, expected any) bool { return assert.EqualValues(t, expected, GetCount(t, bean)) @@ -155,3 +146,39 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond), "Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond) } + +// DumpQueryResult dumps the result of a query for debugging purpose +func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) { + x := db.GetEngine(db.DefaultContext).(*xorm.Engine) + goDB := x.DB().DB + sql, ok := sqlOrBean.(string) + if !ok { + sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean)) + } else if !strings.Contains(sql, " ") { + sql = fmt.Sprintf("SELECT * FROM %s", sql) + } + rows, err := goDB.Query(sql, sqlArgs...) + require.NoError(t, err) + defer rows.Close() + columns, err := rows.Columns() + require.NoError(t, err) + + _, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql) + idx := 0 + for rows.Next() { + row := make([]any, len(columns)) + rowPointers := make([]any, len(columns)) + for i := range row { + rowPointers[i] = &row[i] + } + require.NoError(t, rows.Scan(rowPointers...)) + _, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx) + for i, col := range columns { + _, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i]) + } + idx++ + } + if idx == 0 { + _, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", ")) + } +} diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index c86a03da51..8a613e2c7e 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -162,10 +162,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { UpdateIssueLabel(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) for _, issueID := range testCase.IssueIDs { - unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{ - IssueID: issueID, - LabelID: testCase.LabelID, - }) + if testCase.ExpectedAdd { + unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } else { + unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) + } } unittest.CheckConsistencyFor(t, &issues_model.Label{}) } diff --git a/services/org/user_test.go b/services/org/user_test.go index 56d01a3b63..96d1a1c8ca 100644 --- a/services/org/user_test.go +++ b/services/org/user_test.go @@ -47,7 +47,7 @@ func TestRemoveOrgUser(t *testing.T) { testSuccess := func(org *organization.Organization, user *user_model.User) { expectedNumMembers := org.NumMembers - if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) { + if unittest.GetBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) != nil { expectedNumMembers-- } assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user)) diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 5d37244331..5c50fd0288 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -332,7 +332,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { // Assert members of LDAP group "cn=git" are added for _, gitLDAPUser := range te.gitLDAPUsers { - unittest.BeanExists(t, &user_model.User{ + unittest.AssertExistsAndLoadBean(t, &user_model.User{ Name: gitLDAPUser.UserName, }) } diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go index 5ecf3ef469..68de421413 100644 --- a/tests/integration/pull_review_test.go +++ b/tests/integration/pull_review_test.go @@ -92,7 +92,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) assert.NoError(t, pr.LoadIssue(db.DefaultContext)) err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request") @@ -139,7 +139,7 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) t.Run("Forked Repo Pull Request", func(t *testing.T) { @@ -169,13 +169,13 @@ func TestPullView_CodeOwner(t *testing.T) { testPullCreateDirectly(t, session, "user5", "test_codeowner", forkedRepo.DefaultBranch, "", "", "codeowner-basebranch-forked", "Test Pull Request on Forked Repository") pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertNotExistsBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) // create a pull request to base repository, code reviewers should be mentioned testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkedRepo.OwnerName, forkedRepo.Name, "codeowner-basebranch-forked", "Test Pull Request3") pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"}) - unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) }) }) } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index abad9eb5e5..d7c0b1bcd3 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testLoginFailed(t *testing.T, username, password, message string) { @@ -42,7 +44,7 @@ func TestSignin(t *testing.T) { user.Name = "testuser" user.LowerName = strings.ToLower(user.Name) user.ID = 0 - unittest.AssertSuccessfulInsert(t, user) + require.NoError(t, db.Insert(db.DefaultContext, user)) samples := []struct { username string From ff96873e3e14f6eae853ec8eee59ab4482962c43 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sun, 29 Dec 2024 02:32:19 +0100 Subject: [PATCH 17/55] Fix templating in pull request comparison (#33025) Adds test for expected behavior Closes: https://github.com/go-gitea/gitea/issues/33013 --- templates/base/alert_details.tmpl | 10 ++-- tests/integration/pull_create_test.go | 68 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6801c8240f..6d4c1fb2db 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -1,7 +1,11 @@ {{.Message}} +{{if .Details}}
{{.Summary}} - - {{.Details | SanitizeHTML}} - + {{.Details | SanitizeHTML}}
+{{else}} +
+ {{.Summary}} +
+{{end}} diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index 4bc2a1da9a..162ea532c8 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -83,6 +84,30 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b return resp } +func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, baseRepoName, baseBranch, headRepoOwner, headRepoName, headBranch, title string) *httptest.ResponseRecorder { + headCompare := headBranch + if headRepoOwner != "" { + if headRepoName != "" { + headCompare = fmt.Sprintf("%s/%s:%s", headRepoOwner, headRepoName, headBranch) + } else { + headCompare = fmt.Sprintf("%s:%s", headRepoOwner, headBranch) + } + } + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/compare/%s...%s", baseRepoOwner, baseRepoName, baseBranch, headCompare)) + resp := session.MakeRequest(t, req, http.StatusOK) + + // Submit the form for creating the pull + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action") + assert.True(t, exists, "The template has changed") + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "title": title, + }) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + return resp +} + func TestPullCreate(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user1") @@ -245,3 +270,46 @@ func TestCreateAgitPullWithReadPermission(t *testing.T) { }) }) } + +/* +Setup: user2 has repository, user1 forks it +--- + +1. User2 blocks User1 +2. User1 adds changes to fork +3. User1 attempts to create a pull request +4. User1 sees alert that the action is not allowed because of the block +*/ +func TestCreatePullWhenBlocked(t *testing.T) { + RepoOwner := "user2" + ForkOwner := "user16" + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // Setup + // User1 forks repo1 from User2 + sessionFork := loginUser(t, ForkOwner) + testRepoFork(t, sessionFork, RepoOwner, "repo1", ForkOwner, "forkrepo1", "") + + // 1. User2 blocks user1 + // sessionBase := loginUser(t, "user2") + token := getUserToken(t, RepoOwner, auth_model.AccessTokenScopeWriteUser) + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + // 2. User1 adds changes to fork + testEditFile(t, sessionFork, ForkOwner, "forkrepo1", "master", "README.md", "Hello, World (Edited)\n") + + // 3. User1 attempts to create a pull request + testPullCreateFailure(t, sessionFork, RepoOwner, "repo1", "master", ForkOwner, "forkrepo1", "master", "This is a pull title") + + // Teardown + // Unblock user + req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + }) +} From a96776b3cbaf69a812677d3b49b41ab3eb8ac860 Mon Sep 17 00:00:00 2001 From: Henry Goodman <79623665+henrygoodman@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:04:13 +1100 Subject: [PATCH 18/55] Fix review code comment avatar alignment (#33031) Fixes #33017 Avatar should only have offset if the `Comment` has `Content` or `Attachment` to align with the speech bubble. --------- Co-authored-by: wxiaoguang --- templates/repo/issue/view_content/comments.tmpl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 9fdbf45939..2e1a67edcc 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -365,8 +365,9 @@ {{if .Review}}{{$reviewType = .Review.Type}}{{end}} {{if not .OriginalAuthor}} {{/* Some timeline avatars need a offset to correctly align with their speech bubble. - The condition depends on whether the comment has contents/attachments or reviews */}} - + The condition depends on whether the comment has contents/attachments, + review's comment is also controlled/rendered by issue comment's Content field */}} + {{ctx.AvatarUtils.Avatar .Poster 40}} {{end}} From 1dbf0d7f0822c10b379a21c192c2d63e34fd52f9 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 30 Dec 2024 01:25:49 +0800 Subject: [PATCH 19/55] Test webhook email (#33033) Close #27918 --- services/webhook/webhook_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 63cbce1771..6bac02712b 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -9,11 +9,15 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/services/convert" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWebhook_GetSlackHook(t *testing.T) { @@ -77,3 +81,11 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { unittest.AssertNotExistsBean(t, hookTask) } } + +func TestWebhookUserMail(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + setting.Service.NoReplyAddress = "no-reply.com" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) + assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) +} From cd1b5488a31ff208b511302905330499167faa10 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 30 Dec 2024 09:57:38 +0800 Subject: [PATCH 20/55] Refactor pagination (#33037) I am sure the simple approach should work, let's try it in 1.24 Follow #29834 and #29841 --- models/user/search.go | 2 -- routers/web/admin/emails.go | 2 +- routers/web/admin/packages.go | 4 +--- routers/web/admin/repos.go | 7 ++----- routers/web/admin/users.go | 5 ----- routers/web/explore/code.go | 3 +-- routers/web/explore/repo.go | 21 +-------------------- routers/web/explore/user.go | 5 +---- routers/web/org/home.go | 19 +------------------ routers/web/org/projects.go | 2 +- routers/web/repo/actions/actions.go | 6 +----- routers/web/repo/branch.go | 2 +- routers/web/repo/commit.go | 14 +++----------- routers/web/repo/milestone.go | 4 +--- routers/web/repo/packages.go | 3 +-- routers/web/repo/projects.go | 2 +- routers/web/repo/release.go | 4 ++-- routers/web/repo/search.go | 3 +-- routers/web/repo/wiki.go | 3 +-- routers/web/user/code.go | 3 +-- routers/web/user/home.go | 7 ++----- routers/web/user/notification.go | 22 +++------------------- routers/web/user/package.go | 13 ++----------- routers/web/user/setting/profile.go | 4 ++-- services/context/pagination.go | 24 ++---------------------- 25 files changed, 33 insertions(+), 151 deletions(-) diff --git a/models/user/search.go b/models/user/search.go index 6af3389237..85915f4020 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -39,8 +39,6 @@ type SearchUserOptions struct { IsTwoFactorEnabled optional.Option[bool] IsProhibitLogin optional.Option[bool] IncludeReserved bool - - ExtraParamStrings map[string]string } func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index e925de8937..23ddfa583a 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -94,7 +94,7 @@ func Emails(ctx *context.Context) { ctx.Data["Emails"] = emails pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplEmails) diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index da345f2f89..5122342259 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -77,9 +77,7 @@ func Packages(ctx *context.Context) { ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) - pager.AddParamString("sort", sort) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index 27d39dcf4b..1bc8abb88c 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -4,7 +4,6 @@ package admin import ( - "fmt" "net/http" "net/url" "strings" @@ -84,8 +83,7 @@ func UnadoptedRepos(ctx *context.Context) { if !doSearch { pager := context.NewPagination(0, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) return @@ -99,8 +97,7 @@ func UnadoptedRepos(ctx *context.Context) { } ctx.Data["Dirs"] = repoNames pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("search", fmt.Sprint(doSearch)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUnadoptedRepos) } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index fdd18b2f9d..be2ba4424c 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -47,16 +47,12 @@ func Users(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users") ctx.Data["PageIsAdminUsers"] = true - extraParamStrings := map[string]string{} statusFilterKeys := []string{"is_active", "is_admin", "is_restricted", "is_2fa_enabled", "is_prohibit_login"} statusFilterMap := map[string]string{} for _, filterKey := range statusFilterKeys { paramKey := "status_filter[" + filterKey + "]" paramVal := ctx.FormString(paramKey) statusFilterMap[filterKey] = paramVal - if paramVal != "" { - extraParamStrings[paramKey] = paramVal - } } sortType := ctx.FormString("sort") @@ -82,7 +78,6 @@ func Users(ctx *context.Context) { IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones - ExtraParamStrings: extraParamStrings, }, tplUsers) } diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 4df89253b4..3658144610 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -137,8 +137,7 @@ func Code(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplExploreCode) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index c421aea715..cf3128314b 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -4,7 +4,6 @@ package explore import ( - "fmt" "net/http" "code.gitea.io/gitea/models/db" @@ -139,25 +138,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("topic", fmt.Sprint(topicOnly)) - pager.AddParamString("language", language) - pager.AddParamString(relevantReposOnlyParam, fmt.Sprint(opts.OnlyShowRelevant)) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, opts.TplName) diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index ef103af8cf..b14272c76a 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -120,10 +120,7 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - for paramKey, paramVal := range opts.ExtraParamStrings { - pager.AddParamString(paramKey, paramVal) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplName) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index bdc43acc30..deeb18ae7c 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -4,7 +4,6 @@ package org import ( - "fmt" "net/http" "path" "strings" @@ -146,23 +145,7 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["Total"] = count pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("language", language) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplOrgHome) diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index efcc8fadc8..c037d4a7fd 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -120,7 +120,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 099593bff0..f0d8d81fee 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -6,7 +6,6 @@ package actions import ( "bytes" stdCtx "context" - "fmt" "net/http" "slices" "strings" @@ -262,10 +261,7 @@ func List(ctx *context.Context) { ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx) pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("workflow", workflowID) - pager.AddParamString("actor", fmt.Sprint(actorID)) - pager.AddParamString("status", fmt.Sprint(status)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 72fd958e28..2bcd7821b4 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -87,7 +87,7 @@ func Branches(ctx *context.Context) { ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["DefaultBranchBranch"] = defaultBranch pager := context.NewPagination(int(branchesCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplBranch) } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 6f534b9e2e..3655233312 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -101,7 +101,7 @@ func Commits(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), pageSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } @@ -139,7 +139,6 @@ func Graph(ctx *context.Context) { if err != nil { log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) realBranches = []string{} - branches = []string{} graphCommitsCount, err = ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) if err != nil { ctx.ServerError("GetCommitGraphsCount", err) @@ -175,14 +174,7 @@ func Graph(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) - paginator.AddParamString("mode", mode) - paginator.AddParamString("hide-pr-refs", fmt.Sprint(hidePRRefs)) - for _, branch := range branches { - paginator.AddParamString("branch", branch) - } - for _, file := range files { - paginator.AddParamString("file", file) - } + paginator.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = paginator if ctx.FormBool("div-only") { ctx.HTML(http.StatusOK, tplGraphDiv) @@ -262,7 +254,7 @@ func FileHistory(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 392d87167a..6a0e6b25a9 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -4,7 +4,6 @@ package repo import ( - "fmt" "net/http" "net/url" @@ -93,8 +92,7 @@ func Milestones(ctx *context.Context) { ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("q", keyword) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplMilestone) diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index c8d3719bc0..5dcfd0454c 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -70,8 +70,7 @@ func Packages(ctx *context.Context) { ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 4313b6c403..1800f60eae 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -115,7 +115,7 @@ func Projects(ctx *context.Context) { } pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index b8176cb70b..5b099fe8d4 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -186,7 +186,7 @@ func Releases(ctx *context.Context) { numReleases := ctx.Data["NumReleases"].(int64) pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplReleasesList) } @@ -240,7 +240,7 @@ func TagsList(ctx *context.Context) { ctx.Data["TagCount"] = count pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) ctx.HTML(http.StatusOK, tplTagsList) diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index a037a34833..cbc7e2e0fe 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -108,8 +108,7 @@ func Search(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSearch) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 3fca7cebea..19366c0104 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -440,8 +440,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("action", "_revision") + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager return wikiRepo, entry diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f805f2b028..9d515596f1 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -121,8 +121,7 @@ func CodeSearch(ctx *context.Context) { ctx.Data["SearchResultLanguages"] = searchResultLanguages pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5) - pager.SetDefaultParams(ctx) - pager.AddParamString("l", language) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplUserCode) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index b5d3a7294b..c79648a455 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -139,7 +139,7 @@ func Dashboard(ctx *context.Context) { ctx.Data["Feeds"] = feeds pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5) - pager.AddParamString("date", date) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplDashboard) @@ -330,10 +330,7 @@ func Milestones(ctx *context.Context) { ctx.Data["IsShowClosed"] = isShowClosed pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("q", keyword) - pager.AddParamString("repos", reposQuery) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplMilestones) diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 732fac2595..1c91ff6364 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -173,7 +173,7 @@ func getNotifications(ctx *context.Context) { ctx.Data["Status"] = status ctx.Data["Notifications"] = notifications - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager } @@ -357,8 +357,7 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) return } - pager.AddParamString("sort", sortType) - pager.AddParamString("state", state) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplNotificationSubscriptions) @@ -446,22 +445,7 @@ func NotificationWatching(ctx *context.Context) { // redirect to last page if request page is more than total pages pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) - pager.SetDefaultParams(ctx) - if archived.Has() { - pager.AddParamString("archived", fmt.Sprint(archived.Value())) - } - if fork.Has() { - pager.AddParamString("fork", fmt.Sprint(fork.Value())) - } - if mirror.Has() { - pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) - } - if template.Has() { - pager.AddParamString("template", fmt.Sprint(template.Value())) - } - if private.Has() { - pager.AddParamString("private", fmt.Sprint(private.Value())) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["Status"] = 2 diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 8d78da7c5a..1f75faf1c6 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -128,8 +128,7 @@ func ListPackages(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - pager.AddParamString("q", query) - pager.AddParamString("type", packageType) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackagesList) @@ -348,11 +347,6 @@ func ListPackageVersions(ctx *context.Context) { ctx.Data["Query"] = query ctx.Data["Sort"] = sort - pagerParams := map[string]string{ - "q": query, - "sort": sort, - } - var ( total int64 pvs []*packages_model.PackageVersion @@ -361,7 +355,6 @@ func ListPackageVersions(ctx *context.Context) { case packages_model.TypeContainer: tagged := ctx.FormTrim("tagged") - pagerParams["tagged"] = tagged ctx.Data["Tagged"] = tagged pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ @@ -407,9 +400,7 @@ func ListPackageVersions(ctx *context.Context) { } pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5) - for k, v := range pagerParams { - pager.AddParamString(k, v) - } + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplPackageVersionList) diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 4b3c214096..ebf682bf58 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -222,7 +222,7 @@ func Organization(ctx *context.Context) { ctx.Data["Orgs"] = orgs pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsOrganization) } @@ -329,7 +329,7 @@ func Repos(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) - pager.SetDefaultParams(ctx) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplSettingsRepositories) } diff --git a/services/context/pagination.go b/services/context/pagination.go index 42117cf96d..d33dd217d0 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -27,19 +27,13 @@ func NewPagination(total, pagingNum, current, numPages int) *Pagination { return p } -// AddParamString adds a string parameter directly -func (p *Pagination) AddParamString(key, value string) { - urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) - p.urlParams = append(p.urlParams, urlParam) -} - func (p *Pagination) AddParamFromRequest(req *http.Request) { for key, values := range req.URL.Query() { - if key == "page" || len(values) == 0 { + if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") { continue } for _, value := range values { - urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value)) + urlParam := fmt.Sprintf("%s=%v", url.QueryEscape(key), url.QueryEscape(value)) p.urlParams = append(p.urlParams, urlParam) } } @@ -49,17 +43,3 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) { func (p *Pagination) GetParams() template.URL { return template.URL(strings.Join(p.urlParams, "&")) } - -// SetDefaultParams sets common pagination params that are often used -func (p *Pagination) SetDefaultParams(ctx *Context) { - if v, ok := ctx.Data["SortType"].(string); ok { - p.AddParamString("sort", v) - } - if v, ok := ctx.Data["Keyword"].(string); ok { - p.AddParamString("q", v) - } - if v, ok := ctx.Data["IsFuzzy"].(bool); ok { - p.AddParamString("fuzzy", fmt.Sprint(v)) - } - // do not add any more uncommon params here! -} From 232867cff6d24e70892e55d3205ebdad0c6bf3d5 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 29 Dec 2024 21:37:59 -0500 Subject: [PATCH 21/55] use `-s -w` ldflags for release artifacts (#33041) fix #33030 --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 32ea823e1e..5524cfb08c 100644 --- a/Makefile +++ b/Makefile @@ -806,22 +806,22 @@ $(DIST_DIRS): .PHONY: release-windows release-windows: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) . ifeq (,$(findstring gogit,$(TAGS))) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . endif .PHONY: release-linux release-linux: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w -linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . .PHONY: release-darwin release-darwin: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . .PHONY: release-freebsd release-freebsd: | $(DIST_DIRS) - CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . + CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-s -w $(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . .PHONY: release-copy release-copy: | $(DIST_DIRS) From 344c89ea34bda1ac7382f8cba210ee325e07597e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 29 Dec 2024 19:04:22 -0800 Subject: [PATCH 22/55] Fix bug automerge cannot be chosed when there is only 1 merge style (#33040) This is a quick bug fix. Even if there is only 1 merge style, the dropdown menu will still be displayed to allow users to choose auto-merge. Fix #32448 --- web_src/js/components/PullRequestMergeForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index e8bcee70db..bafeec6c97 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -147,7 +147,7 @@ function clearMergeMessage() { -