From 30008fcfcfa84bf607baa493ffcebe7102363ba4 Mon Sep 17 00:00:00 2001 From: hiifong Date: Fri, 13 Dec 2024 08:45:06 +0800 Subject: [PATCH 01/49] Fix bug of branch/tag selector in the issue sidebar (#32744) Fix: #32731 --------- Co-authored-by: wxiaoguang --- models/issues/issue.go | 7 +++++-- .../repo/issue/branch_selector_field.tmpl | 18 ++++++++++++++++-- templates/repo/issue/new_form.tmpl | 3 ++- templates/repo/issue/view_content/sidebar.tmpl | 2 +- templates/shared/issuelist.tmpl | 2 +- web_src/js/features/repo-issue-sidebar.ts | 1 + 6 files changed, 26 insertions(+), 7 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index 64fc20cc05e..fe347c27156 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -125,8 +125,11 @@ type Issue struct { IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. PullRequest *PullRequest `xorm:"-"` NumComments int - Ref string - PinOrder int `xorm:"DEFAULT 0"` + + // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" + Ref string + + PinOrder int `xorm:"DEFAULT 0"` DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl index 286ac0cd051..9183b7b46ab 100644 --- a/templates/repo/issue/branch_selector_field.tmpl +++ b/templates/repo/issue/branch_selector_field.tmpl @@ -1,3 +1,17 @@ +{{/* TODO: RemoveIssueRef: the Issue.Ref will be removed in 1.24 or 1.25 if no end user really needs it or there could be better alternative then. +PR: https://github.com/go-gitea/gitea/pull/32744 + +The Issue.Ref was added by Add possibility to record branch or tag information in an issue (#780) +After 8 years, this "branch selector" does nothing more than saving the branch/tag name into database and displays it. + +There are still users using it: +* @didim99: it is a really useful feature to specify a branch in which issue found. + +Still needs to figure out: +* Could the "recording branch/tag name" be replaced by other approaches? + * Write the branch name in the issue title/body then it will still be displayed, eg: `[bug] (fix/ui-broken-bug) there is a bug ....` +* Is "GitHub-like development sidebar (`#31899`)" good enough (or better) for your usage? +*/}} {{if and (not .Issue.IsPull) (not .PageIsComparePull)}} {{else}} -
{{ctx.Locale.Tr "no_results_found"}}
+
{{ctx.Locale.Tr "no_results_found"}}
{{end}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index ceaaebc4d54..dd4c7617ce1 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -47,7 +47,8 @@
- {{template "repo/issue/branch_selector_field" $}} + {{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}} + {{if .PageIsComparePull}} {{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 02f5d3e2df9..987a882be7b 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -1,5 +1,5 @@
- {{template "repo/issue/branch_selector_field" $}} + {{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}} {{if .Issue.IsPull}} {{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index fe5184e7d22..a2b802f2a2d 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -99,7 +99,7 @@ {{.Project.Title}} {{end}} - {{if .Ref}} + {{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}} {{svg "octicon-git-branch" 14}} {{index $.IssueRefEndNames .ID}} diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts index 45cd38d533d..ef2b7d143cd 100644 --- a/web_src/js/features/repo-issue-sidebar.ts +++ b/web_src/js/features/repo-issue-sidebar.ts @@ -4,6 +4,7 @@ import {queryElems, toggleElem} from '../utils/dom.ts'; import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts'; function initBranchSelector() { + // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); if (!elSelectBranch) return; From 2910f384d51af26d13b0273ce1a918abc384f51e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 13 Dec 2024 11:57:37 +0800 Subject: [PATCH 02/49] Fix misuse of PublicKeyCallback (#32810) Only upgrading the ssh package is not enough. --- go.mod | 2 +- go.sum | 4 +-- modules/ssh/ssh.go | 68 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 17be4cbd526..671151d4b63 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.5 github.com/fsnotify/fsnotify v1.7.0 - github.com/gliderlabs/ssh v0.3.7 + github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-chi/chi/v5 v5.1.0 diff --git a/go.sum b/go.sum index 73bdb44e337..afa3abece8f 100644 --- a/go.sum +++ b/go.sum @@ -293,8 +293,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c h1:82lzmsy5Nr6JA6HcLRVxGfbdSoWfW45C6jnY3zFS7Ks= diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index f8e4f569b87..6d0695ee163 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -13,10 +13,12 @@ import ( "errors" "fmt" "io" + "maps" "net" "os" "os/exec" "path/filepath" + "reflect" "strconv" "strings" "sync" @@ -33,9 +35,22 @@ import ( gossh "golang.org/x/crypto/ssh" ) -type contextKey string +// The ssh auth overall works like this: +// NewServerConn: +// serverHandshake+serverAuthenticate: +// PublicKeyCallback: +// PublicKeyHandler (our code): +// reset(ctx.Permissions) and set ctx.Permissions.giteaKeyID = keyID +// pubKey.Verify +// return ctx.Permissions // only reaches here, the pub key is really authenticated +// set conn.Permissions from serverAuthenticate +// sessionHandler(conn) +// +// Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one. +// Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key), +// then only A succeeds to authenticate, sessionHandler will see B's keyID -const giteaKeyID = contextKey("gitea-key-id") +const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id" func getExitStatusFromError(err error) int { if err == nil { @@ -61,8 +76,32 @@ func getExitStatusFromError(err error) int { return waitStatus.ExitStatus() } +// sessionPartial is the private struct from "gliderlabs/ssh/session.go" +// We need to read the original "conn" field from "ssh.Session interface" which contains the "*session pointer" +// https://github.com/gliderlabs/ssh/blob/d137aad99cd6f2d9495bfd98c755bec4e5dffb8c/session.go#L109-L113 +// If upstream fixes the problem and/or changes the struct, we need to follow. +// If the struct mismatches, the builtin ssh server will fail during integration tests. +type sessionPartial struct { + sync.Mutex + gossh.Channel + conn *gossh.ServerConn +} + +func ptr[T any](intf any) *T { + // https://pkg.go.dev/unsafe#Pointer + // (1) Conversion of a *T1 to Pointer to *T2. + // Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, + // this conversion allows reinterpreting data of one type as data of another type. + v := reflect.ValueOf(intf) + p := v.UnsafePointer() + return (*T)(p) +} + func sessionHandler(session ssh.Session) { - keyID := fmt.Sprintf("%d", session.Context().Value(giteaKeyID).(int64)) + // here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one. + // so we must use the original ssh conn, which always contains the correct (verified) keyID. + sshConn := ptr[sessionPartial](session) + keyID := sshConn.conn.Permissions.Extensions[giteaPermissionExtensionKeyID] command := session.RawCommand() @@ -164,6 +203,23 @@ func sessionHandler(session ssh.Session) { } func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + // The publicKeyHandler (PublicKeyCallback) only helps to provide the candidate keys to authenticate, + // It does NOT really verify here, so we could only record the related information here. + // After authentication (Verify), the "Permissions" will be assigned to the ssh conn, + // then we can use it in the "session handler" + + // first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does) + // it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions" + oldCtxPerm := ctx.Permissions().Permissions + ctx.Permissions().Permissions = &gossh.Permissions{} + ctx.Permissions().Permissions.CriticalOptions = maps.Clone(oldCtxPerm.CriticalOptions) + + setPermExt := func(keyID int64) { + ctx.Permissions().Permissions.Extensions = map[string]string{ + giteaPermissionExtensionKeyID: fmt.Sprint(keyID), + } + } + if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Handle Public Key: Fingerprint: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr()) } @@ -238,8 +294,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key), principal) } - ctx.SetValue(giteaKeyID, pkey.ID) - + setPermExt(pkey.ID) return true } @@ -266,8 +321,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Successfully authenticated: %s Public Key Fingerprint: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) } - ctx.SetValue(giteaKeyID, pkey.ID) - + setPermExt(pkey.ID) return true } From 887928e0a6e6808a2b0b8fd325eb005dcadfc428 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 12 Dec 2024 20:22:39 -0800 Subject: [PATCH 03/49] Add missing two sync feed for refs/pull (#32815) Fowllow #32659 --- services/feed/notifier.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/services/feed/notifier.go b/services/feed/notifier.go index a8820aeb777..d941027c352 100644 --- a/services/feed/notifier.go +++ b/services/feed/notifier.go @@ -417,6 +417,12 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { + // ignore pull sync message for pull requests refs + // TODO: it's better to have a UI to let users chose + if refFullName.IsPull() { + return + } + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), @@ -431,6 +437,12 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { + // ignore pull sync message for pull requests refs + // TODO: it's better to have a UI to let users chose + if refFullName.IsPull() { + return + } + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), From 5bc030efa2cf88ce7f1ec8d8b33c60a7e9408332 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 13 Dec 2024 14:45:32 +0800 Subject: [PATCH 04/49] Fix various UI bugs (#32821) --- modules/markup/markdown/markdown_math_test.go | 40 +++++++++---------- .../markup/markdown/math/block_renderer.go | 13 +++++- .../markup/markdown/math/inline_renderer.go | 9 ++--- templates/repo/view_list.tmpl | 2 +- web_src/css/repo/clone.css | 3 ++ web_src/css/repo/home-file-list.css | 5 +++ web_src/js/markup/math.ts | 5 +-- 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/modules/markup/markdown/markdown_math_test.go b/modules/markup/markdown/markdown_math_test.go index e371b1c74ab..a2213b2ce76 100644 --- a/modules/markup/markdown/markdown_math_test.go +++ b/modules/markup/markdown/markdown_math_test.go @@ -20,23 +20,23 @@ func TestMathRender(t *testing.T) { }{ { "$a$", - `

a

` + nl, + `

a

` + nl, }, { "$ a $", - `

a

` + nl, + `

a

` + nl, }, { "$a$ $b$", - `

a b

` + nl, + `

a b

` + nl, }, { `\(a\) \(b\)`, - `

a b

` + nl, + `

a b

` + nl, }, { `$a$.`, - `

a.

` + nl, + `

a.

` + nl, }, { `.$a$`, @@ -64,27 +64,27 @@ func TestMathRender(t *testing.T) { }, { "$a$ ($b$) [$c$] {$d$}", - `

a (b) [$c$] {$d$}

` + nl, + `

a (b) [$c$] {$d$}

` + nl, }, { "$$a$$", - `a` + nl, + `a` + nl, }, { "$$a$$ test", - `

a test

` + nl, + `

a test

` + nl, }, { "test $$a$$", - `

test a

` + nl, + `

test a

` + nl, }, { `foo $x=\$$ bar`, - `

foo x=\$ bar

` + nl, + `

foo x=\$ bar

` + nl, }, { `$\text{$b$}$`, - `

\text{$b$}

` + nl, + `

\text{$b$}

` + nl, }, } @@ -110,7 +110,7 @@ func TestMathRenderBlockIndent(t *testing.T) { \alpha \] `, - `

+			`

 \alpha
 
`, @@ -122,7 +122,7 @@ func TestMathRenderBlockIndent(t *testing.T) { \alpha \] `, - `

+			`

 \alpha
 
`, @@ -137,7 +137,7 @@ a d \] `, - `

+			`

 a
 b
 c
@@ -154,7 +154,7 @@ c
   c
   \]
 `,
-			`

+			`

 a
  b
 c
@@ -165,7 +165,7 @@ c
 			"indent-0-oneline",
 			`$$ x $$
 foo`,
-			` x 
+			` x 
 

foo

`, }, @@ -173,7 +173,7 @@ foo`, "indent-3-oneline", ` $$ x $$ foo`, - ` x + ` x

foo

`, }, @@ -188,10 +188,10 @@ foo`, > \] `, `
-

+

 a
 
-

+

 b
 
@@ -207,7 +207,7 @@ b 2. b`, `
  1. a -
    
    +
    
     x
     
  2. diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index a770efa01c7..c29f0618821 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -12,6 +12,17 @@ import ( "github.com/yuin/goldmark/util" ) +// Block render output: +//
    ...
    +// +// Keep in mind that there is another "code block" render in "func (r *GlodmarkRender) highlightingRenderer" +// "highlightingRenderer" outputs the math block with extra "chroma" class: +//
    ...
    +// +// Special classes: +// * "is-loading": show a loading indicator +// * "display": used by JS to decide to render as a block, otherwise render as inline + // BlockRenderer represents a renderer for math Blocks type BlockRenderer struct { renderInternal *internal.RenderInternal @@ -38,7 +49,7 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*Block) if entering { - code := giteaUtil.Iif(n.Inline, "", `
    `) + ``
    +		code := giteaUtil.Iif(n.Inline, "", `
    `) + ``
     		_ = r.renderInternal.FormatWithSafeAttrs(w, code)
     		r.writeLines(w, source, n)
     	} else {
    diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go
    index 0cff4f1e74e..4e0531cf404 100644
    --- a/modules/markup/markdown/math/inline_renderer.go
    +++ b/modules/markup/markdown/math/inline_renderer.go
    @@ -13,6 +13,9 @@ import (
     	"github.com/yuin/goldmark/util"
     )
     
    +// Inline render output:
    +// ...
    +
     // InlineRenderer is an inline renderer
     type InlineRenderer struct {
     	renderInternal *internal.RenderInternal
    @@ -25,11 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen
     
     func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
     	if entering {
    -		extraClass := ""
    -		if _, ok := n.(*InlineBlock); ok {
    -			extraClass = "display "
    -		}
    -		_ = r.renderInternal.FormatWithSafeAttrs(w, ``, extraClass)
    +		_ = r.renderInternal.FormatWithSafeAttrs(w, ``)
     		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
     			segment := c.(*ast.Text).Segment
     			value := util.EscapeHTML(segment.Value(source))
    diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
    index ea61c3736ae..0fdb45e574c 100644
    --- a/templates/repo/view_list.tmpl
    +++ b/templates/repo/view_list.tmpl
    @@ -1,6 +1,6 @@
     {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
     
    -
    +
    {{template "repo/latest_commit" .}}
    {{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
    diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css index 15709a78f65..3f6a1323fea 100644 --- a/web_src/css/repo/clone.css +++ b/web_src/css/repo/clone.css @@ -1,11 +1,14 @@ /* only used by "repo/empty.tmpl" */ .clone-buttons-combo { + display: flex; + align-items: center; flex: 1; } .clone-buttons-combo input { border-left: none !important; border-radius: 0 !important; + height: 30px; } /* used by the clone-panel popup */ diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index eab2124d6f4..ecb26fa6629 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -44,6 +44,10 @@ padding: 6px 10px; } +#repo-files-table .repo-file-last-commit { + background: var(--color-box-header); +} + #repo-files-table .repo-file-cell.name { max-width: 300px; white-space: nowrap; @@ -59,6 +63,7 @@ } #repo-files-table .repo-file-cell.age { + text-align: right; white-space: nowrap; color: var(--color-text-light-1); } diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index 6a1ca2f2e38..22a4de38e98 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -1,9 +1,8 @@ import {displayError} from './common.ts'; function targetElement(el: Element) { - // The target element is either the current element if it has the - // `is-loading` class or the pre that contains it - return el.classList.contains('is-loading') ? el : el.closest('pre'); + // The target element is either the parent "code block with loading indicator", or itself + return el.closest('.code-block.is-loading') ?? el; } export async function renderMath(): Promise { From a66c16dc1bca44d480317c6319144d33949bc724 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 14 Dec 2024 09:39:05 +0800 Subject: [PATCH 05/49] Allow to fork repository into the same owner (#32819) This feature is experimental, not fully tested, and may be changed in the future. It is only designed for users who really need it: set `[repository].ALLOW_FORK_INTO_SAME_OWNER=true` in your app.ini Doc: https://gitea.com/gitea/docs/pulls/122 ![image](https://github.com/user-attachments/assets/38d08c23-9cfc-49d8-9321-ff81edf65395) --- custom/conf/app.example.ini | 6 +++++- modules/repository/fork.go | 10 +++++++++- modules/repository/fork_test.go | 25 +++++++++++++++++++++++++ modules/setting/repository.go | 1 + routers/web/repo/fork.go | 7 ++++--- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 modules/repository/fork_test.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5c23f70d7ca..6377ebf9d2a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1040,9 +1040,13 @@ LEVEL = Info ;; Don't allow download source archive files from UI ;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false -;; Allow fork repositories without maximum number limit +;; Allow to fork repositories without maximum number limit ;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true +;; Allow to fork repositories into the same owner (user or organization) +;; This feature is experimental, not fully tested, and may be changed in the future +;ALLOW_FORK_INTO_SAME_OWNER = false + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository.editor] diff --git a/modules/repository/fork.go b/modules/repository/fork.go index fbf00087167..d5306340716 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -9,14 +9,22 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" ) +func CanUserForkBetweenOwners(id1, id2 int64) bool { + if id1 != id2 { + return true + } + return setting.Repository.AllowForkIntoSameOwner +} + // CanUserForkRepo returns true if specified user can fork repository. func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) { if user == nil { return false, nil } - if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) { + if CanUserForkBetweenOwners(repo.OwnerID, user.ID) && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) { return true, nil } ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, user.ID) diff --git a/modules/repository/fork_test.go b/modules/repository/fork_test.go new file mode 100644 index 00000000000..f8c76d942d1 --- /dev/null +++ b/modules/repository/fork_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestCanUserForkBetweenOwners(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner) + + setting.Repository.AllowForkIntoSameOwner = true + assert.True(t, CanUserForkBetweenOwners(1, 1)) + assert.True(t, CanUserForkBetweenOwners(1, 2)) + + setting.Repository.AllowForkIntoSameOwner = false + assert.False(t, CanUserForkBetweenOwners(1, 1)) + assert.True(t, CanUserForkBetweenOwners(1, 2)) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 14cf5805c02..c5619d0f048 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -53,6 +53,7 @@ var ( AllowDeleteOfUnadoptedRepositories bool DisableDownloadSourceArchives bool AllowForkWithoutMaximumLimit bool + AllowForkIntoSameOwner bool // Repository editor settings Editor struct { diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 27e42a8f98e..86af7056176 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -48,7 +49,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { ctx.Data["repo_name"] = forkRepo.Name ctx.Data["description"] = forkRepo.Description ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate - canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) + canForkToUser := repository.CanUserForkBetweenOwners(forkRepo.OwnerID, ctx.Doer.ID) && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) ctx.Data["ForkRepo"] = forkRepo @@ -66,7 +67,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { traverseParentRepo := forkRepo for { - if ctx.Doer.ID == traverseParentRepo.OwnerID { + if !repository.CanUserForkBetweenOwners(ctx.Doer.ID, traverseParentRepo.OwnerID) { canForkToUser = false } else { for i, org := range orgs { @@ -162,7 +163,7 @@ func ForkPost(ctx *context.Context) { var err error traverseParentRepo := forkRepo for { - if ctxUser.ID == traverseParentRepo.OwnerID { + if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) return } From 7269130d2878d51dcdf11f7081a591f85bd493e8 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Sat, 14 Dec 2024 10:22:30 +0800 Subject: [PATCH 06/49] Fix missing outputs for jobs with matrix (#32823) Fix #32795 If a job uses a matrix, multiple `ActionRunJobs` may have the same `JobID`. We need to merge the outputs of these jobs to make them available to the jobs that need them. --- models/actions/run_job.go | 4 +- models/fixtures/action_run.yml | 19 ++++++++ models/fixtures/action_run_job.yml | 43 +++++++++++++++++ models/fixtures/action_task.yml | 60 ++++++++++++++++++++++++ models/fixtures/action_task_output.yml | 20 ++++++++ routers/api/actions/runner/main_test.go | 14 ++++++ routers/api/actions/runner/utils.go | 60 +++++++++++++++++------- routers/api/actions/runner/utils_test.go | 28 +++++++++++ 8 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 models/fixtures/action_task_output.yml create mode 100644 routers/api/actions/runner/main_test.go create mode 100644 routers/api/actions/runner/utils_test.go diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 4b8664077dc..2319af8e085 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col if err != nil { return 0, err } - run.Status = aggregateJobStatus(jobs) + run.Status = AggregateJobStatus(jobs) if run.Started.IsZero() && run.Status.IsRunning() { run.Started = timeutil.TimeStampNow() } @@ -152,7 +152,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } -func aggregateJobStatus(jobs []*ActionRunJob) Status { +func AggregateJobStatus(jobs []*ActionRunJob) Status { allDone := true allWaiting := true hasFailure := false diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index a42ab77ca5b..0747c46d2f0 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -36,3 +36,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 793 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 189 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index fd90f4fd5d2..9b6f5b9a887 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -26,3 +26,46 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 194 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (1) + attempt: 1 + job_id: job1 + task_id: 49 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 195 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (2) + attempt: 1 + job_id: job1 + task_id: 50 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 196 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: [job1] + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index d88a8ed8a91..506a47d8a04 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -57,3 +57,63 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 49 + job_id: 194 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 50 + job_id: 195 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 51 + job_id: 196 + attempt: 1 + runner_id: 1 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 diff --git a/models/fixtures/action_task_output.yml b/models/fixtures/action_task_output.yml new file mode 100644 index 00000000000..314e9f7115b --- /dev/null +++ b/models/fixtures/action_task_output.yml @@ -0,0 +1,20 @@ +- + id: 1 + task_id: 49 + output_key: output_a + output_value: abc +- + id: 2 + task_id: 49 + output_key: output_b + output_value: '' +- + id: 3 + task_id: 50 + output_key: output_a + output_value: '' +- + id: 4 + task_id: 50 + output_key: output_b + output_value: bbb diff --git a/routers/api/actions/runner/main_test.go b/routers/api/actions/runner/main_test.go new file mode 100644 index 00000000000..1e80a4f5caf --- /dev/null +++ b/routers/api/actions/runner/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index ff6ec5bd54c..539be8d8890 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str return nil, fmt.Errorf("FindRunJobs: %w", err) } - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + jobIDJobs := make(map[string][]*actions_model.ActionRunJob) for _, job := range jobs { - if !needs.Contains(job.JobID) { + jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) + } + + ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + for jobID, jobsWithSameID := range jobIDJobs { + if !needs.Contains(jobID) { continue } - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue + var jobOutputs map[string]string + for _, job := range jobsWithSameID { + if job.TaskID == 0 || !job.Status.IsDone() { + // it shouldn't happen, or the job has been rerun + continue + } + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + if err != nil { + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + } + outputs := make(map[string]string, len(got)) + for _, v := range got { + outputs[v.OutputKey] = v.OutputValue + } + if len(jobOutputs) == 0 { + jobOutputs = outputs + } else { + jobOutputs = mergeTwoOutputs(outputs, jobOutputs) + } } - outputs := make(map[string]string) - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - ret[job.JobID] = &runnerv1.TaskNeed{ - Outputs: outputs, - Result: runnerv1.Result(job.Status), + ret[jobID] = &runnerv1.TaskNeed{ + Outputs: jobOutputs, + Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), } } return ret, nil } + +// mergeTwoOutputs merges two outputs from two different ActionRunJobs +// Values with the same output name may be overridden. The user should ensure the output names are unique. +// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job +func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { + ret := make(map[string]string, len(o1)) + for k1, v1 := range o1 { + if len(v1) > 0 { + ret[k1] = v1 + } else { + ret[k1] = o2[k1] + } + } + return ret +} diff --git a/routers/api/actions/runner/utils_test.go b/routers/api/actions/runner/utils_test.go new file mode 100644 index 00000000000..d7a6f84550f --- /dev/null +++ b/routers/api/actions/runner/utils_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_findTaskNeeds(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + + ret, err := findTaskNeeds(context.Background(), task) + assert.NoError(t, err) + assert.Len(t, ret, 1) + assert.Contains(t, ret, "job1") + assert.Len(t, ret["job1"].Outputs, 2) + assert.Equal(t, "abc", ret["job1"].Outputs["output_a"]) + assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"]) +} From 2ee4aa89989f50182d70fc0d0aec7a032a43debe Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sat, 14 Dec 2024 04:34:03 +0200 Subject: [PATCH 07/49] Upgrade htmx to 2.0.4 (#32834) Release notes: https://github.com/bigskysoftware/htmx/releases/tag/v2.0.4 Tested `Star`, `Watch`, and the admin dashboard page. All functionality remains unchanged. Signed-off-by: Yarden Shoham --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53bd5bc4f1f..4764282f65e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "2.0.3", + "htmx.org": "2.0.4", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.11", @@ -10557,9 +10557,9 @@ } }, "node_modules/htmx.org": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz", - "integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz", + "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==", "license": "0BSD" }, "node_modules/iconv-lite": { diff --git a/package.json b/package.json index 3a81e648228..275ca898e29 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "2.0.3", + "htmx.org": "2.0.4", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.11", From bed563e57490024e43db46ea2ca48094cd4fa7d2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Dec 2024 04:10:20 +0100 Subject: [PATCH 08/49] Improve JSX/TSX support in code editor (#32833) Two tweaks to Monaco to improve JSX/TSX support. 1. Certain language features like JSX/TSX only work when passing `uri` (containing the filename), do this. 2. Set the `jsx` compiler option to avoid error annotations Before: Screenshot 2024-12-13 at 15 11 33 After: Screenshot 2024-12-13 at 15 10 46 --- web_src/js/features/codeeditor.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 62bfccd1393..af9830a4db2 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -58,6 +58,12 @@ function initLanguages(monaco: Monaco): void { for (const extension of extensions || []) { languagesByExt[extension] = id; } + if (id === 'typescript') { + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + // this is needed to suppress error annotations in tsx regarding missing --jsx flag. + jsx: monaco.languages.typescript.JsxEmit.Preserve, + }); + } } } @@ -72,6 +78,8 @@ function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: s const language = model.getLanguageId(); const newLanguage = getLanguage(filename); if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); + // TODO: Need to update the model uri with the new filename, but there is no easy way currently, see + // https://github.com/microsoft/monaco-editor/discussions/3751 } // export editor for customization - https://github.com/go-gitea/gitea/issues/10409 @@ -135,10 +143,11 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri }); updateTheme(monaco); + const model = monaco.editor.createModel(textarea.value, language, monaco.Uri.file(filename)); + const editor = monaco.editor.create(container, { - value: textarea.value, + model, theme: 'gitea', - language, ...other, }); @@ -146,8 +155,6 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri {keybinding: monaco.KeyCode.Enter, command: null}, // disable enter from accepting code completion ]); - const model = editor.getModel(); - if (!model) throw new Error('Unable to get editor model'); model.onDidChangeContent(() => { textarea.value = editor.getValue({ preserveBOM: true, From 82c59d52ea650ce42bbca2c6740d9449d06e77be Mon Sep 17 00:00:00 2001 From: hiifong Date: Sat, 14 Dec 2024 11:35:19 +0800 Subject: [PATCH 09/49] Add User-Agent for gitea's self-implemented lfs client. (#32832) --- modules/lfs/shared.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/lfs/shared.go b/modules/lfs/shared.go index 40ad789c1d9..cd9488e3dbf 100644 --- a/modules/lfs/shared.go +++ b/modules/lfs/shared.go @@ -14,9 +14,12 @@ import ( const ( // MediaType contains the media type for LFS server requests MediaType = "application/vnd.git-lfs+json" - // Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served - AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8" - UserAgentHeader = "git-lfs" + // AcceptHeader Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served + AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8" + // UserAgentHeader Add User-Agent for gitea's self-implemented lfs client, + // and the version is consistent with the latest version of git lfs can be avoided incompatibilities. + // Some lfs servers will check this + UserAgentHeader = "git-lfs/3.6.0 (Gitea)" ) // BatchRequest contains multiple requests processed in one batch operation. From cc5ff98e0d510c1923ad7cabc3e339f9cf0b570f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 14 Dec 2024 13:43:05 +0800 Subject: [PATCH 10/49] Refactor markdown math render (#32831) Add more tests --- modules/markup/markdown/markdown.go | 28 +++--- modules/markup/markdown/markdown_math_test.go | 20 ++++- modules/markup/markdown/math/block_parser.go | 25 ++++-- .../markup/markdown/math/inline_block_node.go | 31 ------- modules/markup/markdown/math/inline_node.go | 2 +- modules/markup/markdown/math/inline_parser.go | 90 +++++++++++-------- .../markup/markdown/math/inline_renderer.go | 1 - modules/markup/markdown/math/math.go | 68 +++++--------- web_src/css/repo/home-file-list.css | 3 +- web_src/js/markup/math.ts | 20 +++-- 10 files changed, 137 insertions(+), 151 deletions(-) delete mode 100644 modules/markup/markdown/math/inline_block_node.go diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index f77db9eb38e..a14c0cad597 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -78,26 +78,23 @@ func (r *GlodmarkRender) Renderer() renderer.Renderer { func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { if entering { - language, _ := c.Language() - if language == nil { - language = []byte("text") - } + languageBytes, _ := c.Language() + languageStr := giteautil.IfZero(string(languageBytes), "text") - languageStr := string(language) - - preClasses := []string{"code-block"} + preClasses := "code-block" if languageStr == "mermaid" || languageStr == "math" { - preClasses = append(preClasses, "is-loading") + preClasses += " is-loading" } - err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
    `, strings.Join(preClasses, " "))
    +		err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
    `, preClasses)
     		if err != nil {
     			return
     		}
     
    -		// include language-x class as part of commonmark spec
    -		// the "display" class is used by "js/markup/math.js" to render the code element as a block
    -		err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, string(language))
    +		// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
    +		// the "display" class is used by "js/markup/math.ts" to render the code element as a block
    +		// the "math.ts" strictly depends on the structure: 
    ...
    + err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, languageStr) if err != nil { return } @@ -128,7 +125,12 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { ), highlighting.WithWrapperRenderer(r.highlightingRenderer), ), - math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)), + math.NewExtension(&ctx.RenderInternal, math.Options{ + Enabled: setting.Markdown.EnableMath, + ParseDollarInline: true, + ParseDollarBlock: true, + ParseSquareBlock: true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options) + }), meta.Meta, ), goldmark.WithParserOptions( diff --git a/modules/markup/markdown/markdown_math_test.go b/modules/markup/markdown/markdown_math_test.go index a2213b2ce76..813f050965a 100644 --- a/modules/markup/markdown/markdown_math_test.go +++ b/modules/markup/markdown/markdown_math_test.go @@ -12,8 +12,9 @@ import ( "github.com/stretchr/testify/assert" ) +const nl = "\n" + func TestMathRender(t *testing.T) { - const nl = "\n" testcases := []struct { testcase string expected string @@ -86,6 +87,18 @@ func TestMathRender(t *testing.T) { `$\text{$b$}$`, `

    \text{$b$}

    ` + nl, }, + { + "a$`b`$c", + `

    abc

    ` + nl, + }, + { + "a $`b`$ c", + `

    a b c

    ` + nl, + }, + { + "a$``b``$c x$```y```$z", + `

    abc xyz

    ` + nl, + }, } for _, test := range testcases { @@ -215,6 +228,11 @@ x
`, }, + { + "inline-non-math", + `\[x]`, + `

[x]

` + nl, + }, } for _, test := range testcases { diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go index 3f37ce83332..2c5553550a7 100644 --- a/modules/markup/markdown/math/block_parser.go +++ b/modules/markup/markdown/math/block_parser.go @@ -16,16 +16,18 @@ import ( type blockParser struct { parseDollars bool + parseSquare bool endBytesDollars []byte - endBytesBracket []byte + endBytesSquare []byte } // NewBlockParser creates a new math BlockParser -func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { +func NewBlockParser(parseDollars, parseSquare bool) parser.BlockParser { return &blockParser{ - parseDollars: parseDollarBlocks, + parseDollars: parseDollars, + parseSquare: parseSquare, endBytesDollars: []byte{'$', '$'}, - endBytesBracket: []byte{'\\', ']'}, + endBytesSquare: []byte{'\\', ']'}, } } @@ -40,7 +42,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex var dollars bool if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' { dollars = true - } else if line[pos] == '\\' && line[pos+1] == '[' { + } else if b.parseSquare && line[pos] == '\\' && line[pos+1] == '[' { if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) { // do not process escaped attention block: "> \[!NOTE\]" return nil, parser.NoChildren @@ -53,10 +55,10 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex node := NewBlock(dollars, pos) // Now we need to check if the ending block is on the segment... - endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesBracket) + endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesSquare) idx := bytes.Index(line[pos+2:], endBytes) if idx >= 0 { - // for case $$ ... $$ any other text + // for case: "$$ ... $$ any other text" (this case will be handled by the inline parser) for i := pos + 2 + idx + 2; i < len(line); i++ { if line[i] != ' ' && line[i] != '\n' { return nil, parser.NoChildren @@ -70,6 +72,13 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex return node, parser.Close | parser.NoChildren } + // for case "\[ ... ]" (no close marker on the same line) + for i := pos + 2 + idx + 2; i < len(line); i++ { + if line[i] != ' ' && line[i] != '\n' { + return nil, parser.NoChildren + } + } + segment.Start += pos + 2 node.Lines().Append(segment) return node, parser.NoChildren @@ -85,7 +94,7 @@ func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Cont line, segment := reader.PeekLine() w, pos := util.IndentWidth(line, reader.LineOffset()) if w < 4 { - endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesBracket) + endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesSquare) if bytes.HasPrefix(line[pos:], endBytes) && util.IsBlank(line[pos+len(endBytes):]) { if util.IsBlank(line[pos+len(endBytes):]) { newline := giteaUtil.Iif(line[len(line)-1] != '\n', 0, 1) diff --git a/modules/markup/markdown/math/inline_block_node.go b/modules/markup/markdown/math/inline_block_node.go deleted file mode 100644 index c92d0c8d84b..00000000000 --- a/modules/markup/markdown/math/inline_block_node.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package math - -import ( - "github.com/yuin/goldmark/ast" -) - -// InlineBlock represents inline math e.g. $$...$$ -type InlineBlock struct { - Inline -} - -// InlineBlock implements InlineBlock. -func (n *InlineBlock) InlineBlock() {} - -// KindInlineBlock is the kind for math inline block -var KindInlineBlock = ast.NewNodeKind("MathInlineBlock") - -// Kind returns KindInlineBlock -func (n *InlineBlock) Kind() ast.NodeKind { - return KindInlineBlock -} - -// NewInlineBlock creates a new ast math inline block node -func NewInlineBlock() *InlineBlock { - return &InlineBlock{ - Inline{}, - } -} diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go index 2221a251bf1..1e4034d54b9 100644 --- a/modules/markup/markdown/math/inline_node.go +++ b/modules/markup/markdown/math/inline_node.go @@ -8,7 +8,7 @@ import ( "github.com/yuin/goldmark/util" ) -// Inline represents inline math e.g. $...$ or \(...\) +// Inline struct represents inline math e.g. $...$ or \(...\) type Inline struct { ast.BaseInline } diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go index 191d1e5a315..a57abe9f9b0 100644 --- a/modules/markup/markdown/math/inline_parser.go +++ b/modules/markup/markdown/math/inline_parser.go @@ -12,31 +12,25 @@ import ( ) type inlineParser struct { - start []byte - end []byte + trigger []byte + endBytesSingleDollar []byte + endBytesDoubleDollar []byte + endBytesBracket []byte } var defaultInlineDollarParser = &inlineParser{ - start: []byte{'$'}, - end: []byte{'$'}, -} - -var defaultDualDollarParser = &inlineParser{ - start: []byte{'$', '$'}, - end: []byte{'$', '$'}, + trigger: []byte{'$'}, + endBytesSingleDollar: []byte{'$'}, + endBytesDoubleDollar: []byte{'$', '$'}, } func NewInlineDollarParser() parser.InlineParser { return defaultInlineDollarParser } -func NewInlineDualDollarParser() parser.InlineParser { - return defaultDualDollarParser -} - var defaultInlineBracketParser = &inlineParser{ - start: []byte{'\\', '('}, - end: []byte{'\\', ')'}, + trigger: []byte{'\\', '('}, + endBytesBracket: []byte{'\\', ')'}, } func NewInlineBracketParser() parser.InlineParser { @@ -45,7 +39,7 @@ func NewInlineBracketParser() parser.InlineParser { // Trigger triggers this parser on $ or \ func (parser *inlineParser) Trigger() []byte { - return parser.start + return parser.trigger } func isPunctuation(b byte) bool { @@ -64,33 +58,60 @@ func isAlphanumeric(b byte) bool { func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { line, _ := block.PeekLine() - if !bytes.HasPrefix(line, parser.start) { + if !bytes.HasPrefix(line, parser.trigger) { // We'll catch this one on the next time round return nil } - precedingCharacter := block.PrecendingCharacter() - if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { - // need to exclude things like `a$` from being considered a start - return nil + var startMarkLen int + var stopMark []byte + checkSurrounding := true + if line[0] == '$' { + startMarkLen = 1 + stopMark = parser.endBytesSingleDollar + if len(line) > 1 { + if line[1] == '$' { + startMarkLen = 2 + stopMark = parser.endBytesDoubleDollar + } else if line[1] == '`' { + pos := 1 + for ; pos < len(line) && line[pos] == '`'; pos++ { + } + startMarkLen = pos + stopMark = bytes.Repeat([]byte{'`'}, pos) + stopMark[len(stopMark)-1] = '$' + checkSurrounding = false + } + } + } else { + startMarkLen = 2 + stopMark = parser.endBytesBracket + } + + if checkSurrounding { + precedingCharacter := block.PrecendingCharacter() + if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { + // need to exclude things like `a$` from being considered a start + return nil + } } // move the opener marker point at the start of the text - opener := len(parser.start) + opener := startMarkLen // Now look for an ending line depth := 0 ender := -1 for i := opener; i < len(line); i++ { - if depth == 0 && bytes.HasPrefix(line[i:], parser.end) { + if depth == 0 && bytes.HasPrefix(line[i:], stopMark) { succeedingCharacter := byte(0) - if i+len(parser.end) < len(line) { - succeedingCharacter = line[i+len(parser.end)] + if i+len(stopMark) < len(line) { + succeedingCharacter = line[i+len(stopMark)] } // check valid ending character isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) || succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 - if !isValidEndingChar { + if checkSurrounding && !isValidEndingChar { break } ender = i @@ -112,21 +133,12 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. block.Advance(opener) _, pos := block.Position() - var node ast.Node - if parser == defaultDualDollarParser { - node = NewInlineBlock() - } else { - node = NewInline() - } + node := NewInline() + segment := pos.WithStop(pos.Start + ender - opener) node.AppendChild(node, ast.NewRawTextSegment(segment)) - block.Advance(ender - opener + len(parser.end)) - - if parser == defaultDualDollarParser { - trimBlock(&(node.(*InlineBlock)).Inline, block) - } else { - trimBlock(node.(*Inline), block) - } + block.Advance(ender - opener + len(stopMark)) + trimBlock(node, block) return node } diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go index 4e0531cf404..d000a7b317a 100644 --- a/modules/markup/markdown/math/inline_renderer.go +++ b/modules/markup/markdown/math/inline_renderer.go @@ -50,5 +50,4 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod // RegisterFuncs registers the renderer for inline math nodes func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindInline, r.renderInline) - reg.Register(KindInlineBlock, r.renderInline) } diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go index 7e8defcd4a1..a6ff593d626 100644 --- a/modules/markup/markdown/math/math.go +++ b/modules/markup/markdown/math/math.go @@ -5,6 +5,7 @@ package math import ( "code.gitea.io/gitea/modules/markup/internal" + giteaUtil "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" @@ -12,70 +13,45 @@ import ( "github.com/yuin/goldmark/util" ) +type Options struct { + Enabled bool + ParseDollarInline bool + ParseDollarBlock bool + ParseSquareBlock bool +} + // Extension is a math extension type Extension struct { - renderInternal *internal.RenderInternal - enabled bool - parseDollarInline bool - parseDollarBlock bool -} - -// Option is the interface Options should implement -type Option interface { - SetOption(e *Extension) -} - -type extensionFunc func(e *Extension) - -func (fn extensionFunc) SetOption(e *Extension) { - fn(e) -} - -// Enabled enables or disables this extension -func Enabled(enable ...bool) Option { - value := true - if len(enable) > 0 { - value = enable[0] - } - return extensionFunc(func(e *Extension) { - e.enabled = value - }) + renderInternal *internal.RenderInternal + options Options } // NewExtension creates a new math extension with the provided options -func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension { +func NewExtension(renderInternal *internal.RenderInternal, opts ...Options) *Extension { + opt := giteaUtil.OptionalArg(opts) r := &Extension{ - renderInternal: renderInternal, - enabled: true, - parseDollarBlock: true, - parseDollarInline: true, - } - - for _, o := range opts { - o.SetOption(r) + renderInternal: renderInternal, + options: opt, } return r } // Extend extends goldmark with our parsers and renderers func (e *Extension) Extend(m goldmark.Markdown) { - if !e.enabled { + if !e.options.Enabled { return } - m.Parser().AddOptions(parser.WithBlockParsers( - util.Prioritized(NewBlockParser(e.parseDollarBlock), 701), - )) - - inlines := []util.PrioritizedValue{ - util.Prioritized(NewInlineBracketParser(), 501), - } - if e.parseDollarInline { - inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 503), - util.Prioritized(NewInlineDualDollarParser(), 502)) + inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)} + if e.options.ParseDollarInline { + inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502)) } m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) + m.Parser().AddOptions(parser.WithBlockParsers( + util.Prioritized(NewBlockParser(e.options.ParseDollarBlock, e.options.ParseSquareBlock), 701), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(NewBlockRenderer(e.renderInternal), 501), util.Prioritized(NewInlineRenderer(e.renderInternal), 502), diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index ecb26fa6629..285b823d57a 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -29,7 +29,7 @@ #repo-files-table .repo-file-line, #repo-files-table .repo-file-cell { border-top: 1px solid var(--color-light-border); - padding: 6px 10px; + padding: 8px 10px; } #repo-files-table .repo-file-line:first-child { @@ -41,7 +41,6 @@ display: flex; align-items: center; gap: 0.5em; - padding: 6px 10px; } #repo-files-table .repo-file-last-commit { diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index 22a4de38e98..4777805e3ca 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -1,8 +1,14 @@ import {displayError} from './common.ts'; -function targetElement(el: Element) { +function targetElement(el: Element): {target: Element, displayAsBlock: boolean} { // The target element is either the parent "code block with loading indicator", or itself - return el.closest('.code-block.is-loading') ?? el; + // It is designed to work for 2 cases (guaranteed by backend code): + // *
...
+ // * ... + return { + target: el.closest('.code-block.is-loading') ?? el, + displayAsBlock: el.classList.contains('display'), + }; } export async function renderMath(): Promise { @@ -19,7 +25,7 @@ export async function renderMath(): Promise { const MAX_EXPAND = 1000; for (const el of els) { - const target = targetElement(el); + const {target, displayAsBlock} = targetElement(el); if (target.hasAttribute('data-render-done')) continue; const source = el.textContent; @@ -27,16 +33,12 @@ export async function renderMath(): Promise { displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); continue; } - - const displayMode = el.classList.contains('display'); - const nodeName = displayMode ? 'p' : 'span'; - try { - const tempEl = document.createElement(nodeName); + const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); katex.render(source, tempEl, { maxSize: MAX_SIZE, maxExpand: MAX_EXPAND, - displayMode, + displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode }); target.replaceWith(tempEl); } catch (error) { From 1a07ebe5492d0c07bfc474441502362f678dba5a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Dec 2024 07:50:12 +0100 Subject: [PATCH 11/49] Fix overflow on org header (#32837) --- templates/org/header.tmpl | 4 ++-- web_src/css/org.css | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 7361df99eaf..80519361fdb 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -1,6 +1,6 @@
{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}} -
+
{{.Org.DisplayName}} @@ -18,7 +18,7 @@ {{end}}
- {{if .RenderedDescription}}
{{.RenderedDescription}}
{{end}} + {{if .RenderedDescription}}
{{.RenderedDescription}}
{{end}}
{{if .Org.Location}}
{{svg "octicon-location"}} {{.Org.Location}}
{{end}} {{if .Org.Website}}
{{svg "octicon-link"}} {{.Org.Website}}
{{end}} diff --git a/web_src/css/org.css b/web_src/css/org.css index 90e5d7ad0e8..10826250419 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -93,11 +93,6 @@ margin-right: 15px; } -.page-content.organization #org-info { - overflow-wrap: anywhere; - flex: 1; -} - .page-content.organization #org-info .ui.header { display: flex; align-items: center; From 32059158da93dc85eff4a0e9bacb474adf7a46a2 Mon Sep 17 00:00:00 2001 From: mevius4 Date: Sun, 15 Dec 2024 10:41:36 +0900 Subject: [PATCH 12/49] Fix SSPI button visibility when SSPI is the only enabled method (#32841) --- templates/user/auth/signin_inner.tmpl | 2 +- templates/user/auth/signup_inner.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 3124048d36e..9daa051e061 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -48,7 +48,7 @@
{{end}}{{/*if .EnablePasswordSignInForm*/}} - {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}} + {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}} {{if and $showOAuth2Methods .EnablePasswordSignInForm}}
{{ctx.Locale.Tr "sign_in_or"}}
{{end}} diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index 41d0cd49b52..ea8d0bafe40 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -47,7 +47,7 @@
{{end}} - {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}} + {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}} {{if $showOAuth2Methods}}
{{ctx.Locale.Tr "sign_in_or"}}
{{template "user/auth/oauth_container" .}} From 7616aeb2ea2a02c15480dcd4a232e98081569690 Mon Sep 17 00:00:00 2001 From: hiifong Date: Sun, 15 Dec 2024 10:06:21 +0800 Subject: [PATCH 13/49] In some lfs server implementations, they require the ref attribute. (#32838) Fix: #32611 In some lfs server implementations, they require the ref attribute. --------- Co-authored-by: wxiaoguang --- modules/lfs/http_client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 50f0e7a8d88..3acd23b8f73 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -72,7 +72,10 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin url := fmt.Sprintf("%s/objects/batch", c.endpoint) - request := &BatchRequest{operation, c.transferNames(), nil, objects} + // `ref` is an "optional object describing the server ref that the objects belong to" + // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones. + // https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37 + request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects} payload := new(bytes.Buffer) err := json.NewEncoder(payload).Encode(request) if err != nil { From 1cfb718976e2db517da0e76bda835edd9df1fabd Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Dec 2024 03:31:07 +0100 Subject: [PATCH 14/49] Update golangci-lint to v1.62.2, fix issues (#32845) Update it and fix new issues related to `redefines-builtin-id` --- Makefile | 2 +- models/issues/issue_index.go | 6 +++--- modules/auth/password/password.go | 4 ++-- modules/git/repo_commit.go | 10 +++++----- modules/graceful/manager.go | 6 +++--- modules/indexer/internal/bleve/query.go | 10 +++++----- modules/indexer/internal/paginator.go | 6 +++--- modules/log/event_format.go | 4 ++-- modules/references/references.go | 6 +++--- modules/templates/util_date.go | 6 +++--- modules/templates/util_string.go | 4 ++-- routers/api/v1/repo/issue_dependency.go | 4 ++-- routers/api/v1/repo/wiki.go | 4 ++-- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 0cd6936b3bb..d5b779f1e59 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ XGO_VERSION := go-1.23.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go index 16274d0ef09..2eb61858bfc 100644 --- a/models/issues/issue_index.go +++ b/models/issues/issue_index.go @@ -18,12 +18,12 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { } defer committer.Close() - var max int64 - if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + var maxIndex int64 + if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&maxIndex); err != nil { return err } - if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, max); err != nil { + if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, maxIndex); err != nil { return err } diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 85f9780709c..c66b62937fd 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -99,10 +99,10 @@ func IsComplexEnough(pwd string) bool { func Generate(n int) (string, error) { NewComplexity() buffer := make([]byte, n) - max := big.NewInt(int64(len(validChars))) + maxInt := big.NewInt(int64(len(validChars))) for { for j := 0; j < n; j++ { - rnd, err := rand.Int(rand.Reader, max) + rnd, err := rand.Int(rand.Reader, maxInt) if err != nil { return "", err } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9405634df12..9ffadb833d8 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -465,15 +465,15 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ refs := strings.Split(stdout, "\n") - var max int + var maxNum int if len(refs) > limit { - max = limit + maxNum = limit } else { - max = len(refs) - 1 + maxNum = len(refs) - 1 } - branches := make([]string, max) - for i, ref := range refs[:max] { + branches := make([]string, maxNum) + for i, ref := range refs[:maxNum] { parts := strings.Fields(ref) branches[i] = parts[len(parts)-1] diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 3f1115066a0..991b2f2b7af 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -218,13 +218,13 @@ func (g *Manager) ServerDone() { g.runningServerWaitGroup.Done() } -func (g *Manager) setStateTransition(old, new state) bool { +func (g *Manager) setStateTransition(oldState, newState state) bool { g.lock.Lock() - if g.state != old { + if g.state != oldState { g.lock.Unlock() return false } - g.state = new + g.state = newState g.lock.Unlock() return true } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index 21422b281c4..1b18ca1a779 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -35,18 +35,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery { return q } -func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery { +func NumericRangeInclusiveQuery(minOption, maxOption optional.Option[int64], field string) *query.NumericRangeQuery { var minF, maxF *float64 var minI, maxI *bool - if min.Has() { + if minOption.Has() { minF = new(float64) - *minF = float64(min.Value()) + *minF = float64(minOption.Value()) minI = new(bool) *minI = true } - if max.Has() { + if maxOption.Has() { maxF = new(float64) - *maxF = float64(max.Value()) + *maxF = float64(maxOption.Value()) maxI = new(bool) *maxI = true } diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go index ee204bf0471..f1e19740eb7 100644 --- a/modules/indexer/internal/paginator.go +++ b/modules/indexer/internal/paginator.go @@ -10,12 +10,12 @@ import ( ) // ParsePaginator parses a db.Paginator into a skip and limit -func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) { +func ParsePaginator(paginator *db.ListOptions, maxNums ...int) (int, int) { // Use a very large number to indicate no limit unlimited := math.MaxInt32 - if len(max) > 0 { + if len(maxNums) > 0 { // Some indexer engines have a limit on the page size, respect that - unlimited = max[0] + unlimited = maxNums[0] } if paginator == nil || paginator.IsListAll() { diff --git a/modules/log/event_format.go b/modules/log/event_format.go index d9dbebf8315..0b8d1cec791 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -110,10 +110,10 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms buf = append(buf, ' ') } if flags&(Ltime|Lmicroseconds) != 0 { - hour, min, sec := t.Clock() + hour, minNum, sec := t.Clock() buf = itoa(buf, hour, 2) buf = append(buf, ':') - buf = itoa(buf, min, 2) + buf = itoa(buf, minNum, 2) buf = append(buf, ':') buf = itoa(buf, sec, 2) if flags&Lmicroseconds != 0 { diff --git a/modules/references/references.go b/modules/references/references.go index 2889430bcf6..6e549cb8758 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -164,9 +164,9 @@ func newKeywords() { }) } -func doNewKeywords(close, reopen []string) { - issueCloseKeywordsPat = makeKeywordsPat(close) - issueReopenKeywordsPat = makeKeywordsPat(reopen) +func doNewKeywords(closeKeywords, reopenKeywords []string) { + issueCloseKeywordsPat = makeKeywordsPat(closeKeywords) + issueReopenKeywordsPat = makeKeywordsPat(reopenKeywords) } // getGiteaHostName returns a normalized string with the local host name, with no scheme or port information diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index 66f83d23fe5..658691ee40c 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -53,8 +53,8 @@ func parseLegacy(datetime string) time.Time { return t } -func anyToTime(any any) (t time.Time, isZero bool) { - switch v := any.(type) { +func anyToTime(value any) (t time.Time, isZero bool) { + switch v := value.(type) { case nil: // it is zero case *time.Time: @@ -72,7 +72,7 @@ func anyToTime(any any) (t time.Time, isZero bool) { case int64: t = timeutil.TimeStamp(v).AsTime() default: - panic(fmt.Sprintf("Unsupported time type %T", any)) + panic(fmt.Sprintf("Unsupported time type %T", value)) } return t, t.IsZero() || t.Unix() == 0 } diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 479b755da1c..2ae27d08336 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -53,8 +53,8 @@ func (su *StringUtils) Cut(s, sep string) []any { return []any{before, after, found} } -func (su *StringUtils) EllipsisString(s string, max int) string { - return base.EllipsisString(s, max) +func (su *StringUtils) EllipsisString(s string, maxLength int) string { + return base.EllipsisString(s, maxLength) } func (su *StringUtils) ToUpper(s string) string { diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 712c71a6823..ae7502c661b 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -338,7 +338,7 @@ func GetIssueBlocks(ctx *context.APIContext) { } skip := (page - 1) * limit - max := page * limit + maxNum := page * limit deps, err := issue.BlockingDependencies(ctx) if err != nil { @@ -352,7 +352,7 @@ func GetIssueBlocks(ctx *context.APIContext) { repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission for i, depMeta := range deps { - if i < skip || i >= max { + if i < skip || i >= maxNum { continue } diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index c7065c1d9df..f9906ed250d 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -308,7 +308,7 @@ func ListWikiPages(ctx *context.APIContext) { } skip := (page - 1) * limit - max := page * limit + maxNum := page * limit entries, err := commit.ListEntries() if err != nil { @@ -317,7 +317,7 @@ func ListWikiPages(ctx *context.APIContext) { } pages := make([]*api.WikiPageMetaData, 0, len(entries)) for i, entry := range entries { - if i < skip || i >= max || !entry.IsRegular() { + if i < skip || i >= maxNum || !entry.IsRegular() { continue } c, err := wikiRepo.GetCommitByPath(entry.Name()) From d1c1e3cbccf88a9180e9a130a81342e0acb87fde Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Dec 2024 14:07:50 +0800 Subject: [PATCH 15/49] Fine tune ssh related comments and code (#32846) Add more comments to explain the ssh problem, and rename `sshConn` to `sshSession` --- modules/ssh/ssh.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 6d0695ee163..7479cfbd95a 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -13,7 +13,6 @@ import ( "errors" "fmt" "io" - "maps" "net" "os" "os/exec" @@ -49,6 +48,10 @@ import ( // Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one. // Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key), // then only A succeeds to authenticate, sessionHandler will see B's keyID +// +// After x/crypto >= 0.31.0 (fix CVE-2024-45337), the PublicKeyCallback will be called again for the verified key, +// it mitigates the misuse for most cases, it's still good for us to make sure we don't rely on that mitigation +// and do not misuse the PublicKeyCallback: we should only use the verified keyID from the verified ssh conn. const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id" @@ -100,8 +103,8 @@ func ptr[T any](intf any) *T { func sessionHandler(session ssh.Session) { // here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one. // so we must use the original ssh conn, which always contains the correct (verified) keyID. - sshConn := ptr[sessionPartial](session) - keyID := sshConn.conn.Permissions.Extensions[giteaPermissionExtensionKeyID] + sshSession := ptr[sessionPartial](session) + keyID := sshSession.conn.Permissions.Extensions[giteaPermissionExtensionKeyID] command := session.RawCommand() @@ -210,10 +213,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { // first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does) // it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions" - oldCtxPerm := ctx.Permissions().Permissions ctx.Permissions().Permissions = &gossh.Permissions{} - ctx.Permissions().Permissions.CriticalOptions = maps.Clone(oldCtxPerm.CriticalOptions) - setPermExt := func(keyID int64) { ctx.Permissions().Permissions.Extensions = map[string]string{ giteaPermissionExtensionKeyID: fmt.Sprint(keyID), From 92648112175753c4eaa13cc6f8e3cc6a3cef9ad5 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 14 Dec 2024 23:44:13 -0800 Subject: [PATCH 16/49] Remove translation to issue add time because the format is fixed should not be translated (#32850) The input content should always be `1h 2m 3s` and will be the same on different UI languages. So the translation is wrong. --- options/locale/locale_en-US.ini | 1 - templates/repo/issue/sidebar/stopwatch_timetracker.tmpl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f50ad1f2981..74ba70b8c8f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1680,7 +1680,6 @@ issues.timetracker_timer_stop = Stop timer issues.timetracker_timer_discard = Discard timer issues.timetracker_timer_manually_add = Add Time -issues.time_estimate_placeholder = 1h 2m issues.time_estimate_set = Set estimated time issues.time_estimate_display = Estimate: %s issues.change_time_estimate_at = changed time estimate to %s %s diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl index 1acf56d7b20..f107dc5ef5a 100644 --- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl +++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl @@ -44,7 +44,7 @@
{{$.CsrfTokenHtml}} - +
From df9a78cd04e364264c103cf3a92d94179cc1dd4f Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Dec 2024 11:01:46 +0100 Subject: [PATCH 17/49] Tweak repo sidebar (#32847) Before and after: Screenshot 2024-12-15 at 04 53 53 Screenshot 2024-12-15 at 04 53 41 Diff without whitespace: https://github.com/go-gitea/gitea/pull/32847/files?diff=unified&w=1 The `tw-mt-2` is fine even if the element renders empty: image --------- Co-authored-by: wxiaoguang --- options/locale/locale_en-US.ini | 1 + templates/repo/home.tmpl | 6 +- templates/repo/home_sidebar_bottom.tmpl | 94 +++++++++--------- templates/repo/home_sidebar_top.tmpl | 124 ++++++++++++------------ templates/repo/release/label.tmpl | 14 +++ templates/repo/release/list.tmpl | 8 +- web_src/css/repo.css | 5 - web_src/css/repo/home.css | 14 +-- web_src/js/features/repo-home.ts | 6 +- 9 files changed, 141 insertions(+), 131 deletions(-) create mode 100644 templates/repo/release/label.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 74ba70b8c8f..92ce4f2db9f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2632,6 +2632,7 @@ release.new_release = New Release release.draft = Draft release.prerelease = Pre-Release release.stable = Stable +release.latest = Latest release.compare = Compare release.edit = edit release.ahead.commits = %d commits diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 4e6d375b517..d73b7470bc5 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -18,7 +18,7 @@ {{$treeNamesLen := len .TreeNames}} {{$isTreePathRoot := eq $treeNamesLen 0}} - {{$showSidebar := $isTreePathRoot}} + {{$showSidebar := and $isTreePathRoot (not .HideRepoInfo) (not .IsBlame)}}
{{template "repo/sub_menu" .}} @@ -130,8 +130,8 @@
{{if $showSidebar}} -
{{template "repo/home_sidebar_top" .}}
-
{{template "repo/home_sidebar_bottom" .}}
+ {{template "repo/home_sidebar_top" .}} + {{template "repo/home_sidebar_bottom" .}} {{end}}
diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl index 57b4a95ddcc..f780dc122d5 100644 --- a/templates/repo/home_sidebar_bottom.tmpl +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -1,59 +1,61 @@ -
- {{if .LatestRelease}} -
-
- -
-
- {{svg "octicon-tag" 16}} +
+
+ {{if .LatestRelease}} +
+
+ -
-
-
- {{.LatestRelease.Title}} - {{ctx.Locale.Tr "latest"}} -
+
+
+ {{svg "octicon-tag" 16}}
-
- {{DateUtils.TimeSince .LatestRelease.CreatedUnix}} +
+
+
+ {{.LatestRelease.Title}} + {{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}} +
+
+
+ {{DateUtils.TimeSince .LatestRelease.CreatedUnix}} +
-
- {{end}} + {{end}} - {{if and (not .IsEmptyRepo) .LanguageStats}} -
-
-
- {{ctx.Locale.Tr "repo.repo_lang"}} -
- -
-
- {{range .LanguageStats}} -
- {{end}} + {{if and (not .IsEmptyRepo) .LanguageStats}} +
+
+
+ {{ctx.Locale.Tr "repo.repo_lang"}}
-
- {{range .LanguageStats}} -
- - - {{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}} - - {{.Percentage}}% -
- {{end}} + +
+
+ {{range .LanguageStats}} +
+ {{end}} +
+
+ {{range .LanguageStats}} +
+ + + {{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}} + + {{.Percentage}}% +
+ {{end}} +
+ {{end}}
- {{end}}
diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl index 4b0ebcd3906..607dc62e2e7 100644 --- a/templates/repo/home_sidebar_top.tmpl +++ b/templates/repo/home_sidebar_top.tmpl @@ -1,68 +1,70 @@ - -
- - {{template "shared/search/button"}} -
- +
+
+
+ {{template "shared/search/button"}} +
+
-
-
-
-
- {{ctx.Locale.Tr "repo.repo_desc"}} -
- {{if and (not .HideRepoInfo) (not .IsBlame)}} -
- {{- $description := .Repository.DescriptionHTML ctx -}} - {{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}} - {{if .Repository.Website}}{{svg "octicon-link"}}{{.Repository.Website}}{{end}} -
-
- {{/* !!!! it SHOULD and MUST match the code in issue-home.js */}} - {{range .Topics}}{{.Name}}{{end}} -
- {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} - - {{end}} - {{end}} - {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} -
- diff --git a/templates/repo/release/label.tmpl b/templates/repo/release/label.tmpl new file mode 100644 index 00000000000..eacb3e36f4c --- /dev/null +++ b/templates/repo/release/label.tmpl @@ -0,0 +1,14 @@ +{{/* +Template Attributes: +* Release: the release +* IsLatest: boolean indicating whether this is the latest release, optional +*/}} +{{if .IsLatest}} + {{ctx.Locale.Tr "repo.release.latest"}} +{{else if .Release.IsDraft}} + {{ctx.Locale.Tr "repo.release.draft"}} +{{else if .Release.IsPrerelease}} + {{ctx.Locale.Tr "repo.release.prerelease"}} +{{else if (not .Release.IsTag)}} + {{ctx.Locale.Tr "repo.release.stable"}} +{{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index efaac4432a6..99934d21182 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -33,13 +33,7 @@

{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}{{$release.Title}}{{end}} {{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}} - {{if $release.IsDraft}} - {{ctx.Locale.Tr "repo.release.draft"}} - {{else if $release.IsPrerelease}} - {{ctx.Locale.Tr "repo.release.prerelease"}} - {{else if (not $release.IsTag)}} - {{ctx.Locale.Tr "repo.release.stable"}} - {{end}} + {{template "repo/release/label" (dict "Release" $release)}}

{{if and $.CanCreateRelease (not $.PageIsSingleTag)}} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 9a43e10e826..6fdc9ec2a88 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -101,11 +101,6 @@ margin-bottom: 12px; } -.repository .repo-description { - font-size: 16px; - margin-bottom: 5px; -} - .commit-summary { flex: 1; overflow-wrap: anywhere; diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index ca5b432804f..65005e22635 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -4,22 +4,24 @@ grid-template-rows: auto auto 1fr; } -.repo-grid-filelist-sidebar .repo-home-filelist { +.repo-home-filelist { min-width: 0; grid-column: 1; grid-row: 1 / 4; } -.repo-grid-filelist-sidebar .repo-home-sidebar-top { +.repo-home-sidebar-top { grid-column: 2; grid-row: 1; padding-left: 1em; } -.repo-grid-filelist-sidebar .repo-home-sidebar-bottom { + +.repo-home-sidebar-bottom { grid-column: 2; grid-row: 2; padding-left: 1em; } + .repo-home-sidebar-bottom .flex-list > :first-child { border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */ } @@ -29,16 +31,16 @@ grid-template-columns: 100%; grid-template-rows: auto auto auto; } - .repo-grid-filelist-sidebar .repo-home-filelist { + .repo-home-filelist { grid-column: 1; grid-row: 2; } - .repo-grid-filelist-sidebar .repo-home-sidebar-top { + .repo-home-sidebar-top { grid-column: 1; grid-row: 1; padding-left: 0; } - .repo-grid-filelist-sidebar .repo-home-sidebar-bottom { + .repo-home-sidebar-bottom { grid-column: 1; grid-row: 3; padding-left: 0; diff --git a/web_src/js/features/repo-home.ts b/web_src/js/features/repo-home.ts index 4c69a004344..abda29cc52e 100644 --- a/web_src/js/features/repo-home.ts +++ b/web_src/js/features/repo-home.ts @@ -16,7 +16,7 @@ export function initRepoTopicBar() { let lastErrorToast: Toast; mgrBtn.addEventListener('click', () => { - hideElem(viewDiv); + hideElem([viewDiv, mgrBtn]); showElem(editDiv); topicDropdown.querySelector('input.search').focus(); }); @@ -24,7 +24,7 @@ export function initRepoTopicBar() { document.querySelector('#cancel_topic_edit').addEventListener('click', () => { lastErrorToast?.hideToast(); hideElem(editDiv); - showElem(viewDiv); + showElem([viewDiv, mgrBtn]); mgrBtn.focus(); }); @@ -55,7 +55,7 @@ export function initRepoTopicBar() { } } hideElem(editDiv); - showElem(viewDiv); + showElem([viewDiv, mgrBtn]); } } else if (response.status === 422) { // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save" From 33e8e82c4b528db8efb1cf9fc4dbab2d9e21ace8 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sun, 15 Dec 2024 11:41:29 +0100 Subject: [PATCH 18/49] Enable tenv and testifylint rules (#32852) Enables tenv and testifylint linters closes: https://github.com/go-gitea/gitea/issues/32842 --- .golangci.yml | 6 +++ models/actions/runner_token_test.go | 6 +-- models/activities/user_heatmap_test.go | 5 +-- models/auth/oauth2_test.go | 4 +- models/db/iterate_test.go | 2 - models/git/commit_status_test.go | 4 +- models/git/protected_branch_test.go | 3 +- models/issues/comment_test.go | 2 +- models/issues/issue_test.go | 2 +- models/issues/issue_watch_test.go | 6 +-- models/issues/label_test.go | 8 ++-- models/issues/milestone_test.go | 2 +- models/issues/pull_list_test.go | 2 +- models/issues/pull_test.go | 2 +- models/issues/stopwatch_test.go | 2 +- models/issues/tracked_time_test.go | 8 ++-- models/migrations/v1_16/v193_test.go | 8 ++-- models/migrations/v1_22/v286_test.go | 4 +- models/migrations/v1_22/v294_test.go | 2 +- models/organization/org_list_test.go | 2 +- models/organization/org_test.go | 2 +- models/perm/access_mode_test.go | 2 +- models/project/column_test.go | 5 +-- models/repo/repo_test.go | 12 +++--- models/repo/star_test.go | 4 +- models/repo/user_repo_test.go | 2 +- models/repo/watch_test.go | 4 +- models/unittest/unit_tests.go | 2 +- models/user/email_address_test.go | 3 +- models/user/setting_test.go | 2 +- models/user/user_test.go | 18 ++++----- models/webhook/webhook_test.go | 4 +- modules/activitypub/client_test.go | 5 +-- modules/assetfs/layered_test.go | 2 +- modules/auth/pam/pam_test.go | 2 +- modules/auth/password/hash/dummy_test.go | 2 +- modules/base/tool_test.go | 7 ++-- modules/dump/dumper_test.go | 2 +- modules/git/commit_sha256_test.go | 2 +- modules/git/commit_test.go | 6 +-- modules/git/grep_test.go | 4 +- modules/git/parse_nogogit_test.go | 2 +- modules/git/repo_branch_test.go | 4 +- modules/git/repo_compare_test.go | 4 +- .../indexer/issues/internal/tests/tests.go | 38 +++++++++---------- modules/lfs/transferadapter_test.go | 6 +-- modules/log/logger_test.go | 4 +- modules/markup/html_internal_test.go | 4 +- .../packages/conan/conanfile_parser_test.go | 2 +- .../packages/conan/conaninfo_parser_test.go | 2 +- modules/queue/base_test.go | 6 +-- modules/queue/workerqueue_test.go | 4 +- modules/references/references_test.go | 2 +- modules/repository/repo_test.go | 6 +-- modules/setting/cron_test.go | 2 +- modules/setting/oauth2_test.go | 2 +- modules/setting/storage_test.go | 10 ++--- modules/user/user_test.go | 3 +- modules/util/color_test.go | 6 +-- modules/util/keypair_test.go | 5 +-- modules/util/time_str_test.go | 4 +- modules/util/util_test.go | 16 ++++---- routers/private/hook_post_receive_test.go | 2 +- routers/web/repo/wiki_test.go | 4 +- services/actions/auth_test.go | 12 +++--- services/auth/oauth2_test.go | 2 +- .../auth/source/oauth2/source_sync_test.go | 8 ++-- services/convert/pull_review_test.go | 2 +- services/cron/tasks_test.go | 2 +- services/feed/feed_test.go | 6 +-- services/gitdiff/gitdiff_test.go | 5 +-- services/migrations/gitlab_test.go | 2 +- services/org/team_test.go | 2 +- services/pull/reviewer_test.go | 4 +- services/release/release_test.go | 2 +- services/repository/archiver/archiver_test.go | 5 +-- services/repository/license_test.go | 2 +- services/repository/transfer_test.go | 2 +- services/webhook/packagist_test.go | 30 +++++++-------- services/webhook/webhook_test.go | 4 +- .../integration/api_actions_artifact_test.go | 4 +- tests/integration/api_branch_test.go | 4 +- tests/integration/api_issue_config_test.go | 6 +-- tests/integration/api_issue_pin_test.go | 4 +- tests/integration/api_issue_stopwatch_test.go | 2 +- tests/integration/api_issue_test.go | 2 +- tests/integration/api_keys_test.go | 6 +-- tests/integration/api_notification_test.go | 4 +- tests/integration/api_oauth2_apps_test.go | 14 +++---- tests/integration/api_packages_npm_test.go | 2 +- tests/integration/api_pull_test.go | 6 +-- .../integration/api_repo_git_commits_test.go | 10 ++--- tests/integration/api_repo_lfs_locks_test.go | 2 +- tests/integration/api_repo_teams_test.go | 2 +- tests/integration/api_user_orgs_test.go | 2 +- tests/integration/api_user_search_test.go | 8 ++-- tests/integration/auth_ldap_test.go | 2 +- .../git_helper_for_declarative_test.go | 6 +-- tests/integration/git_push_test.go | 3 +- tests/integration/gpg_git_test.go | 5 +-- tests/integration/integration_test.go | 10 ++--- .../migration-test/migration_test.go | 2 +- tests/integration/mirror_push_test.go | 2 +- tests/integration/oauth_test.go | 22 +++++------ tests/integration/org_count_test.go | 2 +- tests/integration/pull_compare_test.go | 6 +-- tests/integration/pull_merge_test.go | 2 +- tests/integration/repo_archive_test.go | 2 +- tests/integration/repo_fork_test.go | 2 +- tests/integration/repo_generate_test.go | 2 +- tests/integration/repo_test.go | 2 +- tests/integration/session_test.go | 4 +- tests/integration/user_test.go | 2 +- 113 files changed, 272 insertions(+), 282 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 37617ad3652..c39d7ac5f2f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,8 @@ linters: - revive - staticcheck - stylecheck + - tenv + - testifylint - typecheck - unconvert - unused @@ -34,6 +36,10 @@ output: show-stats: true linters-settings: + testifylint: + disable: + - go-require + - require-error stylecheck: checks: ["all", "-ST1005", "-ST1003"] nakedret: diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go index e85e99abe53..159805e5f7c 100644 --- a/models/actions/runner_token_test.go +++ b/models/actions/runner_token_test.go @@ -17,7 +17,7 @@ func TestGetLatestRunnerToken(t *testing.T) { token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } func TestNewRunnerToken(t *testing.T) { @@ -26,7 +26,7 @@ func TestNewRunnerToken(t *testing.T) { assert.NoError(t, err) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } func TestUpdateRunnerToken(t *testing.T) { @@ -36,5 +36,5 @@ func TestUpdateRunnerToken(t *testing.T) { assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token)) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index b7babcbde1d..a039fd36132 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -4,7 +4,6 @@ package activities_test import ( - "fmt" "testing" "time" @@ -91,11 +90,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { assert.NoError(t, err) assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") assert.Equal(t, count, int64(contributions)) - assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) + assert.Equal(t, tc.CountResult, contributions, "testcase '%s'", tc.desc) // Test JSON rendering jsonData, err := json.Marshal(heatmap) assert.NoError(t, err) - assert.Equal(t, tc.JSONResult, string(jsonData)) + assert.JSONEq(t, tc.JSONResult, string(jsonData)) } } diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 0829d31d51b..43daa0b5ec1 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -18,7 +18,7 @@ func TestOAuth2Application_GenerateClientSecret(t *testing.T) { app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) secret, err := app.GenerateClientSecret(db.DefaultContext) assert.NoError(t, err) - assert.True(t, len(secret) > 0) + assert.NotEmpty(t, secret) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret}) } @@ -165,7 +165,7 @@ func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) { code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") assert.NoError(t, err) assert.NotNil(t, code) - assert.True(t, len(code.Code) > 32) // secret length > 32 + assert.Greater(t, len(code.Code), 32) // secret length > 32 } func TestOAuth2Grant_TableName(t *testing.T) { diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index 0f6ba2cc94a..e9f27906711 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -38,8 +38,6 @@ func TestIterate(t *testing.T) { if !has { return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID} } - assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID) - assert.EqualValues(t, repoUnit.CreatedUnix, repoUnit.CreatedUnix) return nil }) assert.NoError(t, err) diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index 7ac4da68103..37d785e9385 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -34,7 +34,7 @@ func TestGetCommitStatuses(t *testing.T) { SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Len(t, statuses, 5) assert.Equal(t, "ci/awesomeness", statuses[0].Context) @@ -63,7 +63,7 @@ func TestGetCommitStatuses(t *testing.T) { SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Empty(t, statuses) } diff --git a/models/git/protected_branch_test.go b/models/git/protected_branch_test.go index 49d433f845d..e1c91d927d8 100644 --- a/models/git/protected_branch_test.go +++ b/models/git/protected_branch_test.go @@ -4,7 +4,6 @@ package git import ( - "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -76,7 +75,7 @@ func TestBranchRuleMatch(t *testing.T) { infact = " not" } assert.EqualValues(t, kase.ExpectedMatch, pb.Match(kase.BranchName), - fmt.Sprintf("%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact), + "%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact, ) } } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c5bbfdedc28..d81f33f953d 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -64,7 +64,7 @@ func TestFetchCodeComments(t *testing.T) { } func TestAsCommentType(t *testing.T) { - assert.Equal(t, issues_model.CommentType(0), issues_model.CommentTypeComment) + assert.Equal(t, issues_model.CommentTypeComment, issues_model.CommentType(0)) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("")) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("nonsense")) assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 548f137f394..dbbb1e41790 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -434,7 +434,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.EqualValues(t, milestone.ID, 1) + assert.EqualValues(t, 1, milestone.ID) reaction := &issues_model.Reaction{ Type: "heart", UserID: owner.ID, diff --git a/models/issues/issue_watch_test.go b/models/issues/issue_watch_test.go index d4ce8d8d3d6..fad94e243e6 100644 --- a/models/issues/issue_watch_test.go +++ b/models/issues/issue_watch_test.go @@ -48,17 +48,17 @@ func TestGetIssueWatchers(t *testing.T) { iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{}) assert.NoError(t, err) // Watcher is inactive, thus 0 - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) assert.NoError(t, err) // Watcher is explicit not watching - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{}) assert.NoError(t, err) // Issue has no Watchers - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{}) assert.NoError(t, err) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index c2ff084c236..1d4b6f4684c 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -31,12 +31,12 @@ func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) { // First test : with negative and scope label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"}) assert.Equal(t, "1", label.QueryString) - assert.Equal(t, true, label.IsSelected) + assert.True(t, label.IsSelected) // Second test : with duplicates label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"}) assert.Equal(t, "1,8", label.QueryString) - assert.Equal(t, false, label.IsSelected) + assert.False(t, label.IsSelected) // Third test : empty set label.LoadSelectedLabelsAfterClick([]int64{}, []string{}) @@ -248,7 +248,7 @@ func TestGetLabelsByIssueID(t *testing.T) { labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) - assert.Len(t, labels, 0) + assert.Empty(t, labels) } func TestUpdateLabel(t *testing.T) { @@ -271,7 +271,7 @@ func TestUpdateLabel(t *testing.T) { assert.EqualValues(t, label.Color, newLabel.Color) assert.EqualValues(t, label.Name, newLabel.Name) assert.EqualValues(t, label.Description, newLabel.Description) - assert.EqualValues(t, newLabel.ArchivedUnix, 0) + assert.EqualValues(t, 0, newLabel.ArchivedUnix) unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) } diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index e5f6f15ca2a..28cd0c028b8 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -87,7 +87,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { IsClosed: optional.Some(false), }) assert.NoError(t, err) - assert.Len(t, milestones, 0) + assert.Empty(t, milestones) } func TestGetMilestones(t *testing.T) { diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go index 8b814a0d0fc..c7a898ca4e8 100644 --- a/models/issues/pull_list_test.go +++ b/models/issues/pull_list_test.go @@ -40,7 +40,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { assert.NoError(t, err) assert.Len(t, reviewComments, 2) for _, pr := range prs { - assert.EqualValues(t, reviewComments[pr.IssueID], 1) + assert.EqualValues(t, 1, reviewComments[pr.IssueID]) } } diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index cb7b47263d8..090659864a8 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -83,7 +83,7 @@ func TestLoadRequestedReviewers(t *testing.T) { assert.NoError(t, pull.LoadIssue(db.DefaultContext)) issue := pull.Issue assert.NoError(t, issue.LoadRepo(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 0) + assert.Empty(t, pull.RequestedReviewers) user1, err := user_model.GetUserByID(db.DefaultContext, 1) assert.NoError(t, err) diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go index 39958a7f36b..a1bf9dc931f 100644 --- a/models/issues/stopwatch_test.go +++ b/models/issues/stopwatch_test.go @@ -32,7 +32,7 @@ func TestCancelStopwatch(t *testing.T) { _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - assert.Nil(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) + assert.NoError(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) } func TestStopwatchExists(t *testing.T) { diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index d82bff967a9..44054a1b836 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -50,7 +50,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by User times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1}) @@ -60,7 +60,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by Repo times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2}) @@ -69,7 +69,7 @@ func TestGetTrackedTimes(t *testing.T) { assert.Equal(t, int64(1), times[0].Time) issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID) assert.NoError(t, err) - assert.Equal(t, issue.RepoID, int64(2)) + assert.Equal(t, int64(2), issue.RepoID) times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) assert.NoError(t, err) @@ -77,7 +77,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) } func TestTotalTimesForEachUser(t *testing.T) { diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index d99bbc29620..b279967a2c0 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -56,8 +56,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) { err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments) assert.NoError(t, err) for _, attach := range issueAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.IssueID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.IssueID) var issue Issue has, err := x.ID(attach.IssueID).Get(&issue) assert.NoError(t, err) @@ -69,8 +69,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) { err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) assert.NoError(t, err) for _, attach := range releaseAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.ReleaseID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.ReleaseID) var release Release has, err := x.ID(attach.ReleaseID).Get(&release) assert.NoError(t, err) diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index a19c9396e2e..1f213ddb6e0 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -107,12 +107,12 @@ func Test_RepositoryFormat(t *testing.T) { repo = new(Repository) ok, err := x.ID(2).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) + assert.True(t, ok) assert.EqualValues(t, "sha1", repo.ObjectFormatName) repo = new(Repository) ok, err = x.ID(id).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) + assert.True(t, ok) assert.EqualValues(t, "sha256", repo.ObjectFormatName) } diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 82a3bcd602e..a1d702cb77d 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -39,7 +39,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { tables, err := x.DBMetas() assert.NoError(t, err) - assert.EqualValues(t, 1, len(tables)) + assert.Len(t, tables, 1) found := false for _, index := range tables[0].Indexes { if index.Type == schemas.UniqueType { diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index edc8996f3ec..0f0f8a4bcd0 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -40,7 +40,7 @@ func TestFindOrgs(t *testing.T) { IncludePrivate: false, }) assert.NoError(t, err) - assert.Len(t, orgs, 0) + assert.Empty(t, orgs) total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ UserID: 4, diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 7159f0fc465..5e99e88689e 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -283,7 +283,7 @@ func TestGetOrgUsersByOrgID(t *testing.T) { OrgID: unittest.NonexistentID, }) assert.NoError(t, err) - assert.Len(t, orgUsers, 0) + assert.Empty(t, orgUsers) } func TestChangeOrgUserStatus(t *testing.T) { diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go index 982fceee5a5..c4c7d483fb2 100644 --- a/models/perm/access_mode_test.go +++ b/models/perm/access_mode_test.go @@ -15,7 +15,7 @@ func TestAccessMode(t *testing.T) { m := ParseAccessMode(name) assert.Equal(t, AccessMode(i), m) } - assert.Equal(t, AccessMode(4), AccessModeOwner) + assert.Equal(t, AccessModeOwner, AccessMode(4)) assert.Equal(t, "owner", AccessModeOwner.ToString()) assert.Equal(t, AccessModeNone, ParseAccessMode("owner")) assert.Equal(t, AccessModeNone, ParseAccessMode("invalid")) diff --git a/models/project/column_test.go b/models/project/column_test.go index 911649fb726..566667e45d1 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -5,7 +5,6 @@ package project import ( "fmt" - "strings" "testing" "code.gitea.io/gitea/models/db" @@ -66,7 +65,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) { issues, err = column1.GetIssues(db.DefaultContext) assert.NoError(t, err) - assert.Len(t, issues, 0) + assert.Empty(t, issues) issues, err = column2.GetIssues(db.DefaultContext) assert.NoError(t, err) @@ -123,5 +122,5 @@ func Test_NewColumn(t *testing.T) { ProjectID: project1.ID, }) assert.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached")) + assert.Contains(t, err.Error(), "maximum number of columns reached") } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6468e0f6058..6d88d170da3 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -144,8 +144,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "https://try.gitea.io/user2/repo2") @@ -159,8 +159,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") @@ -177,8 +177,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "sshuser@try.gitea.io:user2/repo2") diff --git a/models/repo/star_test.go b/models/repo/star_test.go index aaac89d975d..b540f54310c 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -52,7 +52,7 @@ func TestRepository_GetStargazers2(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) - assert.Len(t, gazers, 0) + assert.Empty(t, gazers) } func TestClearRepoStars(t *testing.T) { @@ -71,5 +71,5 @@ func TestClearRepoStars(t *testing.T) { gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) - assert.Len(t, gazers, 0) + assert.Empty(t, gazers) } diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index f2abc2ffa01..44ebe5f214c 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -21,7 +21,7 @@ func TestRepoAssignees(t *testing.T) { users, err := repo_model.GetRepoAssignees(db.DefaultContext, repo2) assert.NoError(t, err) assert.Len(t, users, 1) - assert.Equal(t, users[0].ID, int64(2)) + assert.Equal(t, int64(2), users[0].ID) repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index a95a2679616..c39ef607e8b 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -41,7 +41,7 @@ func TestGetWatchers(t *testing.T) { watches, err = repo_model.GetWatchers(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) - assert.Len(t, watches, 0) + assert.Empty(t, watches) } func TestRepository_GetWatchers(t *testing.T) { @@ -58,7 +58,7 @@ func TestRepository_GetWatchers(t *testing.T) { repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9}) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) - assert.Len(t, watchers, 0) + assert.Empty(t, watchers) } func TestWatchIfAuto(t *testing.T) { diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 3b0f28d3a60..4ac858e04ea 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -79,7 +79,7 @@ func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...any) e := db.GetEngine(db.DefaultContext).Table(table) res, err := whereOrderConditions(e, conditions).Query() assert.NoError(t, err) - assert.True(t, len(res) == 1, + assert.Len(t, res, 1, "Expected to find one row in %s (with conditions %+v), but found %d", table, conditions, len(res), ) diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index c2e010d95b3..d72d873de2c 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -97,8 +97,7 @@ func TestListEmails(t *testing.T) { } emails, count, err := user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) - assert.NotEqual(t, int64(0), count) - assert.True(t, count > 5) + assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { for _, v := range emails { diff --git a/models/user/setting_test.go b/models/user/setting_test.go index c56fe930750..c607d9fd008 100644 --- a/models/user/setting_test.go +++ b/models/user/setting_test.go @@ -56,5 +56,5 @@ func TestSettings(t *testing.T) { assert.NoError(t, err) settings, err = user_model.GetUserAllSettings(db.DefaultContext, 99) assert.NoError(t, err) - assert.Len(t, settings, 0) + assert.Empty(t, settings) } diff --git a/models/user/user_test.go b/models/user/user_test.go index 6701be39a55..7ebc64f69e7 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -201,7 +201,7 @@ func TestNewGitSig(t *testing.T) { assert.NotContains(t, sig.Name, "<") assert.NotContains(t, sig.Name, ">") assert.NotContains(t, sig.Name, "\n") - assert.NotEqual(t, len(strings.TrimSpace(sig.Name)), 0) + assert.NotEmpty(t, strings.TrimSpace(sig.Name)) } } @@ -216,7 +216,7 @@ func TestDisplayName(t *testing.T) { if len(strings.TrimSpace(user.FullName)) == 0 { assert.Equal(t, user.Name, displayName) } - assert.NotEqual(t, len(strings.TrimSpace(displayName)), 0) + assert.NotEmpty(t, strings.TrimSpace(displayName)) } } @@ -322,15 +322,15 @@ func TestGetMaileableUsersByIDs(t *testing.T) { assert.NoError(t, err) assert.Len(t, results, 1) if len(results) > 1 { - assert.Equal(t, results[0].ID, 1) + assert.Equal(t, 1, results[0].ID) } results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true) assert.NoError(t, err) assert.Len(t, results, 2) if len(results) > 2 { - assert.Equal(t, results[0].ID, 1) - assert.Equal(t, results[1].ID, 4) + assert.Equal(t, 1, results[0].ID) + assert.Equal(t, 4, results[1].ID) } } @@ -499,7 +499,7 @@ func Test_ValidateUser(t *testing.T) { {ID: 2, Visibility: structs.VisibleTypePrivate}: true, } for kase, expected := range kases { - assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), fmt.Sprintf("case: %+v", kase)) + assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase) } } @@ -570,11 +570,11 @@ func TestDisabledUserFeatures(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0) + assert.Empty(t, setting.Admin.UserDisabledFeatures.Values()) // no features should be disabled with a plain login type assert.LessOrEqual(t, user.LoginType, auth.Plain) - assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0) + assert.Empty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) for _, f := range testValues.Values() { assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } @@ -600,5 +600,5 @@ func TestGetInactiveUsers(t *testing.T) { interval := time.Now().Unix() - 1730468968 + 3600*24 users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second))) assert.NoError(t, err) - assert.Len(t, users, 0) + assert.Empty(t, users) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index f4403776cec..c6c3f40d46e 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -43,7 +43,7 @@ func TestWebhook_History(t *testing.T) { webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) tasks, err = webhook.History(db.DefaultContext, 0) assert.NoError(t, err) - assert.Len(t, tasks, 0) + assert.Empty(t, tasks) } func TestWebhook_UpdateEvent(t *testing.T) { @@ -206,7 +206,7 @@ func TestHookTasks(t *testing.T) { hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1) assert.NoError(t, err) - assert.Len(t, hookTasks, 0) + assert.Empty(t, hookTasks) } func TestCreateHookTask(t *testing.T) { diff --git a/modules/activitypub/client_test.go b/modules/activitypub/client_test.go index 65ea8d4d5bf..d0c48454457 100644 --- a/modules/activitypub/client_test.go +++ b/modules/activitypub/client_test.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/http/httptest" - "regexp" "testing" "code.gitea.io/gitea/models/db" @@ -28,9 +27,9 @@ func TestActivityPubSignedPost(t *testing.T) { expected := "BODY" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest")) + assert.Regexp(t, "^"+setting.Federation.DigestAlgorithm, r.Header.Get("Digest")) assert.Contains(t, r.Header.Get("Signature"), pubID) - assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType) + assert.Equal(t, ActivityStreamsContentType, r.Header.Get("Content-Type")) body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expected, string(body)) diff --git a/modules/assetfs/layered_test.go b/modules/assetfs/layered_test.go index b82111e745e..03a3ae0d7cc 100644 --- a/modules/assetfs/layered_test.go +++ b/modules/assetfs/layered_test.go @@ -58,7 +58,7 @@ func TestLayered(t *testing.T) { assertRead := func(expected string, expectedErr error, elems ...string) { bs, err := assets.ReadFile(elems...) if err != nil { - assert.ErrorAs(t, err, &expectedErr) + assert.ErrorIs(t, err, expectedErr) } else { assert.NoError(t, err) assert.Equal(t, expected, string(bs)) diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go index c277d59c415..7265b5d0c15 100644 --- a/modules/auth/pam/pam_test.go +++ b/modules/auth/pam/pam_test.go @@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) { result, err := Auth("gitea", "user1", "false-pwd") assert.Error(t, err) assert.EqualError(t, err, "Authentication failure") - assert.Len(t, result, 0) + assert.Len(t, result) } diff --git a/modules/auth/password/hash/dummy_test.go b/modules/auth/password/hash/dummy_test.go index f3b36df6250..e56e3f1a7f7 100644 --- a/modules/auth/password/hash/dummy_test.go +++ b/modules/auth/password/hash/dummy_test.go @@ -18,7 +18,7 @@ func TestDummyHasher(t *testing.T) { password, salt := "password", "ZogKvWdyEx" hash, err := dummy.Hash(password, salt) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, hash, salt+":"+password) assert.True(t, dummy.VerifyPassword(password, hash, salt)) diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 86cccdf2092..f63679048e3 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -6,7 +6,6 @@ package base import ( "crypto/sha1" "fmt" - "os" "testing" "time" @@ -157,7 +156,7 @@ func TestStringsToInt64s(t *testing.T) { testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256}) ints, err := StringsToInt64s([]string{"-1", "a"}) - assert.Len(t, ints, 0) + assert.Empty(t, ints) assert.Error(t, err) } @@ -172,9 +171,9 @@ func TestInt64sToStrings(t *testing.T) { // TODO: Test EntryIcon func TestSetupGiteaRoot(t *testing.T) { - _ = os.Setenv("GITEA_ROOT", "test") + t.Setenv("GITEA_ROOT", "test") assert.Equal(t, "test", SetupGiteaRoot()) - _ = os.Setenv("GITEA_ROOT", "") + t.Setenv("GITEA_ROOT", "") assert.NotEqual(t, "test", SetupGiteaRoot()) } diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go index b444fa2de53..2db3a598a4c 100644 --- a/modules/dump/dumper_test.go +++ b/modules/dump/dumper_test.go @@ -25,7 +25,7 @@ func TestPrepareFileNameAndType(t *testing.T) { assert.Equal(t, fmt.Sprintf("outFile=%s, outType=%s", expFile, expType), fmt.Sprintf("outFile=%s, outType=%s", outFile, outType), - fmt.Sprintf("argFile=%s, argType=%s", argFile, argType), + "argFile=%s, argType=%s", argFile, argType, ) } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 3b8b6d3763a..2184a9c47cf 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -146,7 +146,7 @@ func TestHasPreviousCommitSha256(t *testing.T) { parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") assert.Equal(t, objectFormat, parentSHA.Type()) - assert.Equal(t, objectFormat.Name(), "sha256") + assert.Equal(t, "sha256", objectFormat.Name()) haz, err := commit.HasPreviousCommit(parentSHA) assert.NoError(t, err) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index bf381a53501..6ac65564dc1 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -343,9 +343,9 @@ func TestGetCommitFileStatusMerges(t *testing.T) { }, } - assert.Equal(t, commitFileStatus.Added, expected.Added) - assert.Equal(t, commitFileStatus.Removed, expected.Removed) - assert.Equal(t, commitFileStatus.Modified, expected.Modified) + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) } func Test_GetCommitBranchStart(t *testing.T) { diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 6a99f804070..005d5397267 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -73,9 +73,9 @@ func TestGrepSearch(t *testing.T) { res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) assert.NoError(t, err) - assert.Len(t, res, 0) + assert.Empty(t, res) res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) assert.Error(t, err) - assert.Len(t, res, 0) + assert.Empty(t, res) } diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_nogogit_test.go index 23fddb014c1..a4436ce499a 100644 --- a/modules/git/parse_nogogit_test.go +++ b/modules/git/parse_nogogit_test.go @@ -100,5 +100,5 @@ func TestParseTreeEntriesInvalid(t *testing.T) { // there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315 entries, err := ParseTreeEntries([]byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af")) assert.Error(t, err) - assert.Len(t, entries, 0) + assert.Empty(t, entries) } diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 009c545832a..5d3b8abb3a8 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -34,7 +34,7 @@ func TestRepository_GetBranches(t *testing.T) { branches, countAll, err = bareRepo1.GetBranchNames(5, 1) assert.NoError(t, err) - assert.Len(t, branches, 0) + assert.Empty(t, branches) assert.EqualValues(t, 3, countAll) assert.ElementsMatch(t, []string{}, branches) } @@ -66,7 +66,7 @@ func TestGetRefsBySha(t *testing.T) { // do not exist branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") assert.NoError(t, err) - assert.Len(t, branches, 0) + assert.Empty(t, branches) // refs/pull/1/head branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 99838731867..454ed6b9f85 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -72,7 +72,7 @@ func TestReadPatch(t *testing.T) { assert.Empty(t, noFile) assert.Empty(t, noCommit) assert.Len(t, oldCommit, 40) - assert.True(t, oldCommit == "6e8e2a6f9efd71dbe6917816343ed8415ad696c3") + assert.Equal(t, "6e8e2a6f9efd71dbe6917816343ed8415ad696c3", oldCommit) } func TestReadWritePullHead(t *testing.T) { @@ -113,7 +113,7 @@ func TestReadWritePullHead(t *testing.T) { } assert.Len(t, headContents, 40) - assert.True(t, headContents == newCommit) + assert.Equal(t, headContents, newCommit) // Remove file after the test err = repo.RemoveReference(PullPrefix + "1/head") diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 16f0a78ec04..94ce8520bf6 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -113,7 +113,7 @@ var cases = []*testIndexerCase{ }, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) assert.Equal(t, len(data), int(result.Total)) }, }, @@ -176,7 +176,7 @@ var cases = []*testIndexerCase{ IsPull: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.False(t, data[v.ID].IsPull) } @@ -192,7 +192,7 @@ var cases = []*testIndexerCase{ IsPull: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.True(t, data[v.ID].IsPull) } @@ -208,7 +208,7 @@ var cases = []*testIndexerCase{ IsClosed: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.False(t, data[v.ID].IsClosed) } @@ -224,7 +224,7 @@ var cases = []*testIndexerCase{ IsClosed: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.True(t, data[v.ID].IsClosed) } @@ -274,7 +274,7 @@ var cases = []*testIndexerCase{ MilestoneIDs: []int64{1, 2, 6}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, []int64{1, 2, 6}, data[v.ID].MilestoneID) } @@ -292,7 +292,7 @@ var cases = []*testIndexerCase{ MilestoneIDs: []int64{0}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].MilestoneID) } @@ -310,7 +310,7 @@ var cases = []*testIndexerCase{ ProjectID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].ProjectID) } @@ -328,7 +328,7 @@ var cases = []*testIndexerCase{ ProjectID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].ProjectID) } @@ -346,7 +346,7 @@ var cases = []*testIndexerCase{ ProjectColumnID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].ProjectColumnID) } @@ -364,7 +364,7 @@ var cases = []*testIndexerCase{ ProjectColumnID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].ProjectColumnID) } @@ -382,7 +382,7 @@ var cases = []*testIndexerCase{ PosterID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].PosterID) } @@ -400,7 +400,7 @@ var cases = []*testIndexerCase{ AssigneeID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].AssigneeID) } @@ -418,7 +418,7 @@ var cases = []*testIndexerCase{ AssigneeID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].AssigneeID) } @@ -436,7 +436,7 @@ var cases = []*testIndexerCase{ MentionID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].MentionIDs, int64(1)) } @@ -454,7 +454,7 @@ var cases = []*testIndexerCase{ ReviewedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].ReviewedIDs, int64(1)) } @@ -472,7 +472,7 @@ var cases = []*testIndexerCase{ ReviewRequestedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1)) } @@ -490,7 +490,7 @@ var cases = []*testIndexerCase{ SubscriberID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].SubscriberIDs, int64(1)) } @@ -509,7 +509,7 @@ var cases = []*testIndexerCase{ UpdatedBeforeUnix: optional.Some(int64(30)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.GreaterOrEqual(t, data[v.ID].UpdatedUnix, int64(20)) assert.LessOrEqual(t, data[v.ID].UpdatedUnix, int64(30)) diff --git a/modules/lfs/transferadapter_test.go b/modules/lfs/transferadapter_test.go index 7fec137efe5..a430b71a5f1 100644 --- a/modules/lfs/transferadapter_test.go +++ b/modules/lfs/transferadapter_test.go @@ -96,7 +96,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { _, err := a.Download(context.Background(), c.link) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } @@ -129,7 +129,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { err := a.Upload(context.Background(), c.link, p, bytes.NewBufferString("dummy")) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } @@ -162,7 +162,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { err := a.Verify(context.Background(), c.link, p) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go index 70222f64f5c..0de14eb411e 100644 --- a/modules/log/logger_test.go +++ b/modules/log/logger_test.go @@ -56,7 +56,7 @@ func TestLogger(t *testing.T) { logger := NewLoggerWithWriters(context.Background(), "test") dump := logger.DumpWriters() - assert.EqualValues(t, 0, len(dump)) + assert.Empty(t, dump) assert.EqualValues(t, NONE, logger.GetLevel()) assert.False(t, logger.IsEnabled()) @@ -69,7 +69,7 @@ func TestLogger(t *testing.T) { assert.EqualValues(t, DEBUG, logger.GetLevel()) dump = logger.DumpWriters() - assert.EqualValues(t, 2, len(dump)) + assert.Len(t, dump, 2) logger.Trace("trace-level") // this level is not logged logger.Debug("debug-level") diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 9419350e615..159d7129557 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -278,12 +278,12 @@ func TestRender_AutoLink(t *testing.T) { test := func(input, expected string) { var buffer strings.Builder err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer) - assert.Equal(t, err, nil) + assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) buffer.Reset() err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer) - assert.Equal(t, err, nil) + assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) } diff --git a/modules/packages/conan/conanfile_parser_test.go b/modules/packages/conan/conanfile_parser_test.go index 58015701841..aabafd5f64f 100644 --- a/modules/packages/conan/conanfile_parser_test.go +++ b/modules/packages/conan/conanfile_parser_test.go @@ -40,7 +40,7 @@ class ConanPackageConan(ConanFile): func TestParseConanfile(t *testing.T) { metadata, err := ParseConanfile(strings.NewReader(contentConanfile)) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, license, metadata.License) assert.Equal(t, author, metadata.Author) assert.Equal(t, homepage, metadata.ProjectURL) diff --git a/modules/packages/conan/conaninfo_parser_test.go b/modules/packages/conan/conaninfo_parser_test.go index 556a4b939ee..f6510ca6670 100644 --- a/modules/packages/conan/conaninfo_parser_test.go +++ b/modules/packages/conan/conaninfo_parser_test.go @@ -50,7 +50,7 @@ const ( func TestParseConaninfo(t *testing.T) { info, err := ParseConaninfo(strings.NewReader(contentConaninfo)) assert.NotNil(t, info) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal( t, map[string]string{ diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go index c5bf526ae67..01b52b3c16e 100644 --- a/modules/queue/base_test.go +++ b/modules/queue/base_test.go @@ -46,10 +46,10 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) assert.NoError(t, err) if !isUnique { assert.EqualValues(t, 2, cnt) - assert.EqualValues(t, false, has) // non-unique queues don't check for duplicates + assert.False(t, has) // non-unique queues don't check for duplicates } else { assert.EqualValues(t, 1, cnt) - assert.EqualValues(t, true, has) + assert.True(t, has) } // push another item @@ -101,7 +101,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) pushBlockTime = 30 * time.Millisecond err = q.PushItem(ctx, []byte("item-full")) assert.ErrorIs(t, err, context.DeadlineExceeded) - assert.True(t, time.Since(timeStart) >= pushBlockTime*2/3) + assert.GreaterOrEqual(t, time.Since(timeStart), pushBlockTime*2/3) pushBlockTime = oldPushBlockTime // remove all diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index d66253ff664..c0841a1752a 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -172,8 +172,8 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett q2() // restart the queue to continue to execute the tasks in it - assert.NotZero(t, len(tasksQ1)) - assert.NotZero(t, len(tasksQ2)) + assert.NotEmpty(t, tasksQ1) + assert.NotEmpty(t, tasksQ2) assert.EqualValues(t, testCount, len(tasksQ1)+len(tasksQ2)) } diff --git a/modules/references/references_test.go b/modules/references/references_test.go index e5a0d60fe3b..e224c919e92 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -526,7 +526,7 @@ func TestCustomizeCloseKeywords(t *testing.T) { func TestParseCloseKeywords(t *testing.T) { // Test parsing of CloseKeywords and ReopenKeywords - assert.Len(t, parseKeywords([]string{""}), 0) + assert.Empty(t, parseKeywords([]string{""})) assert.Len(t, parseKeywords([]string{" aa ", " bb ", "99", "#", "", "this is", "cc"}), 3) for _, test := range []struct { diff --git a/modules/repository/repo_test.go b/modules/repository/repo_test.go index 68980f92f94..f3e7be6d7d7 100644 --- a/modules/repository/repo_test.go +++ b/modules/repository/repo_test.go @@ -62,15 +62,15 @@ func Test_calcSync(t *testing.T) { } inserts, deletes, updates := calcSync(gitTags, dbReleases) - if assert.EqualValues(t, 1, len(inserts), "inserts") { + if assert.Len(t, inserts, 1, "inserts") { assert.EqualValues(t, *gitTags[2], *inserts[0], "inserts equal") } - if assert.EqualValues(t, 1, len(deletes), "deletes") { + if assert.Len(t, deletes, 1, "deletes") { assert.EqualValues(t, 1, deletes[0], "deletes equal") } - if assert.EqualValues(t, 1, len(updates), "updates") { + if assert.Len(t, updates, 1, "updates") { assert.EqualValues(t, *gitTags[1], *updates[0], "updates equal") } } diff --git a/modules/setting/cron_test.go b/modules/setting/cron_test.go index 3187ab18a25..55244d70757 100644 --- a/modules/setting/cron_test.go +++ b/modules/setting/cron_test.go @@ -38,6 +38,6 @@ EXTEND = true _, err = getCronSettings(cfg, "test", extended) assert.NoError(t, err) assert.True(t, extended.Base) - assert.EqualValues(t, extended.Second, "white rabbit") + assert.EqualValues(t, "white rabbit", extended.Second) assert.True(t, extended.Extend) } diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go index 38ee4d248d6..d0e5ccf13d2 100644 --- a/modules/setting/oauth2_test.go +++ b/modules/setting/oauth2_test.go @@ -74,5 +74,5 @@ DEFAULT_APPLICATIONS = tea DEFAULT_APPLICATIONS = `) loadOAuth2From(cfg) - assert.Nil(t, nil, OAuth2.DefaultApplications) + assert.Nil(t, OAuth2.DefaultApplications) } diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go index 8ee37fd2b6d..afff85537e3 100644 --- a/modules/setting/storage_test.go +++ b/modules/setting/storage_test.go @@ -447,7 +447,7 @@ MINIO_USE_SSL = true assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) } @@ -464,7 +464,7 @@ MINIO_BASE_PATH = /prefix assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -477,7 +477,7 @@ MINIO_BASE_PATH = /prefix assert.NoError(t, err) assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -495,7 +495,7 @@ MINIO_BASE_PATH = /lfs assert.NoError(t, loadLFSFrom(cfg)) assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL) + assert.True(t, LFS.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -513,7 +513,7 @@ MINIO_BASE_PATH = /lfs assert.NoError(t, loadLFSFrom(cfg)) assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL) + assert.True(t, LFS.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath) } diff --git a/modules/user/user_test.go b/modules/user/user_test.go index 9129ae79a13..372a675d342 100644 --- a/modules/user/user_test.go +++ b/modules/user/user_test.go @@ -4,7 +4,6 @@ package user import ( - "os" "os/exec" "runtime" "strings" @@ -36,7 +35,7 @@ func TestCurrentUsername(t *testing.T) { if user != whoami { t.Errorf("expected %s as user, got: %s", whoami, user) } - os.Setenv("USER", "spoofed") + t.Setenv("USER", "spoofed") user = CurrentUsername() if user != whoami { t.Errorf("expected %s as user, got: %s", whoami, user) diff --git a/modules/util/color_test.go b/modules/util/color_test.go index be6e6b122a5..abd55512184 100644 --- a/modules/util/color_test.go +++ b/modules/util/color_test.go @@ -27,9 +27,9 @@ func Test_HexToRBGColor(t *testing.T) { } for n, c := range cases { r, g, b := HexToRBGColor(c.colorString) - assert.Equal(t, c.expectedR, r, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r) - assert.Equal(t, c.expectedG, g, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g) - assert.Equal(t, c.expectedB, b, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b) + assert.InDelta(t, c.expectedR, r, 0, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r) + assert.InDelta(t, c.expectedG, g, 0, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g) + assert.InDelta(t, c.expectedB, b, 0, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b) } } diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go index c6f68c845a4..2bade3bb28a 100644 --- a/modules/util/keypair_test.go +++ b/modules/util/keypair_test.go @@ -10,7 +10,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/pem" - "regexp" "testing" "github.com/stretchr/testify/assert" @@ -23,8 +22,8 @@ func TestKeygen(t *testing.T) { assert.NotEmpty(t, priv) assert.NotEmpty(t, pub) - assert.Regexp(t, regexp.MustCompile("^-----BEGIN RSA PRIVATE KEY-----.*"), priv) - assert.Regexp(t, regexp.MustCompile("^-----BEGIN PUBLIC KEY-----.*"), pub) + assert.Regexp(t, "^-----BEGIN RSA PRIVATE KEY-----.*", priv) + assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----.*", pub) } func TestSignUsingKeys(t *testing.T) { diff --git a/modules/util/time_str_test.go b/modules/util/time_str_test.go index 67b7978d0bf..8d1de51c8e6 100644 --- a/modules/util/time_str_test.go +++ b/modules/util/time_str_test.go @@ -27,9 +27,9 @@ func TestTimeStr(t *testing.T) { t.Run(test.input, func(t *testing.T) { output, err := TimeEstimateParse(test.input) if test.err { - assert.NotNil(t, err) + assert.Error(t, err) } else { - assert.Nil(t, err) + assert.NoError(t, err) } assert.Equal(t, test.output, output) }) diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 9ce72fb8669..5abce08b41f 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -122,8 +122,8 @@ func Test_NormalizeEOL(t *testing.T) { func Test_RandomInt(t *testing.T) { randInt, err := CryptoRandomInt(255) - assert.True(t, randInt >= 0) - assert.True(t, randInt <= 255) + assert.GreaterOrEqual(t, randInt, int64(0)) + assert.LessOrEqual(t, randInt, int64(255)) assert.NoError(t, err) } @@ -223,22 +223,22 @@ func BenchmarkToUpper(b *testing.B) { } func TestToTitleCase(t *testing.T) { - assert.Equal(t, ToTitleCase(`foo bar baz`), `Foo Bar Baz`) - assert.Equal(t, ToTitleCase(`FOO BAR BAZ`), `Foo Bar Baz`) + assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`foo bar baz`)) + assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`)) } func TestToPointer(t *testing.T) { assert.Equal(t, "abc", *ToPointer("abc")) assert.Equal(t, 123, *ToPointer(123)) abc := "abc" - assert.False(t, &abc == ToPointer(abc)) + assert.NotSame(t, &abc, ToPointer(abc)) val123 := 123 - assert.False(t, &val123 == ToPointer(val123)) + assert.NotSame(t, &val123, ToPointer(val123)) } func TestReserveLineBreakForTextarea(t *testing.T) { - assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata") - assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n") + assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata")) + assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n")) } func TestOptionalArg(t *testing.T) { diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go index 658557d3cfc..a089739d156 100644 --- a/routers/private/hook_post_receive_test.go +++ b/routers/private/hook_post_receive_test.go @@ -39,7 +39,7 @@ func TestHandlePullRequestMerging(t *testing.T) { }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ {NewCommitID: "01234567"}, }) - assert.Equal(t, 0, len(resp.Body.String())) + assert.Empty(t, resp.Body.String()) pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID) assert.NoError(t, err) assert.True(t, pr.HasMerged) diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index b81f2ea02e3..958ff802d40 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -135,7 +135,7 @@ func TestNewWikiPost(t *testing.T) { NewWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) + assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) } } @@ -194,7 +194,7 @@ func TestEditWikiPost(t *testing.T) { EditWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) + assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) if title != "Home" { assertWikiNotExists(t, ctx.Repo.Repository, "Home") } diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go index 12db2bae565..85e74091052 100644 --- a/services/actions/auth_test.go +++ b/services/actions/auth_test.go @@ -17,19 +17,19 @@ import ( func TestCreateAuthorizationToken(t *testing.T) { var taskID int64 = 23 token, err := CreateAuthorizationToken(taskID, 1, 2) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, "", token) claims := jwt.MapClaims{} _, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) { return setting.GetGeneralTokenSigningSecret(), nil }) - assert.Nil(t, err) + assert.NoError(t, err) scp, ok := claims["scp"] assert.True(t, ok, "Has scp claim in jwt token") assert.Contains(t, scp, "Actions.Results:1:2") taskIDClaim, ok := claims["TaskID"] assert.True(t, ok, "Has TaskID claim in jwt token") - assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one") + assert.InDelta(t, float64(taskID), taskIDClaim, 0, "Supplied taskid must match stored one") acClaim, ok := claims["ac"] assert.True(t, ok, "Has ac claim in jwt token") ac, ok := acClaim.(string) @@ -43,14 +43,14 @@ func TestCreateAuthorizationToken(t *testing.T) { func TestParseAuthorizationToken(t *testing.T) { var taskID int64 = 23 token, err := CreateAuthorizationToken(taskID, 1, 2) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, "", token) headers := http.Header{} headers.Set("Authorization", "Bearer "+token) rTaskID, err := ParseAuthorizationToken(&http.Request{ Header: headers, }) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, taskID, rTaskID) } @@ -59,6 +59,6 @@ func TestParseAuthorizationTokenNoAuthHeader(t *testing.T) { rTaskID, err := ParseAuthorizationToken(&http.Request{ Header: headers, }) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, int64(0), rTaskID) } diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 75c231ff7a4..b706847e8e1 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -28,7 +28,7 @@ func TestUserIDFromToken(t *testing.T) { o := OAuth2{} uid := o.userIDFromToken(context.Background(), token, ds) assert.Equal(t, int64(user_model.ActionsUserID), uid) - assert.Equal(t, ds["IsActionsToken"], true) + assert.Equal(t, true, ds["IsActionsToken"]) assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) }) } diff --git a/services/auth/source/oauth2/source_sync_test.go b/services/auth/source/oauth2/source_sync_test.go index 25408e8727e..893ed625028 100644 --- a/services/auth/source/oauth2/source_sync_test.go +++ b/services/auth/source/oauth2/source_sync_test.go @@ -64,8 +64,8 @@ func TestSource(t *testing.T) { ok, err := user_model.GetExternalLogin(context.Background(), e) assert.NoError(t, err) assert.True(t, ok) - assert.Equal(t, e.RefreshToken, "refresh") - assert.Equal(t, e.AccessToken, "token") + assert.Equal(t, "refresh", e.RefreshToken) + assert.Equal(t, "token", e.AccessToken) u, err := user_model.GetUserByID(context.Background(), user.ID) assert.NoError(t, err) @@ -89,8 +89,8 @@ func TestSource(t *testing.T) { ok, err := user_model.GetExternalLogin(context.Background(), e) assert.NoError(t, err) assert.True(t, ok) - assert.Equal(t, e.RefreshToken, "") - assert.Equal(t, e.AccessToken, "") + assert.Equal(t, "", e.RefreshToken) + assert.Equal(t, "", e.AccessToken) u, err := user_model.GetUserByID(context.Background(), user.ID) assert.NoError(t, err) diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go index 68869502802..a1296fafd4e 100644 --- a/services/convert/pull_review_test.go +++ b/services/convert/pull_review_test.go @@ -40,7 +40,7 @@ func Test_ToPullReview(t *testing.T) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4) assert.NoError(t, err) - assert.Len(t, prList, 0) + assert.Empty(t, prList) }) t.Run("Admin User", func(t *testing.T) { diff --git a/services/cron/tasks_test.go b/services/cron/tasks_test.go index 979371a0229..ab22403ede7 100644 --- a/services/cron/tasks_test.go +++ b/services/cron/tasks_test.go @@ -12,7 +12,7 @@ import ( ) func TestAddTaskToScheduler(t *testing.T) { - assert.Len(t, scheduler.Jobs(), 0) + assert.Empty(t, scheduler.Jobs()) defer scheduler.Clear() // no seconds diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go index 6f1cb9a969b..1e4d029e18c 100644 --- a/services/feed/feed_test.go +++ b/services/feed/feed_test.go @@ -41,7 +41,7 @@ func TestGetFeeds(t *testing.T) { OnlyPerformedBy: false, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) } @@ -57,7 +57,7 @@ func TestGetFeedsForRepos(t *testing.T) { IncludePrivate: true, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) // public repo & no login @@ -119,7 +119,7 @@ func TestGetFeeds2(t *testing.T) { IncludeDeleted: true, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index adcac355a7b..2351c5da879 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -5,7 +5,6 @@ package gitdiff import ( - "fmt" "strconv" "strings" "testing" @@ -643,9 +642,9 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { MaxFiles: setting.Git.MaxGitDiffFiles, WhitespaceBehavior: behavior, }) - assert.NoError(t, err, fmt.Sprintf("Error when diff with %s", behavior)) + assert.NoError(t, err, "Error when diff with %s", behavior) for _, f := range diffs.Files { - assert.True(t, len(f.Sections) > 0, fmt.Sprintf("%s should have sections", f.Name)) + assert.NotEmpty(t, f.Sections, "%s should have sections", f.Name) } } } diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 0b9eeaed540..eccfc4def1f 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -50,7 +50,7 @@ func TestGitlabDownloadRepo(t *testing.T) { topics, err := downloader.GetTopics() assert.NoError(t, err) - assert.True(t, len(topics) == 2) + assert.Len(t, topics, 2) assert.EqualValues(t, []string{"migration", "test"}, topics) milestones, err := downloader.GetMilestones() diff --git a/services/org/team_test.go b/services/org/team_test.go index 58b8e0803c4..98addac8f8a 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -121,7 +121,7 @@ func TestDeleteTeam(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) accessMode, err := access_model.AccessLevel(db.DefaultContext, user, repo) assert.NoError(t, err) - assert.True(t, accessMode < perm.AccessModeWrite) + assert.Less(t, accessMode, perm.AccessModeWrite) } func TestAddTeamMember(t *testing.T) { diff --git a/services/pull/reviewer_test.go b/services/pull/reviewer_test.go index 1ff373bafb7..b106e2e89f5 100644 --- a/services/pull/reviewer_test.go +++ b/services/pull/reviewer_test.go @@ -30,7 +30,7 @@ func TestRepoGetReviewers(t *testing.T) { // should not include doer and remove the poster reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2) assert.NoError(t, err) - assert.Len(t, reviewers, 0) + assert.Empty(t, reviewers) // should not include PR poster, if PR poster would be otherwise eligible reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4) @@ -43,7 +43,7 @@ func TestRepoGetReviewers(t *testing.T) { reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4) assert.NoError(t, err) assert.Len(t, reviewers, 1) - assert.EqualValues(t, reviewers[0].ID, 2) + assert.EqualValues(t, 2, reviewers[0].ID) // test private org repo repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) diff --git a/services/release/release_test.go b/services/release/release_test.go index 3d0681f1e17..95a54832b9e 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -228,7 +228,7 @@ func TestRelease_Update(t *testing.T) { IsTag: false, } assert.NoError(t, CreateRelease(gitRepo, release, nil, "")) - assert.Greater(t, release.ID, int64(0)) + assert.Positive(t, release.ID) release.IsDraft = false tagName := release.TagName diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index 2ab18edf491..1d0c6e513d8 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -4,7 +4,6 @@ package archiver import ( - "errors" "testing" "time" @@ -121,7 +120,7 @@ func TestArchive_Basic(t *testing.T) { // It's fine to go ahead and set it to nil now. assert.Equal(t, zipReq, zipReq2) - assert.False(t, zipReq == zipReq2) + assert.NotSame(t, zipReq, zipReq2) // Same commit, different compression formats should have different names. // Ideally, the extension would match what we originally requested. @@ -131,5 +130,5 @@ func TestArchive_Basic(t *testing.T) { func TestErrUnknownArchiveFormat(t *testing.T) { err := ErrUnknownArchiveFormat{RequestFormat: "master"} - assert.True(t, errors.Is(err, ErrUnknownArchiveFormat{})) + assert.ErrorIs(t, err, ErrUnknownArchiveFormat{}) } diff --git a/services/repository/license_test.go b/services/repository/license_test.go index 39e9738145c..9d3e0f36e36 100644 --- a/services/repository/license_test.go +++ b/services/repository/license_test.go @@ -65,7 +65,7 @@ func Test_detectLicense(t *testing.T) { result, err := detectLicense(strings.NewReader(tests[2].arg + tests[3].arg + tests[4].arg)) assert.NoError(t, err) t.Run("multiple licenses test", func(t *testing.T) { - assert.Equal(t, 3, len(result)) + assert.Len(t, result, 3) assert.Contains(t, result, tests[2].want[0]) assert.Contains(t, result, tests[3].want[0]) assert.Contains(t, result, tests[4].want[0]) diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 67799eddcc3..0401701ba55 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -103,7 +103,7 @@ func TestRepositoryTransfer(t *testing.T) { assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) - assert.Nil(t, err) + assert.NoError(t, err) assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) assert.Equal(t, "user2", transfer.Recipient.Name) diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index e9b0695baae..f47807fa6e2 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -25,7 +25,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Create(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Delete", func(t *testing.T) { @@ -33,7 +33,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Delete(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Fork", func(t *testing.T) { @@ -41,7 +41,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Fork(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Push", func(t *testing.T) { @@ -59,12 +59,12 @@ func TestPackagistPayload(t *testing.T) { p.Action = api.HookIssueOpened pl, err := pc.Issue(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookIssueClosed pl, err = pc.Issue(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("IssueComment", func(t *testing.T) { @@ -72,7 +72,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("PullRequest", func(t *testing.T) { @@ -80,7 +80,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.PullRequest(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("PullRequestComment", func(t *testing.T) { @@ -88,7 +88,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Review", func(t *testing.T) { @@ -97,7 +97,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Repository", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Repository(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Package", func(t *testing.T) { @@ -113,7 +113,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Package(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Wiki", func(t *testing.T) { @@ -122,17 +122,17 @@ func TestPackagistPayload(t *testing.T) { p.Action = api.HookWikiCreated pl, err := pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookWikiEdited pl, err = pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookWikiDeleted pl, err = pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Release", func(t *testing.T) { @@ -140,7 +140,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Release(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) } diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 5f5c1462323..63cbce17712 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -21,11 +21,11 @@ func TestWebhook_GetSlackHook(t *testing.T) { Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, } slackHook := GetSlackHook(w) - assert.Equal(t, *slackHook, SlackMeta{ + assert.Equal(t, SlackMeta{ Channel: "foo", Username: "username", Color: "blue", - }) + }, *slackHook) } func TestPrepareWebhooks(t *testing.T) { diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go index 29e9930538e..6393fc53cc9 100644 --- a/tests/integration/api_actions_artifact_test.go +++ b/tests/integration/api_actions_artifact_test.go @@ -133,7 +133,7 @@ func TestActionsArtifactDownload(t *testing.T) { } } assert.NotNil(t, artifactIdx) - assert.Equal(t, listResp.Value[artifactIdx].Name, "artifact-download") + assert.Equal(t, "artifact-download", listResp.Value[artifactIdx].Name) assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts") idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") @@ -374,7 +374,7 @@ func TestActionsArtifactOverwrite(t *testing.T) { break } } - assert.Equal(t, uploadedItem.Name, "artifact-download") + assert.Equal(t, "artifact-download", uploadedItem.Name) idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/") url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact-download" diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 24a041de17e..8a0bd2e4ffa 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -244,7 +244,7 @@ func TestAPIBranchProtection(t *testing.T) { StatusCheckContexts: []string{"test1"}, }, http.StatusOK) bp := testAPIGetBranchProtection(t, "master", http.StatusOK) - assert.Equal(t, true, bp.EnableStatusCheck) + assert.True(t, bp.EnableStatusCheck) assert.Equal(t, []string{"test1"}, bp.StatusCheckContexts) // disable status checks, clear the list of required checks @@ -253,7 +253,7 @@ func TestAPIBranchProtection(t *testing.T) { StatusCheckContexts: []string{}, }, http.StatusOK) bp = testAPIGetBranchProtection(t, "master", http.StatusOK) - assert.Equal(t, false, bp.EnableStatusCheck) + assert.False(t, bp.EnableStatusCheck) assert.Equal(t, []string{}, bp.StatusCheckContexts) testAPIDeleteBranchProtection(t, "master", http.StatusNoContent) diff --git a/tests/integration/api_issue_config_test.go b/tests/integration/api_issue_config_test.go index 745d0cb2a23..ad399654437 100644 --- a/tests/integration/api_issue_config_test.go +++ b/tests/integration/api_issue_config_test.go @@ -47,7 +47,7 @@ func TestAPIRepoGetIssueConfig(t *testing.T) { issueConfig := getIssueConfig(t, owner.Name, repo.Name) assert.True(t, issueConfig.BlankIssuesEnabled) - assert.Len(t, issueConfig.ContactLinks, 0) + assert.Empty(t, issueConfig.ContactLinks) }) t.Run("DisableBlankIssues", func(t *testing.T) { @@ -59,7 +59,7 @@ func TestAPIRepoGetIssueConfig(t *testing.T) { issueConfig := getIssueConfig(t, owner.Name, repo.Name) assert.False(t, issueConfig.BlankIssuesEnabled) - assert.Len(t, issueConfig.ContactLinks, 0) + assert.Empty(t, issueConfig.ContactLinks) }) t.Run("ContactLinks", func(t *testing.T) { @@ -135,7 +135,7 @@ func TestAPIRepoIssueConfigPaths(t *testing.T) { issueConfig := getIssueConfig(t, owner.Name, repo.Name) assert.False(t, issueConfig.BlankIssuesEnabled) - assert.Len(t, issueConfig.ContactLinks, 0) + assert.Empty(t, issueConfig.ContactLinks) _, err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch) assert.NoError(t, err) diff --git a/tests/integration/api_issue_pin_test.go b/tests/integration/api_issue_pin_test.go index 1cff937254b..c1bfa5aa0eb 100644 --- a/tests/integration/api_issue_pin_test.go +++ b/tests/integration/api_issue_pin_test.go @@ -153,7 +153,7 @@ func TestAPIListPinnedIssues(t *testing.T) { var issueList []api.Issue DecodeJSON(t, resp, &issueList) - assert.Equal(t, 1, len(issueList)) + assert.Len(t, issueList, 1) assert.Equal(t, issue.ID, issueList[0].ID) } @@ -169,7 +169,7 @@ func TestAPIListPinnedPullrequests(t *testing.T) { var prList []api.PullRequest DecodeJSON(t, resp, &prList) - assert.Equal(t, 0, len(prList)) + assert.Empty(t, prList) } func TestAPINewPinAllowed(t *testing.T) { diff --git a/tests/integration/api_issue_stopwatch_test.go b/tests/integration/api_issue_stopwatch_test.go index 23066782173..4765787e6f2 100644 --- a/tests/integration/api_issue_stopwatch_test.go +++ b/tests/integration/api_issue_stopwatch_test.go @@ -40,7 +40,7 @@ func TestAPIListStopWatches(t *testing.T) { assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) - assert.Greater(t, apiWatches[0].Seconds, int64(0)) + assert.Positive(t, apiWatches[0].Seconds) } } diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 9f75478ebfc..d8394a33d96 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -252,7 +252,7 @@ func TestAPIEditIssue(t *testing.T) { assert.Equal(t, api.StateClosed, apiIssue.State) assert.Equal(t, milestone, apiIssue.Milestone.ID) assert.Equal(t, body, apiIssue.Body) - assert.True(t, apiIssue.Deadline == nil) + assert.Nil(t, apiIssue.Deadline) assert.Equal(t, title, apiIssue.Title) // in database diff --git a/tests/integration/api_keys_test.go b/tests/integration/api_keys_test.go index 89ad1ec0df5..2276b955cfc 100644 --- a/tests/integration/api_keys_test.go +++ b/tests/integration/api_keys_test.go @@ -168,7 +168,7 @@ func TestCreateUserKey(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fingerprintPublicKeys) - assert.Len(t, fingerprintPublicKeys, 0) + assert.Empty(t, fingerprintPublicKeys) // Fail searching for wrong users key req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", "user2", newPublicKey.Fingerprint)). @@ -176,7 +176,7 @@ func TestCreateUserKey(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fingerprintPublicKeys) - assert.Len(t, fingerprintPublicKeys, 0) + assert.Empty(t, fingerprintPublicKeys) // Now login as user 2 session2 := loginUser(t, "user2") @@ -208,5 +208,5 @@ func TestCreateUserKey(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &fingerprintPublicKeys) - assert.Len(t, fingerprintPublicKeys, 0) + assert.Empty(t, fingerprintPublicKeys) } diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go index abb9852eef4..dc4ba83ecc5 100644 --- a/tests/integration/api_notification_test.go +++ b/tests/integration/api_notification_test.go @@ -120,7 +120,7 @@ func TestAPINotification(t *testing.T) { AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &newStruct) - assert.True(t, newStruct.New > 0) + assert.Positive(t, newStruct.New) // -- mark notifications as read -- req = NewRequest(t, "GET", "/api/v1/notifications?status-types=unread"). @@ -154,7 +154,7 @@ func TestAPINotification(t *testing.T) { AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &newStruct) - assert.True(t, newStruct.New == 0) + assert.Zero(t, newStruct.New) } func TestAPINotificationPUT(t *testing.T) { diff --git a/tests/integration/api_oauth2_apps_test.go b/tests/integration/api_oauth2_apps_test.go index 0ea3dc72ff9..7a17b4ca880 100644 --- a/tests/integration/api_oauth2_apps_test.go +++ b/tests/integration/api_oauth2_apps_test.go @@ -74,9 +74,9 @@ func testAPIListOAuth2Applications(t *testing.T) { DecodeJSON(t, resp, &appList) expectedApp := appList[0] - assert.EqualValues(t, existApp.Name, expectedApp.Name) - assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID) - assert.Equal(t, existApp.ConfidentialClient, expectedApp.ConfidentialClient) + assert.EqualValues(t, expectedApp.Name, existApp.Name) + assert.EqualValues(t, expectedApp.ClientID, existApp.ClientID) + assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient) assert.Len(t, expectedApp.ClientID, 36) assert.Empty(t, expectedApp.ClientSecret) assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0]) @@ -128,13 +128,13 @@ func testAPIGetOAuth2Application(t *testing.T) { DecodeJSON(t, resp, &app) expectedApp := app - assert.EqualValues(t, existApp.Name, expectedApp.Name) - assert.EqualValues(t, existApp.ClientID, expectedApp.ClientID) - assert.Equal(t, existApp.ConfidentialClient, expectedApp.ConfidentialClient) + assert.EqualValues(t, expectedApp.Name, existApp.Name) + assert.EqualValues(t, expectedApp.ClientID, existApp.ClientID) + assert.Equal(t, expectedApp.ConfidentialClient, existApp.ConfidentialClient) assert.Len(t, expectedApp.ClientID, 36) assert.Empty(t, expectedApp.ClientSecret) assert.Len(t, expectedApp.RedirectURIs, 1) - assert.EqualValues(t, existApp.RedirectURIs[0], expectedApp.RedirectURIs[0]) + assert.EqualValues(t, expectedApp.RedirectURIs[0], existApp.RedirectURIs[0]) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: expectedApp.ID, Name: expectedApp.Name}) } diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go index 9c888972ffb..b9660aeeb9a 100644 --- a/tests/integration/api_packages_npm_test.go +++ b/tests/integration/api_packages_npm_test.go @@ -325,7 +325,7 @@ func TestPackageNpm(t *testing.T) { pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm) assert.NoError(t, err) - assert.Len(t, pvs, 0) + assert.Empty(t, pvs) }) }) } diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go index d26b285a1a8..969e1108951 100644 --- a/tests/integration/api_pull_test.go +++ b/tests/integration/api_pull_test.go @@ -48,7 +48,7 @@ func TestAPIViewPulls(t *testing.T) { pull := pulls[0] assert.EqualValues(t, 1, pull.Poster.ID) assert.Len(t, pull.RequestedReviewers, 2) - assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.Empty(t, pull.RequestedReviewersTeams) assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID) assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID) assert.EqualValues(t, 1, pull.ChangedFiles) @@ -83,7 +83,7 @@ func TestAPIViewPulls(t *testing.T) { pull = pulls[1] assert.EqualValues(t, 1, pull.Poster.ID) assert.Len(t, pull.RequestedReviewers, 4) - assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.Empty(t, pull.RequestedReviewersTeams) assert.EqualValues(t, 3, pull.RequestedReviewers[0].ID) assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID) assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID) @@ -120,7 +120,7 @@ func TestAPIViewPulls(t *testing.T) { pull = pulls[2] assert.EqualValues(t, 1, pull.Poster.ID) assert.Len(t, pull.RequestedReviewers, 1) - assert.Len(t, pull.RequestedReviewersTeams, 0) + assert.Empty(t, pull.RequestedReviewersTeams) assert.EqualValues(t, 1, pull.RequestedReviewers[0].ID) assert.EqualValues(t, 0, pull.ChangedFiles) diff --git a/tests/integration/api_repo_git_commits_test.go b/tests/integration/api_repo_git_commits_test.go index 36552062073..c4c626eb496 100644 --- a/tests/integration/api_repo_git_commits_test.go +++ b/tests/integration/api_repo_git_commits_test.go @@ -77,7 +77,7 @@ func TestAPIReposGitCommitList(t *testing.T) { assert.EqualValues(t, "c8e31bc7688741a5287fcde4fbb8fc129ca07027", apiData[1].CommitMeta.SHA) compareCommitFiles(t, []string{"test.csv"}, apiData[1].Files) - assert.EqualValues(t, resp.Header().Get("X-Total"), "2") + assert.EqualValues(t, "2", resp.Header().Get("X-Total")) } func TestAPIReposGitCommitListNotMaster(t *testing.T) { @@ -103,7 +103,7 @@ func TestAPIReposGitCommitListNotMaster(t *testing.T) { assert.EqualValues(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA) compareCommitFiles(t, []string{"readme.md"}, apiData[2].Files) - assert.EqualValues(t, resp.Header().Get("X-Total"), "3") + assert.EqualValues(t, "3", resp.Header().Get("X-Total")) } func TestAPIReposGitCommitListPage2Empty(t *testing.T) { @@ -121,7 +121,7 @@ func TestAPIReposGitCommitListPage2Empty(t *testing.T) { var apiData []api.Commit DecodeJSON(t, resp, &apiData) - assert.Len(t, apiData, 0) + assert.Empty(t, apiData) } func TestAPIReposGitCommitListDifferentBranch(t *testing.T) { @@ -208,7 +208,7 @@ func TestGetFileHistory(t *testing.T) { assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA) compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files) - assert.EqualValues(t, resp.Header().Get("X-Total"), "1") + assert.EqualValues(t, "1", resp.Header().Get("X-Total")) } func TestGetFileHistoryNotOnMaster(t *testing.T) { @@ -229,5 +229,5 @@ func TestGetFileHistoryNotOnMaster(t *testing.T) { assert.Equal(t, "c8e31bc7688741a5287fcde4fbb8fc129ca07027", apiData[0].CommitMeta.SHA) compareCommitFiles(t, []string{"test.csv"}, apiData[0].Files) - assert.EqualValues(t, resp.Header().Get("X-Total"), "1") + assert.EqualValues(t, "1", resp.Header().Get("X-Total")) } diff --git a/tests/integration/api_repo_lfs_locks_test.go b/tests/integration/api_repo_lfs_locks_test.go index 427e0b9fb11..4ba01e6d9bd 100644 --- a/tests/integration/api_repo_lfs_locks_test.go +++ b/tests/integration/api_repo_lfs_locks_test.go @@ -176,6 +176,6 @@ func TestAPILFSLocksLogged(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var lfsLocks api.LFSLockList DecodeJSON(t, resp, &lfsLocks) - assert.Len(t, lfsLocks.Locks, 0) + assert.Empty(t, lfsLocks.Locks) } } diff --git a/tests/integration/api_repo_teams_test.go b/tests/integration/api_repo_teams_test.go index 558bac81501..07d065b02ba 100644 --- a/tests/integration/api_repo_teams_test.go +++ b/tests/integration/api_repo_teams_test.go @@ -39,7 +39,7 @@ func TestAPIRepoTeams(t *testing.T) { if assert.Len(t, teams, 2) { assert.EqualValues(t, "Owners", teams[0].Name) assert.True(t, teams[0].CanCreateOrgRepo) - assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), fmt.Sprintf("%v == %v", unit.AllUnitKeyNames(), teams[0].Units)) + assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), "%v == %v", unit.AllUnitKeyNames(), teams[0].Units) assert.EqualValues(t, "owner", teams[0].Permission) assert.EqualValues(t, "test_team", teams[1].Name) diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go index c656ded5ae9..9b8726c6c2e 100644 --- a/tests/integration/api_user_orgs_test.go +++ b/tests/integration/api_user_orgs_test.go @@ -76,7 +76,7 @@ func TestUserOrgs(t *testing.T) { // unrelated user should not get private org membership of privateMemberUsername orgs = getUserOrgs(t, unrelatedUsername, privateMemberUsername) - assert.Len(t, orgs, 0) + assert.Empty(t, orgs) // not authenticated call should not be allowed testUserOrgsUnauthenticated(t, privateMemberUsername) diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go index e9805a51393..5604a14259b 100644 --- a/tests/integration/api_user_search_test.go +++ b/tests/integration/api_user_search_test.go @@ -49,7 +49,7 @@ func TestAPIUserSearchLoggedIn(t *testing.T) { for _, user := range results.Data { assert.Contains(t, user.UserName, query) assert.NotEmpty(t, user.Email) - assert.True(t, user.Visibility == "public") + assert.Equal(t, "public", user.Visibility) } } @@ -83,7 +83,7 @@ func TestAPIUserSearchSystemUsers(t *testing.T) { var results SearchResults DecodeJSON(t, resp, &results) assert.NotEmpty(t, results.Data) - if assert.EqualValues(t, 1, len(results.Data)) { + if assert.Len(t, results.Data, 1) { user := results.Data[0] assert.EqualValues(t, user.UserName, systemUser.Name) assert.EqualValues(t, user.ID, systemUser.ID) @@ -137,7 +137,7 @@ func TestAPIUserSearchByEmail(t *testing.T) { var results SearchResults DecodeJSON(t, resp, &results) - assert.Equal(t, 1, len(results.Data)) + assert.Len(t, results.Data, 1) assert.Equal(t, query, results.Data[0].Email) // no login user can not search user with private email @@ -155,6 +155,6 @@ func TestAPIUserSearchByEmail(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &results) - assert.Equal(t, 1, len(results.Data)) + assert.Len(t, results.Data, 1) assert.Equal(t, query, results.Data[0].Email) } diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 00ef72c1c3f..1837e827956 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -265,7 +265,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) tr := htmlDoc.doc.Find("table.table tbody tr") - assert.True(t, tr.Length() == 0) + assert.Equal(t, 0, tr.Length()) } for _, u := range gitLDAPUsers { diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index d1d935da4f9..43b151e0b6e 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -40,10 +40,10 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) { assert.NoError(t, err) // Setup ssh wrapper - os.Setenv("GIT_SSH", path.Join(tmpDir, "ssh")) - os.Setenv("GIT_SSH_COMMAND", + t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh")) + t.Setenv("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"") - os.Setenv("GIT_SSH_VARIANT", "ssh") + t.Setenv("GIT_SSH_VARIANT", "ssh") callback(keyFile) } diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go index dc0b52203a9..e68f8bfce2d 100644 --- a/tests/integration/git_push_test.go +++ b/tests/integration/git_push_test.go @@ -6,7 +6,6 @@ package integration import ( "fmt" "net/url" - "strings" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -211,6 +210,6 @@ func TestPushPullRefs(t *testing.T) { }) assert.Error(t, err) assert.Empty(t, stdout) - assert.False(t, strings.Contains(stderr, "[deleted]"), "stderr: %s", stderr) + assert.NotContains(t, stderr, "[deleted]", "stderr: %s", stderr) }) } diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go index 047c049c7f4..acfe70026e1 100644 --- a/tests/integration/gpg_git_test.go +++ b/tests/integration/gpg_git_test.go @@ -29,10 +29,7 @@ func TestGPGGit(t *testing.T) { err := os.Chmod(tmpDir, 0o700) assert.NoError(t, err) - oldGNUPGHome := os.Getenv("GNUPGHOME") - err = os.Setenv("GNUPGHOME", tmpDir) - assert.NoError(t, err) - defer os.Setenv("GNUPGHOME", oldGNUPGHome) + t.Setenv("GNUPGHOME", tmpDir) // Need to create a root key rootKeyPair, err := importTestingKey() diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 6b1b6b8b21a..9b3b2f2b924 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -278,7 +278,7 @@ func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth. resp = session.MakeRequest(t, req, http.StatusSeeOther) // Log the flash values on failure - if !assert.Equal(t, resp.Result().Header["Location"], []string{"/user/settings/applications"}) { + if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) { for _, cookie := range resp.Result().Cookies() { if cookie.Name != gitea_context.CookieNameFlash { continue @@ -453,16 +453,16 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile schemaFilePath := filepath.Join(filepath.Dir(setting.AppPath), "tests", "integration", "schemas", schemaFile) _, schemaFileErr := os.Stat(schemaFilePath) - assert.Nil(t, schemaFileErr) + assert.NoError(t, schemaFileErr) schema, schemaFileReadErr := os.ReadFile(schemaFilePath) - assert.Nil(t, schemaFileReadErr) - assert.True(t, len(schema) > 0) + assert.NoError(t, schemaFileReadErr) + assert.NotEmpty(t, schema) nodeinfoSchema := gojsonschema.NewStringLoader(string(schema)) nodeinfoString := gojsonschema.NewStringLoader(resp.Body.String()) result, schemaValidationErr := gojsonschema.Validate(nodeinfoSchema, nodeinfoString) - assert.Nil(t, schemaValidationErr) + assert.NoError(t, schemaValidationErr) assert.Empty(t, result.Errors()) assert.True(t, result.Valid()) } diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 627d1f89c41..462cb73eeed 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -59,7 +59,7 @@ func initMigrationTest(t *testing.T) func() { unittest.InitSettings() - assert.True(t, len(setting.RepoRootPath) != 0) + assert.NotEmpty(t, setting.RepoRootPath) assert.NoError(t, unittest.SyncDirs(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath)) assert.NoError(t, git.InitFull(context.Background())) setting.LoadDBSetting() diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 9ff4669befe..0dd8919bff9 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -78,7 +78,7 @@ func testMirrorPush(t *testing.T, u *url.URL) { assert.True(t, doRemovePushMirror(t, session, user.Name, srcRepo.Name, mirrors[0].ID)) mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{}) assert.NoError(t, err) - assert.Len(t, mirrors, 0) + assert.Empty(t, mirrors) } func testCreatePushMirror(t *testing.T, session *TestSession, owner, repo, address, username, password, interval string) { diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index f177bd3a23b..d6f1ba33ec8 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -89,7 +89,7 @@ func TestAuthorizeRedirectWithExistingGrant(t *testing.T) { u, err := resp.Result().Location() assert.NoError(t, err) assert.Equal(t, "thestate", u.Query().Get("state")) - assert.Truef(t, len(u.Query().Get("code")) > 30, "authorization code '%s' should be longer then 30", u.Query().Get("code")) + assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code")) u.RawQuery = "" assert.Equal(t, "https://example.com/xyzzy", u.String()) } @@ -125,8 +125,8 @@ func TestAccessTokenExchange(t *testing.T) { parsed := new(response) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed)) - assert.True(t, len(parsed.AccessToken) > 10) - assert.True(t, len(parsed.RefreshToken) > 10) + assert.Greater(t, len(parsed.AccessToken), 10) + assert.Greater(t, len(parsed.RefreshToken), 10) } func TestAccessTokenExchangeWithPublicClient(t *testing.T) { @@ -148,8 +148,8 @@ func TestAccessTokenExchangeWithPublicClient(t *testing.T) { parsed := new(response) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed)) - assert.True(t, len(parsed.AccessToken) > 10) - assert.True(t, len(parsed.RefreshToken) > 10) + assert.Greater(t, len(parsed.AccessToken), 10) + assert.Greater(t, len(parsed.RefreshToken), 10) } func TestAccessTokenExchangeJSON(t *testing.T) { @@ -172,8 +172,8 @@ func TestAccessTokenExchangeJSON(t *testing.T) { parsed := new(response) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed)) - assert.True(t, len(parsed.AccessToken) > 10) - assert.True(t, len(parsed.RefreshToken) > 10) + assert.Greater(t, len(parsed.AccessToken), 10) + assert.Greater(t, len(parsed.RefreshToken), 10) } func TestAccessTokenExchangeWithoutPKCE(t *testing.T) { @@ -289,8 +289,8 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) { parsed := new(response) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed)) - assert.True(t, len(parsed.AccessToken) > 10) - assert.True(t, len(parsed.RefreshToken) > 10) + assert.Greater(t, len(parsed.AccessToken), 10) + assert.Greater(t, len(parsed.RefreshToken), 10) // use wrong client_secret req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ @@ -449,8 +449,8 @@ func TestOAuthIntrospection(t *testing.T) { parsed := new(response) assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed)) - assert.True(t, len(parsed.AccessToken) > 10) - assert.True(t, len(parsed.RefreshToken) > 10) + assert.Greater(t, len(parsed.AccessToken), 10) + assert.Greater(t, len(parsed.RefreshToken), 10) // successful request with a valid client_id/client_secret and a valid token req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{ diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go index 6386f53f059..8a33c218bec 100644 --- a/tests/integration/org_count_test.go +++ b/tests/integration/org_count_test.go @@ -130,7 +130,7 @@ func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, ca calcOrgCounts[org.LowerName] = org.NumRepos count, ok := canonicalCounts[org.LowerName] if ok { - assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count) + assert.Equal(t, count, org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count) } else { assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos) } diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index ad0be72dcbd..106774aa548 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -40,7 +40,7 @@ func TestPullCompare(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() - assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") onGiteaRun(t, func(t *testing.T, u *url.URL) { defer tests.PrepareTestEnv(t)() @@ -58,7 +58,7 @@ func TestPullCompare(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() - assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") + assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -71,7 +71,7 @@ func TestPullCompare(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() - assert.EqualValues(t, editButtonCount, 0, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted") + assert.EqualValues(t, 0, editButtonCount, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted") }) } diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index eb3743bc176..1521fcfe8a8 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -661,7 +661,7 @@ func TestPullMergeIndexerNotifier(t *testing.T) { searchIssuesResp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) var apiIssuesBefore []*api.Issue DecodeJSON(t, searchIssuesResp, &apiIssuesBefore) - assert.Len(t, apiIssuesBefore, 0) + assert.Empty(t, apiIssuesBefore) // merge the pull request elem := strings.Split(test.RedirectURL(createPullResp), "/") diff --git a/tests/integration/repo_archive_test.go b/tests/integration/repo_archive_test.go index 664b04baf75..c64ad1193d9 100644 --- a/tests/integration/repo_archive_test.go +++ b/tests/integration/repo_archive_test.go @@ -29,5 +29,5 @@ func TestRepoDownloadArchive(t *testing.T) { bs, err := io.ReadAll(resp.Body) assert.NoError(t, err) assert.Empty(t, resp.Header().Get("Content-Encoding")) - assert.Equal(t, 320, len(bs)) + assert.Len(t, bs, 320) } diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index e7c98531795..267fd0d56e5 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -43,7 +43,7 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO link, exists = htmlDoc.doc.Find(`form.ui.form[action*="/fork"]`).Attr("action") assert.True(t, exists, "The template has changed") _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value") - assert.True(t, exists, fmt.Sprintf("Fork owner '%s' is not present in select box", forkOwnerName)) + assert.True(t, exists, "Fork owner '%s' is not present in select box", forkOwnerName) req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "uid": fmt.Sprintf("%d", forkOwner.ID), diff --git a/tests/integration/repo_generate_test.go b/tests/integration/repo_generate_test.go index 961255cedfb..ff2aa220d3b 100644 --- a/tests/integration/repo_generate_test.go +++ b/tests/integration/repo_generate_test.go @@ -41,7 +41,7 @@ func testRepoGenerate(t *testing.T, session *TestSession, templateID, templateOw link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action") assert.True(t, exists, "The template has changed") _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value") - assert.True(t, exists, fmt.Sprintf("Generate owner '%s' is not present in select box", generateOwnerName)) + assert.True(t, exists, "Generate owner '%s' is not present in select box", generateOwnerName) req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": htmlDoc.GetCSRF(), "uid": fmt.Sprintf("%d", generateOwner.ID), diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index 7889dfaf3b7..8c568a12723 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -228,7 +228,7 @@ func TestViewRepoDirectory(t *testing.T) { repoSummary := htmlDoc.doc.Find(".repository-summary") repoFilesTable := htmlDoc.doc.Find("#repo-files-table") - assert.NotZero(t, len(repoFilesTable.Nodes)) + assert.NotEmpty(t, repoFilesTable.Nodes) assert.Zero(t, description.Length()) assert.Zero(t, repoTopics.Length()) diff --git a/tests/integration/session_test.go b/tests/integration/session_test.go index d47148efa23..b18a25827de 100644 --- a/tests/integration/session_test.go +++ b/tests/integration/session_test.go @@ -28,10 +28,10 @@ func Test_RegenerateSession(t *testing.T) { sess, err := auth.RegenerateSession(db.DefaultContext, "", key) assert.NoError(t, err) assert.EqualValues(t, key, sess.Key) - assert.Len(t, sess.Data, 0) + assert.Empty(t, sess.Data) sess, err = auth.ReadSession(db.DefaultContext, key2) assert.NoError(t, err) assert.EqualValues(t, key2, sess.Key) - assert.Len(t, sess.Data, 0) + assert.Empty(t, sess.Data) } diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 99e413c6d95..5b6f28d1ff7 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -255,7 +255,7 @@ func TestListStopWatches(t *testing.T) { assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) - assert.Greater(t, apiWatches[0].Seconds, int64(0)) + assert.Positive(t, apiWatches[0].Seconds) } } From b01b0b99a596ff6f879b460aef9a115a2cd811a5 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Dec 2024 19:59:18 +0800 Subject: [PATCH 19/49] Refactor some LDAP code (#32849) --- services/auth/source/ldap/source.go | 5 +- .../auth/source/ldap/source_authenticate.go | 12 +- services/auth/source/ldap/source_search.go | 33 +- services/auth/source/ldap/source_sync.go | 20 +- services/auth/source/ldap/util.go | 6 +- tests/integration/auth_ldap_test.go | 366 +++++++++++------- 6 files changed, 264 insertions(+), 178 deletions(-) diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go index dc4cb2c9403..963cdba7c21 100644 --- a/services/auth/source/ldap/source.go +++ b/services/auth/source/ldap/source.go @@ -56,8 +56,7 @@ type Source struct { UserUID string // User Attribute listed in Group SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source - // reference to the authSource - authSource *auth.Source + authSource *auth.Source // reference to the authSource } // FromDB fills up a LDAPConfig from serialized format. @@ -107,7 +106,7 @@ func (source *Source) UseTLS() bool { // ProvidesSSHKeys returns if this source provides SSH Keys func (source *Source) ProvidesSSHKeys() bool { - return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + return strings.TrimSpace(source.AttributeSSHPublicKey) != "" } // SetAuthSource sets the related AuthSource diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 01cb7437205..020e5784dcf 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -31,13 +31,13 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u return nil, user_model.ErrUserNotExist{Name: loginName} } // Fallback. - if len(sr.Username) == 0 { + if sr.Username == "" { sr.Username = userName } - if len(sr.Mail) == 0 { + if sr.Mail == "" { sr.Mail = fmt.Sprintf("%s@localhost.local", sr.Username) } - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" // Update User admin flag if exist if isExist, err := user_model.IsUserExist(ctx, 0, sr.Username); err != nil { @@ -51,11 +51,11 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u } if user != nil && !user.ProhibitLogin { opts := &user_service.UpdateOptions{} - if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin { + if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin { // Change existing admin flag only if AdminFilter option is set opts.IsAdmin = optional.Some(sr.IsAdmin) } - if !sr.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted { + if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted { // Change existing restricted flag only if RestrictedFilter option is set opts.IsRestricted = optional.Some(sr.IsRestricted) } @@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u return user, err } } - if len(source.AttributeAvatar) > 0 { + if source.AttributeAvatar != "" { if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil { return user, err } diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index b20c90e791d..fa2c45ce4ad 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -147,7 +147,7 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error { } func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool { - if len(ls.AdminFilter) == 0 { + if ls.AdminFilter == "" { return false } log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN) @@ -169,7 +169,7 @@ func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool { } func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { - if len(ls.RestrictedFilter) == 0 { + if ls.RestrictedFilter == "" { return false } if ls.RestrictedFilter == "*" { @@ -250,8 +250,17 @@ func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult { + if MockedSearchEntry != nil { + return MockedSearchEntry(source, name, passwd, directBind) + } + return realSearchEntry(source, name, passwd, directBind) +} + +var MockedSearchEntry func(source *Source, name, passwd string, directBind bool) *SearchResult + +func realSearchEntry(source *Source, name, passwd string, directBind bool) *SearchResult { // See https://tools.ietf.org/search/rfc4513#section-5.1.2 - if len(passwd) == 0 { + if passwd == "" { log.Debug("Auth. failed for %s, password cannot be empty", name) return nil } @@ -323,17 +332,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR return nil } - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 - isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" + isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != "" attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail} - if len(strings.TrimSpace(source.UserUID)) > 0 { + if strings.TrimSpace(source.UserUID) != "" { attribs = append(attribs, source.UserUID) } if isAttributeSSHPublicKeySet { attribs = append(attribs, source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { attribs = append(attribs, source.AttributeAvatar) } @@ -375,7 +384,7 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR isRestricted = checkRestricted(l, source, userDN) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar) } @@ -440,14 +449,14 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { userFilter := fmt.Sprintf(source.Filter, "*") - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 - isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" + isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != "" attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.UserUID} if isAttributeSSHPublicKeySet { attribs = append(attribs, source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { attribs = append(attribs, source.AttributeAvatar) } @@ -503,7 +512,7 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { user.SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { user.Avatar = v.GetRawAttributeValue(source.AttributeAvatar) } diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index a6d6d2a0f2f..e817bf1fa93 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -25,7 +25,7 @@ import ( func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name) - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" var sshKeysNeedUpdate bool // Find all users with this login type - FIXME: Should this be an iterator? @@ -86,26 +86,26 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name) default: } - if len(su.Username) == 0 && len(su.Mail) == 0 { + if su.Username == "" && su.Mail == "" { continue } var usr *user_model.User - if len(su.Username) > 0 { + if su.Username != "" { usr = usernameUsers[su.LowerName] } - if usr == nil && len(su.Mail) > 0 { + if usr == nil && su.Mail != "" { usr = mailUsers[strings.ToLower(su.Mail)] } if usr != nil { keepActiveUsers.Add(usr.ID) - } else if len(su.Username) == 0 { + } else if su.Username == "" { // we cannot create the user if su.Username is empty continue } - if len(su.Mail) == 0 { + if su.Mail == "" { su.Mail = fmt.Sprintf("%s@localhost.local", su.Username) } @@ -141,7 +141,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } } - if err == nil && len(source.AttributeAvatar) > 0 { + if err == nil && source.AttributeAvatar != "" { _ = user_service.UploadAvatar(ctx, usr, su.Avatar) } } else if updateExisting { @@ -151,8 +151,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } // Check if user data has changed - if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) || - (len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) || + if (source.AdminFilter != "" && usr.IsAdmin != su.IsAdmin) || + (source.RestrictedFilter != "" && usr.IsRestricted != su.IsRestricted) || !strings.EqualFold(usr.Email, su.Mail) || usr.FullName != fullName || !usr.IsActive { @@ -180,7 +180,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } if usr.IsUploadAvatarChanged(su.Avatar) { - if err == nil && len(source.AttributeAvatar) > 0 { + if err == nil && source.AttributeAvatar != "" { _ = user_service.UploadAvatar(ctx, usr, su.Avatar) } } diff --git a/services/auth/source/ldap/util.go b/services/auth/source/ldap/util.go index bd11e2d1193..05ae32c0fd2 100644 --- a/services/auth/source/ldap/util.go +++ b/services/auth/source/ldap/util.go @@ -6,11 +6,11 @@ package ldap // composeFullName composes a firstname surname or username func composeFullName(firstname, surname, username string) string { switch { - case len(firstname) == 0 && len(surname) == 0: + case firstname == "" && surname == "": return username - case len(firstname) == 0: + case firstname == "": return surname - case len(surname) == 0: + case surname == "": return firstname default: return firstname + " " + surname diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 1837e827956..5d372443310 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -15,13 +15,17 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/ldap" org_service "code.gitea.io/gitea/services/org" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type ldapUser struct { @@ -35,87 +39,97 @@ type ldapUser struct { SSHKeys []string } -var gitLDAPUsers = []ldapUser{ - { - UserName: "professor", - Password: "professor", - FullName: "Hubert Farnsworth", - Email: "professor@planetexpress.com", - OtherEmails: []string{"hubert@planetexpress.com"}, - IsAdmin: true, - }, - { - UserName: "hermes", - Password: "hermes", - FullName: "Conrad Hermes", - Email: "hermes@planetexpress.com", - SSHKeys: []string{ - "SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8", - "SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ", - "SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI", +type ldapTestEnv struct { + gitLDAPUsers []ldapUser + otherLDAPUsers []ldapUser + serverHost string + serverPort string +} + +func prepareLdapTestEnv(t *testing.T) *ldapTestEnv { + if os.Getenv("TEST_LDAP") != "1" { + t.Skip() + return nil + } + + gitLDAPUsers := []ldapUser{ + { + UserName: "professor", + Password: "professor", + FullName: "Hubert Farnsworth", + Email: "professor@planetexpress.com", + OtherEmails: []string{"hubert@planetexpress.com"}, + IsAdmin: true, + }, + { + UserName: "hermes", + Password: "hermes", + FullName: "Conrad Hermes", + Email: "hermes@planetexpress.com", + SSHKeys: []string{ + "SHA256:qLY06smKfHoW/92yXySpnxFR10QFrLdRjf/GNPvwcW8", + "SHA256:QlVTuM5OssDatqidn2ffY+Lc4YA5Fs78U+0KOHI51jQ", + "SHA256:DXdeUKYOJCSSmClZuwrb60hUq7367j4fA+udNC3FdRI", + }, + IsAdmin: true, + }, + { + UserName: "fry", + Password: "fry", + FullName: "Philip Fry", + Email: "fry@planetexpress.com", + }, + { + UserName: "leela", + Password: "leela", + FullName: "Leela Turanga", + Email: "leela@planetexpress.com", + IsRestricted: true, + }, + { + UserName: "bender", + Password: "bender", + FullName: "Bender Rodríguez", + Email: "bender@planetexpress.com", }, - IsAdmin: true, - }, - { - UserName: "fry", - Password: "fry", - FullName: "Philip Fry", - Email: "fry@planetexpress.com", - }, - { - UserName: "leela", - Password: "leela", - FullName: "Leela Turanga", - Email: "leela@planetexpress.com", - IsRestricted: true, - }, - { - UserName: "bender", - Password: "bender", - FullName: "Bender Rodríguez", - Email: "bender@planetexpress.com", - }, -} - -var otherLDAPUsers = []ldapUser{ - { - UserName: "zoidberg", - Password: "zoidberg", - FullName: "John Zoidberg", - Email: "zoidberg@planetexpress.com", - }, - { - UserName: "amy", - Password: "amy", - FullName: "Amy Kroker", - Email: "amy@planetexpress.com", - }, -} - -func skipLDAPTests() bool { - return os.Getenv("TEST_LDAP") != "1" -} - -func getLDAPServerHost() string { - host := os.Getenv("TEST_LDAP_HOST") - if len(host) == 0 { - host = "ldap" } - return host -} -func getLDAPServerPort() string { - port := os.Getenv("TEST_LDAP_PORT") - if len(port) == 0 { - port = "389" + otherLDAPUsers := []ldapUser{ + { + UserName: "zoidberg", + Password: "zoidberg", + FullName: "John Zoidberg", + Email: "zoidberg@planetexpress.com", + }, + { + UserName: "amy", + Password: "amy", + FullName: "Amy Kroker", + Email: "amy@planetexpress.com", + }, + } + + return &ldapTestEnv{ + gitLDAPUsers: gitLDAPUsers, + otherLDAPUsers: otherLDAPUsers, + serverHost: util.IfZero(os.Getenv("TEST_LDAP_HOST"), "ldap"), + serverPort: util.IfZero(os.Getenv("TEST_LDAP_PORT"), "389"), } - return port } -func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval string) map[string]string { +type ldapAuthOptions struct { + attributeUID optional.Option[string] // defaults to "uid" + attributeSSHPublicKey string + groupFilter string + groupTeamMap string + groupTeamMapRemoval string +} + +func (te *ldapTestEnv) buildAuthSourcePayload(csrf string, opts ...ldapAuthOptions) map[string]string { + opt := util.OptionalArg(opts) // Modify user filter to test group filter explicitly userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))" - if groupFilter != "" { + if opt.groupFilter != "" { userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))" } @@ -123,53 +137,47 @@ func buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap "_csrf": csrf, "type": "2", "name": "ldap", - "host": getLDAPServerHost(), - "port": getLDAPServerPort(), + "host": te.serverHost, + "port": te.serverPort, "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com", "bind_password": "password", "user_base": "ou=people,dc=planetexpress,dc=com", "filter": userFilter, "admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)", "restricted_filter": "(uid=leela)", - "attribute_username": "uid", + "attribute_username": util.Iif(opt.attributeUID.Has(), opt.attributeUID.Value(), "uid"), "attribute_name": "givenName", "attribute_surname": "sn", "attribute_mail": "mail", - "attribute_ssh_public_key": sshKeyAttribute, + "attribute_ssh_public_key": opt.attributeSSHPublicKey, "is_sync_enabled": "on", "is_active": "on", "groups_enabled": "on", "group_dn": "ou=people,dc=planetexpress,dc=com", "group_member_uid": "member", - "group_filter": groupFilter, - "group_team_map": groupTeamMap, - "group_team_map_removal": groupTeamMapRemoval, + "group_filter": opt.groupFilter, + "group_team_map": opt.groupTeamMap, + "group_team_map_removal": opt.groupTeamMapRemoval, "user_uid": "DN", } } -func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupMapParams ...string) { - groupTeamMapRemoval := "off" - groupTeamMap := "" - if len(groupMapParams) == 2 { - groupTeamMapRemoval = groupMapParams[0] - groupTeamMap = groupMapParams[1] - } +func (te *ldapTestEnv) addAuthSource(t *testing.T, opts ...ldapAuthOptions) { session := loginUser(t, "user1") csrf := GetUserCSRFToken(t, session) - req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval)) + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", te.buildAuthSourcePayload(csrf, opts...)) session.MakeRequest(t, req, http.StatusSeeOther) } func TestLDAPUserSignin(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + te.addAuthSource(t) - u := gitLDAPUsers[0] + u := te.gitLDAPUsers[0] session := loginUserWithPassword(t, u.UserName, u.Password) req := NewRequest(t, "GET", "/user/settings") @@ -183,8 +191,13 @@ func TestLDAPUserSignin(t *testing.T) { } func TestLDAPAuthChange(t *testing.T) { + te := prepareLdapTestEnv(t) + if te == nil { + return + } + defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + te.addAuthSource(t) session := loginUser(t, "user1") req := NewRequest(t, "GET", "/-/admin/auths") @@ -201,34 +214,35 @@ func TestLDAPAuthChange(t *testing.T) { doc = NewHTMLParser(t, resp.Body) csrf := doc.GetCSRF() host, _ := doc.Find(`input[name="host"]`).Attr("value") - assert.Equal(t, host, getLDAPServerHost()) + assert.Equal(t, te.serverHost, host) binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) - req = NewRequestWithValues(t, "POST", href, buildAuthSourceLDAPPayload(csrf, "", "", "", "off")) + req = NewRequestWithValues(t, "POST", href, te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMapRemoval: "off"})) session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", href) resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) host, _ = doc.Find(`input[name="host"]`).Attr("value") - assert.Equal(t, host, getLDAPServerHost()) + assert.Equal(t, te.serverHost, host) binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value") assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) } func TestLDAPUserSync(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } + defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") + te.addAuthSource(t) err := auth.SyncExternalUsers(context.Background(), true) assert.NoError(t, err) // Check if users exists - for _, gitLDAPUser := range gitLDAPUsers { + for _, gitLDAPUser := range te.gitLDAPUsers { dbUser, err := user_model.GetUserByName(db.DefaultContext, gitLDAPUser.UserName) assert.NoError(t, err) assert.Equal(t, gitLDAPUser.UserName, dbUser.Name) @@ -238,27 +252,28 @@ func TestLDAPUserSync(t *testing.T) { } // Check if no users exist - for _, otherLDAPUser := range otherLDAPUsers { + for _, otherLDAPUser := range te.otherLDAPUsers { _, err := user_model.GetUserByName(db.DefaultContext, otherLDAPUser.UserName) assert.True(t, user_model.IsErrUserNotExist(err)) } } func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } + defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") csrf := GetUserCSRFToken(t, session) - payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "") + payload := te.buildAuthSourcePayload(csrf) payload["attribute_username"] = "" req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload) session.MakeRequest(t, req, http.StatusSeeOther) - for _, u := range gitLDAPUsers { + for _, u := range te.gitLDAPUsers { req := NewRequest(t, "GET", "/-/admin/users?q="+u.UserName) resp := session.MakeRequest(t, req, http.StatusOK) @@ -268,7 +283,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { assert.Equal(t, 0, tr.Length()) } - for _, u := range gitLDAPUsers { + for _, u := range te.gitLDAPUsers { req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{ "_csrf": csrf, "user_name": u.UserName, @@ -277,7 +292,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { MakeRequest(t, req, http.StatusSeeOther) } - auth.SyncExternalUsers(context.Background(), true) + require.NoError(t, auth.SyncExternalUsers(context.Background(), true)) authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{ Name: payload["name"], @@ -285,9 +300,9 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { unittest.AssertCount(t, &user_model.User{ LoginType: auth_model.LDAP, LoginSource: authSource.ID, - }, len(gitLDAPUsers)) + }, len(te.gitLDAPUsers)) - for _, u := range gitLDAPUsers { + for _, u := range te.gitLDAPUsers { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ Name: u.UserName, }) @@ -296,12 +311,13 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { } func TestLDAPUserSyncWithGroupFilter(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } + defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "(cn=git)") + te.addAuthSource(t, ldapAuthOptions{groupFilter: "(cn=git)"}) // Assert a user not a member of the LDAP group "cn=git" cannot login // This test may look like TestLDAPUserSigninFailed but it is not. @@ -309,20 +325,20 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { // This test is for the case when LDAP user records may not be linked with // all groups the user is a member of, the user filter is modified accordingly inside // the addAuthSourceLDAP based on the value of the groupFilter - u := otherLDAPUsers[0] + u := te.otherLDAPUsers[0] testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) - auth.SyncExternalUsers(context.Background(), true) + require.NoError(t, auth.SyncExternalUsers(context.Background(), true)) // Assert members of LDAP group "cn=git" are added - for _, gitLDAPUser := range gitLDAPUsers { + for _, gitLDAPUser := range te.gitLDAPUsers { unittest.BeanExists(t, &user_model.User{ Name: gitLDAPUser.UserName, }) } // Assert everyone else is not added - for _, gitLDAPUser := range otherLDAPUsers { + for _, gitLDAPUser := range te.otherLDAPUsers { unittest.AssertNotExistsBean(t, &user_model.User{ Name: gitLDAPUser.UserName, }) @@ -333,11 +349,11 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { }) ldapConfig := ldapSource.Cfg.(*ldap.Source) ldapConfig.GroupFilter = "(cn=ship_crew)" - auth_model.UpdateSource(db.DefaultContext, ldapSource) + require.NoError(t, auth_model.UpdateSource(db.DefaultContext, ldapSource)) - auth.SyncExternalUsers(context.Background(), true) + require.NoError(t, auth.SyncExternalUsers(context.Background(), true)) - for _, gitLDAPUser := range gitLDAPUsers { + for _, gitLDAPUser := range te.gitLDAPUsers { if gitLDAPUser.UserName == "fry" || gitLDAPUser.UserName == "leela" || gitLDAPUser.UserName == "bender" { // Assert members of the LDAP group "cn-ship_crew" are still active user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ @@ -355,29 +371,31 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { } func TestLDAPUserSigninFailed(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } - defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "") - u := otherLDAPUsers[0] + defer tests.PrepareTestEnv(t)() + te.addAuthSource(t) + + u := te.otherLDAPUsers[0] testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) } func TestLDAPUserSSHKeySync(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } - defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "sshPublicKey", "") - auth.SyncExternalUsers(context.Background(), true) + defer tests.PrepareTestEnv(t)() + te.addAuthSource(t, ldapAuthOptions{attributeSSHPublicKey: "sshPublicKey"}) + + require.NoError(t, auth.SyncExternalUsers(context.Background(), true)) // Check if users has SSH keys synced - for _, u := range gitLDAPUsers { + for _, u := range te.gitLDAPUsers { if len(u.SSHKeys) == 0 { continue } @@ -400,18 +418,22 @@ func TestLDAPUserSSHKeySync(t *testing.T) { } func TestLDAPGroupTeamSyncAddMember(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } + defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`) + te.addAuthSource(t, ldapAuthOptions{ + groupTeamMap: `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`, + groupTeamMapRemoval: "on", + }) org, err := organization.GetOrgByName(db.DefaultContext, "org26") assert.NoError(t, err) team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") assert.NoError(t, err) - auth.SyncExternalUsers(context.Background(), true) - for _, gitLDAPUser := range gitLDAPUsers { + require.NoError(t, auth.SyncExternalUsers(context.Background(), true)) + for _, gitLDAPUser := range te.gitLDAPUsers { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ Name: gitLDAPUser.UserName, }) @@ -445,19 +467,22 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) { } func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } defer tests.PrepareTestEnv(t)() - addAuthSourceLDAP(t, "", "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`) + te.addAuthSource(t, ldapAuthOptions{ + groupTeamMap: `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`, + groupTeamMapRemoval: "on", + }) org, err := organization.GetOrgByName(db.DefaultContext, "org26") assert.NoError(t, err) team, err := organization.GetTeam(db.DefaultContext, org.ID, "team11") assert.NoError(t, err) - loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password) + loginUserWithPassword(t, te.gitLDAPUsers[0].UserName, te.gitLDAPUsers[0].Password) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ - Name: gitLDAPUsers[0].UserName, + Name: te.gitLDAPUsers[0].UserName, }) err = organization.AddOrgUser(db.DefaultContext, org.ID, user.ID) assert.NoError(t, err) @@ -470,7 +495,7 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { assert.NoError(t, err) assert.True(t, isMember, "User should be member of this team") // assert team member "professor" gets removed from org26 team11 - loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password) + loginUserWithPassword(t, te.gitLDAPUsers[0].UserName, te.gitLDAPUsers[0].Password) isMember, err = organization.IsOrganizationMember(db.DefaultContext, org.ID, user.ID) assert.NoError(t, err) assert.False(t, isMember, "User membership should have been removed from organization") @@ -480,14 +505,67 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { } func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) { - if skipLDAPTests() { - t.Skip() + te := prepareLdapTestEnv(t) + if te == nil { return } defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") csrf := GetUserCSRFToken(t, session) - req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) + payload := te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMap: `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, groupTeamMapRemoval: "off"}) + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload) session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok } + +func TestLDAPEmailSignin(t *testing.T) { + te := ldapTestEnv{ + gitLDAPUsers: []ldapUser{ + { + UserName: "u1", + Password: "xx", + FullName: "user 1", + Email: "u1@gitea.com", + }, + }, + serverHost: "mock-host", + serverPort: "mock-port", + } + defer test.MockVariableValue(&ldap.MockedSearchEntry, func(source *ldap.Source, name, passwd string, directBind bool) *ldap.SearchResult { + var u *ldapUser + for _, user := range te.gitLDAPUsers { + if user.Email == name && user.Password == passwd { + u = &user + break + } + } + if u == nil { + return nil + } + result := &ldap.SearchResult{ + Username: u.UserName, + Mail: u.Email, + LowerName: strings.ToLower(u.UserName), + } + nameFields := strings.Split(u.FullName, " ") + result.Name = nameFields[0] + if len(nameFields) > 1 { + result.Surname = nameFields[1] + } + return result + })() + defer tests.PrepareTestEnv(t)() + te.addAuthSource(t) + + u := te.gitLDAPUsers[0] + + session := loginUserWithPassword(t, u.Email, u.Password) + req := NewRequest(t, "GET", "/user/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) + assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) + assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text()) +} From 74b06d4f5cc8dd11140a778768d384c4240ecd66 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Dec 2024 17:56:56 +0100 Subject: [PATCH 20/49] Repo file list enhancements (#32835) 1. restore background color 2. fix border radius on top/bottom and on hover 3. parent link is now full-row again, much easier to click 4. parent link now uses directory icon, matching github 5 changed grid layout to remove auto width on file name column which could get too small. 6. mobile layout now shows more of the filename. --------- Co-authored-by: wxiaoguang --- templates/repo/view_list.tmpl | 6 ++-- web_src/css/repo/home-file-list.css | 44 +++++++++++++++++------- web_src/css/themes/theme-gitea-dark.css | 1 + web_src/css/themes/theme-gitea-light.css | 1 + 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 0fdb45e574c..2d555e4c2e9 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -5,9 +5,9 @@
{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
{{if .HasParentPath}} -
- {{svg "octicon-reply"}} .. -
+ + {{svg "octicon-file-directory-fill"}} .. + {{end}} {{range $item := .Files}}
diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index 285b823d57a..19ba1f2bcbe 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -1,12 +1,23 @@ #repo-files-table { width: 100%; display: grid; - grid-template-columns: auto 1fr auto; - border: 1px solid var(--color-light-border); + grid-template-columns: 2fr 3fr auto; + border: 1px solid var(--color-secondary); + background: var(--color-box-body); border-radius: var(--border-radius); margin: 10px 0; /* match the "clone-panel-popup" margin to avoid "visual double-border" */ } +@media (max-width: 767.98px) { + #repo-files-table { + grid-template-columns: auto 1fr auto; + } +} + +#repo-files-table .repo-file-cell.name .svg { + margin-right: 2px; +} + #repo-files-table .svg.octicon-file-directory-fill, #repo-files-table .svg.octicon-file-submodule { color: var(--color-primary); @@ -22,18 +33,28 @@ display: contents; } -#repo-files-table .repo-file-item:hover > .repo-file-cell { - background: var(--color-hover); +#repo-files-table .repo-file-item:hover > .repo-file-cell, +#repo-files-table .parent-link:hover { + background: var(--color-hover-opaque); } #repo-files-table .repo-file-line, #repo-files-table .repo-file-cell { - border-top: 1px solid var(--color-light-border); + border-top: 1px solid var(--color-secondary); padding: 8px 10px; } #repo-files-table .repo-file-line:first-child { border-top: none; + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +#repo-files-table .repo-file-item:last-child .repo-file-cell:first-child { + border-bottom-left-radius: calc(var(--border-radius) - 1px); +} + +#repo-files-table .repo-file-item:last-child .repo-file-cell:last-child { + border-bottom-right-radius: calc(var(--border-radius) - 1px); } #repo-files-table .repo-file-line { @@ -48,12 +69,17 @@ } #repo-files-table .repo-file-cell.name { - max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +@media (max-width: 767.98px) { + #repo-files-table .repo-file-cell.name { + max-width: 35vw; + } +} + #repo-files-table .repo-file-cell.message { white-space: nowrap; overflow: hidden; @@ -66,9 +92,3 @@ white-space: nowrap; color: var(--color-text-light-1); } - -@media (max-width: 767.98px) { - #repo-files-table .repo-file-cell.name { - max-width: 150px; - } -} diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 85de7e210a0..9bc77476976 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -203,6 +203,7 @@ --color-light-mimic-enabled: rgba(0, 0, 0, calc(40 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #e8f3ff28; --color-hover: #e8f3ff19; + --color-hover-opaque: #21252a; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ --color-active: #e8f3ff24; --color-menu: #171a1e; --color-card: #171a1e; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 0bdfd076d69..d7f9debf900 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -203,6 +203,7 @@ --color-light-mimic-enabled: rgba(0, 0, 0, calc(6 / 255 * 222 / 255 / var(--opacity-disabled))); --color-light-border: #0000171d; --color-hover: #00001708; + --color-hover-opaque: #f1f3f5; /* TODO: color-mix(in srgb, var(--color-body), var(--color-hover)); */ --color-active: #00001714; --color-menu: #f8f9fb; --color-card: #f8f9fb; From c8ea41b049c887794e4dd87b690b3031b98458b9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Dec 2024 22:02:32 +0100 Subject: [PATCH 21/49] Fix remaining typescript issues, enable `tsc` (#32840) Fixes 79 typescript errors. Discovered at least two bugs in `notifications.ts`, and I'm pretty sure this feature was at least partially broken and may still be, I don't really know how to test it. After this, only like ~10 typescript errors remain in the codebase but those are harder to solve. --------- Co-authored-by: wxiaoguang --- Makefile | 8 +-- package-lock.json | 62 +++++++++---------- package.json | 4 +- tsconfig.json | 3 +- web_src/js/features/common-issue-list.ts | 11 ++-- .../js/features/comp/ComboMarkdownEditor.ts | 4 +- .../js/features/comp/EasyMDEToolbarActions.ts | 44 ++++++------- web_src/js/features/comp/ReactionSelector.ts | 2 +- web_src/js/features/comp/WebHookEditor.ts | 2 +- web_src/js/features/dropzone.ts | 13 ++-- web_src/js/features/emoji.ts | 2 +- .../js/features/eventsource.sharedworker.ts | 7 ++- web_src/js/features/heatmap.ts | 4 +- web_src/js/features/install.ts | 36 +++++------ web_src/js/features/notification.ts | 31 +++++----- web_src/js/features/oauth2-settings.ts | 8 ++- web_src/js/features/pull-view-file.ts | 4 +- web_src/js/features/repo-editor.ts | 2 +- web_src/js/features/repo-search.ts | 7 ++- .../features/repo-settings-branches.test.ts | 5 +- web_src/js/features/tribute.ts | 1 + web_src/js/globals.d.ts | 15 ++++- web_src/js/modules/tippy.ts | 1 - web_src/js/utils.ts | 10 +-- 24 files changed, 152 insertions(+), 134 deletions(-) diff --git a/Makefile b/Makefile index d5b779f1e59..4889958c3b6 100644 --- a/Makefile +++ b/Makefile @@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig .PHONY: lint-js lint-js: node_modules npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) -# npx vue-tsc + npx vue-tsc .PHONY: lint-js-fix lint-js-fix: node_modules npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix -# npx vue-tsc + npx vue-tsc .PHONY: lint-css lint-css: node_modules @@ -451,10 +451,6 @@ lint-templates: .venv node_modules lint-yaml: .venv @poetry run yamllint . -.PHONY: tsc -tsc: - npx vue-tsc - .PHONY: watch watch: @bash tools/watch.sh diff --git a/package-lock.json b/package-lock.json index 4764282f65e..8755cfe06f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.0", + "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", @@ -111,8 +112,7 @@ "type-fest": "4.30.0", "updates": "16.4.0", "vite-string-plugin": "1.3.4", - "vitest": "2.1.8", - "vue-tsc": "2.1.10" + "vitest": "2.1.8" }, "engines": { "node": ">= 18.0.0" @@ -3833,6 +3833,24 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@silverwind/vue-tsc": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@silverwind/vue-tsc/-/vue-tsc-2.1.13.tgz", + "integrity": "sha512-ejFxz1KZiUGAESbC+eURnjqt0N95qkU9eZU7W15wgF9zV+v2FEu3ZLduuXTC7D/Sg6lL1R/QjPfUbxbAbBQOsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.1.10", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/@silverwind/vue3-calendar-heatmap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz", @@ -5335,30 +5353,30 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz", - "integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.10" + "@volar/source-map": "2.4.11" } }, "node_modules/@volar/source-map": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz", - "integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz", - "integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.10", + "@volar/language-core": "2.4.11", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -15780,24 +15798,6 @@ } } }, - "node_modules/vue-tsc": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", - "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "~2.4.8", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", diff --git a/package.json b/package.json index 275ca898e29..61e65c1f43e 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.0", + "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", @@ -110,8 +111,7 @@ "type-fest": "4.30.0", "updates": "16.4.0", "vite-string-plugin": "1.3.4", - "vitest": "2.1.8", - "vue-tsc": "2.1.10" + "vitest": "2.1.8" }, "browserslist": [ "defaults" diff --git a/tsconfig.json b/tsconfig.json index e006535c02c..7d0316db299 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ ], "compilerOptions": { "target": "es2020", - "module": "nodenext", + "module": "esnext", + "moduleResolution": "bundler", "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], "allowImportingTsExtensions": true, "allowJs": true, diff --git a/web_src/js/features/common-issue-list.ts b/web_src/js/features/common-issue-list.ts index e8a47eabad6..e2073647940 100644 --- a/web_src/js/features/common-issue-list.ts +++ b/web_src/js/features/common-issue-list.ts @@ -7,7 +7,7 @@ const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123" const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}" // if the searchText can be parsed to an "issue goto link", return the link, otherwise return empty string -export function parseIssueListQuickGotoLink(repoLink, searchText) { +export function parseIssueListQuickGotoLink(repoLink: string, searchText: string) { searchText = searchText.trim(); let targetUrl = ''; if (repoLink) { @@ -15,13 +15,12 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) { if (reIssueIndex.test(searchText)) { targetUrl = `${repoLink}/issues/${searchText}`; } else if (reIssueSharpIndex.test(searchText)) { - targetUrl = `${repoLink}/issues/${searchText.substr(1)}`; + targetUrl = `${repoLink}/issues/${searchText.substring(1)}`; } } else { // try to parse it for a global search (eg: "owner/repo#123") - const matchIssueOwnerRepoIndex = searchText.match(reIssueOwnerRepoIndex); - if (matchIssueOwnerRepoIndex) { - const [_, owner, repo, index] = matchIssueOwnerRepoIndex; + const [_, owner, repo, index] = reIssueOwnerRepoIndex.exec(searchText) || []; + if (owner) { targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`; } } @@ -33,7 +32,7 @@ export function initCommonIssueListQuickGoto() { if (!goto) return; const form = goto.closest('form'); - const input = form.querySelector('input[name=q]'); + const input = form.querySelector('input[name=q]'); const repoLink = goto.getAttribute('data-repo-link'); form.addEventListener('submit', (e) => { diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 80eabaa37ae..bba50a1296f 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -283,8 +283,8 @@ export class ComboMarkdownEditor { ]; } - parseEasyMDEToolbar(EasyMDE, actions) { - this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(EasyMDE, this); + parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) { + this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this); const processed = []; for (const action of actions) { const actionButton = this.easyMDEToolbarActions[action]; diff --git a/web_src/js/features/comp/EasyMDEToolbarActions.ts b/web_src/js/features/comp/EasyMDEToolbarActions.ts index d91dd23d115..ec5c7304bef 100644 --- a/web_src/js/features/comp/EasyMDEToolbarActions.ts +++ b/web_src/js/features/comp/EasyMDEToolbarActions.ts @@ -1,100 +1,102 @@ import {svg} from '../../svg.ts'; +import type EasyMDE from 'easymde'; +import type {ComboMarkdownEditor} from './ComboMarkdownEditor.ts'; -export function easyMDEToolbarActions(EasyMDE, editor) { - const actions = { +export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record> { + const actions: Record | string> = { '|': '|', 'heading-1': { - action: EasyMDE.toggleHeading1, + action: easyMde.toggleHeading1, icon: svg('octicon-heading'), title: 'Heading 1', }, 'heading-2': { - action: EasyMDE.toggleHeading2, + action: easyMde.toggleHeading2, icon: svg('octicon-heading'), title: 'Heading 2', }, 'heading-3': { - action: EasyMDE.toggleHeading3, + action: easyMde.toggleHeading3, icon: svg('octicon-heading'), title: 'Heading 3', }, 'heading-smaller': { - action: EasyMDE.toggleHeadingSmaller, + action: easyMde.toggleHeadingSmaller, icon: svg('octicon-heading'), title: 'Decrease Heading', }, 'heading-bigger': { - action: EasyMDE.toggleHeadingBigger, + action: easyMde.toggleHeadingBigger, icon: svg('octicon-heading'), title: 'Increase Heading', }, 'bold': { - action: EasyMDE.toggleBold, + action: easyMde.toggleBold, icon: svg('octicon-bold'), title: 'Bold', }, 'italic': { - action: EasyMDE.toggleItalic, + action: easyMde.toggleItalic, icon: svg('octicon-italic'), title: 'Italic', }, 'strikethrough': { - action: EasyMDE.toggleStrikethrough, + action: easyMde.toggleStrikethrough, icon: svg('octicon-strikethrough'), title: 'Strikethrough', }, 'quote': { - action: EasyMDE.toggleBlockquote, + action: easyMde.toggleBlockquote, icon: svg('octicon-quote'), title: 'Quote', }, 'code': { - action: EasyMDE.toggleCodeBlock, + action: easyMde.toggleCodeBlock, icon: svg('octicon-code'), title: 'Code', }, 'link': { - action: EasyMDE.drawLink, + action: easyMde.drawLink, icon: svg('octicon-link'), title: 'Link', }, 'unordered-list': { - action: EasyMDE.toggleUnorderedList, + action: easyMde.toggleUnorderedList, icon: svg('octicon-list-unordered'), title: 'Unordered List', }, 'ordered-list': { - action: EasyMDE.toggleOrderedList, + action: easyMde.toggleOrderedList, icon: svg('octicon-list-ordered'), title: 'Ordered List', }, 'image': { - action: EasyMDE.drawImage, + action: easyMde.drawImage, icon: svg('octicon-image'), title: 'Image', }, 'table': { - action: EasyMDE.drawTable, + action: easyMde.drawTable, icon: svg('octicon-table'), title: 'Table', }, 'horizontal-rule': { - action: EasyMDE.drawHorizontalRule, + action: easyMde.drawHorizontalRule, icon: svg('octicon-horizontal-rule'), title: 'Horizontal Rule', }, 'preview': { - action: EasyMDE.togglePreview, + action: easyMde.togglePreview, icon: svg('octicon-eye'), title: 'Preview', }, 'fullscreen': { - action: EasyMDE.toggleFullScreen, + action: easyMde.toggleFullScreen, icon: svg('octicon-screen-full'), title: 'Fullscreen', }, 'side-by-side': { - action: EasyMDE.toggleSideBySide, + action: easyMde.toggleSideBySide, icon: svg('octicon-columns'), title: 'Side by Side', }, diff --git a/web_src/js/features/comp/ReactionSelector.ts b/web_src/js/features/comp/ReactionSelector.ts index 1e955c7ab40..671bade3bec 100644 --- a/web_src/js/features/comp/ReactionSelector.ts +++ b/web_src/js/features/comp/ReactionSelector.ts @@ -3,7 +3,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts'; export function initCompReactionSelector(parent: ParentNode = document) { for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) { - container.addEventListener('click', async (e) => { + container.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => { // there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment const target = e.target.closest('.comment-reaction-button'); if (!target) return; diff --git a/web_src/js/features/comp/WebHookEditor.ts b/web_src/js/features/comp/WebHookEditor.ts index b13a2ffca3d..203396af80a 100644 --- a/web_src/js/features/comp/WebHookEditor.ts +++ b/web_src/js/features/comp/WebHookEditor.ts @@ -23,7 +23,7 @@ export function initCompWebHookEditor() { } // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field - const httpMethodInput = document.querySelector('#http_method'); + const httpMethodInput = document.querySelector('#http_method'); if (httpMethodInput) { const updateContentType = function () { const visible = httpMethodInput.value === 'POST'; diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index c9b0149df58..666c6452304 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -6,6 +6,7 @@ import {GET, POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; import {isImageFile, isVideoFile} from '../utils.ts'; +import type {DropzoneFile} from 'dropzone/index.js'; const {csrfToken, i18n} = window.config; @@ -15,14 +16,14 @@ export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; async function createDropzone(el, opts) { - const [{Dropzone}] = await Promise.all([ + const [{default: Dropzone}] = await Promise.all([ import(/* webpackChunkName: "dropzone" */'dropzone'), import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), ]); return new Dropzone(el, opts); } -export function generateMarkdownLinkForAttachment(file, {width, dppx} = {}) { +export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) { let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; if (isImageFile(file)) { fileMarkdown = `!${fileMarkdown}`; @@ -60,14 +61,14 @@ function addCopyLink(file) { /** * @param {HTMLElement} dropzoneEl */ -export async function initDropzone(dropzoneEl) { +export async function initDropzone(dropzoneEl: HTMLElement) { const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url'); const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url'); const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone - const opts = { + const opts: Record = { url: dropzoneEl.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts'), @@ -88,7 +89,7 @@ export async function initDropzone(dropzoneEl) { // "http://localhost:3000/owner/repo/issues/[object%20Event]" // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '' const dzInst = await createDropzone(dropzoneEl, opts); - dzInst.on('success', (file, resp) => { + dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => { file.uuid = resp.uuid; fileUuidDict[file.uuid] = {submitted: false}; const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); @@ -97,7 +98,7 @@ export async function initDropzone(dropzoneEl) { dzInst.emit(DropzoneCustomEventUploadDone, {file}); }); - dzInst.on('removedfile', async (file) => { + dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => { if (disableRemovedfileEvent) return; dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts index 032a3efe8aa..933aa951c54 100644 --- a/web_src/js/features/emoji.ts +++ b/web_src/js/features/emoji.ts @@ -1,4 +1,4 @@ -import emojis from '../../../assets/emoji.json'; +import emojis from '../../../assets/emoji.json' with {type: 'json'}; const {assetUrlPrefix, customEmojis} = window.config; diff --git a/web_src/js/features/eventsource.sharedworker.ts b/web_src/js/features/eventsource.sharedworker.ts index 62581cf687d..991c92cc8e8 100644 --- a/web_src/js/features/eventsource.sharedworker.ts +++ b/web_src/js/features/eventsource.sharedworker.ts @@ -2,6 +2,11 @@ const sourcesByUrl = {}; const sourcesByPort = {}; class Source { + url: string; + eventSource: EventSource; + listening: Record; + clients: Array; + constructor(url) { this.url = url; this.eventSource = new EventSource(url); @@ -67,7 +72,7 @@ class Source { } } -self.addEventListener('connect', (e) => { +self.addEventListener('connect', (e: Event & {ports: Array}) => { for (const port of e.ports) { port.addEventListener('message', (event) => { if (!self.EventSource) { diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 69cd069a94f..53eebc93e55 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -21,8 +21,8 @@ export function initHeatmap() { // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8 const locale = { heatMapLocale: { - months: new Array(12).fill().map((_, idx) => translateMonth(idx)), - days: new Array(7).fill().map((_, idx) => translateDay(idx)), + months: new Array(12).fill(undefined).map((_, idx) => translateMonth(idx)), + days: new Array(7).fill(undefined).map((_, idx) => translateDay(idx)), on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday" more: el.getAttribute('data-locale-more'), less: el.getAttribute('data-locale-less'), diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index 3defb7904aa..725dcafab08 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -22,9 +22,9 @@ function initPreInstall() { mssql: '127.0.0.1:1433', }; - const dbHost = document.querySelector('#db_host'); - const dbUser = document.querySelector('#db_user'); - const dbName = document.querySelector('#db_name'); + const dbHost = document.querySelector('#db_host'); + const dbUser = document.querySelector('#db_user'); + const dbName = document.querySelector('#db_name'); // Database type change detection. document.querySelector('#db_type').addEventListener('change', function () { @@ -48,12 +48,12 @@ function initPreInstall() { }); document.querySelector('#db_type').dispatchEvent(new Event('change')); - const appUrl = document.querySelector('#app_url'); + const appUrl = document.querySelector('#app_url'); if (appUrl.value.includes('://localhost')) { appUrl.value = window.location.href; } - const domain = document.querySelector('#domain'); + const domain = document.querySelector('#domain'); if (domain.value.trim() === 'localhost') { domain.value = window.location.hostname; } @@ -61,43 +61,43 @@ function initPreInstall() { // TODO: better handling of exclusive relations. document.querySelector('#offline-mode input').addEventListener('change', function () { if (this.checked) { - document.querySelector('#disable-gravatar input').checked = true; - document.querySelector('#federated-avatar-lookup input').checked = false; + document.querySelector('#disable-gravatar input').checked = true; + document.querySelector('#federated-avatar-lookup input').checked = false; } }); document.querySelector('#disable-gravatar input').addEventListener('change', function () { if (this.checked) { - document.querySelector('#federated-avatar-lookup input').checked = false; + document.querySelector('#federated-avatar-lookup input').checked = false; } else { - document.querySelector('#offline-mode input').checked = false; + document.querySelector('#offline-mode input').checked = false; } }); document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () { if (this.checked) { - document.querySelector('#disable-gravatar input').checked = false; - document.querySelector('#offline-mode input').checked = false; + document.querySelector('#disable-gravatar input').checked = false; + document.querySelector('#offline-mode input').checked = false; } }); document.querySelector('#enable-openid-signin input').addEventListener('change', function () { if (this.checked) { - if (!document.querySelector('#disable-registration input').checked) { - document.querySelector('#enable-openid-signup input').checked = true; + if (!document.querySelector('#disable-registration input').checked) { + document.querySelector('#enable-openid-signup input').checked = true; } } else { - document.querySelector('#enable-openid-signup input').checked = false; + document.querySelector('#enable-openid-signup input').checked = false; } }); document.querySelector('#disable-registration input').addEventListener('change', function () { if (this.checked) { - document.querySelector('#enable-captcha input').checked = false; - document.querySelector('#enable-openid-signup input').checked = false; + document.querySelector('#enable-captcha input').checked = false; + document.querySelector('#enable-openid-signup input').checked = false; } else { - document.querySelector('#enable-openid-signup input').checked = true; + document.querySelector('#enable-openid-signup input').checked = true; } }); document.querySelector('#enable-captcha input').addEventListener('change', function () { if (this.checked) { - document.querySelector('#disable-registration input').checked = false; + document.querySelector('#disable-registration input').checked = false; } }); } diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index 539f7790561..5cdcd967f06 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -14,25 +14,25 @@ export function initNotificationsTable() { window.addEventListener('pageshow', (e) => { if (e.persisted) { // page was restored from bfcache const table = document.querySelector('#notification_table'); - const unreadCountEl = document.querySelector('.notifications-unread-count'); + const unreadCountEl = document.querySelector('.notifications-unread-count'); let unreadCount = parseInt(unreadCountEl.textContent); for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { item.remove(); unreadCount -= 1; } - unreadCountEl.textContent = unreadCount; + unreadCountEl.textContent = String(unreadCount); } }); // mark clicked unread links for deletion on bfcache restore for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { - link.addEventListener('click', (e) => { + link.addEventListener('click', (e : MouseEvent & {target: HTMLElement}) => { e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); }); } } -async function receiveUpdateCount(event) { +async function receiveUpdateCount(event: MessageEvent) { try { const data = JSON.parse(event.data); @@ -50,7 +50,7 @@ export function initNotificationCount() { if (!document.querySelector('.notification_count')) return; let usingPeriodicPoller = false; - const startPeriodicPoller = (timeout, lastCount) => { + const startPeriodicPoller = (timeout: number, lastCount?: number) => { if (timeout <= 0 || !Number.isFinite(timeout)) return; usingPeriodicPoller = true; lastCount = lastCount ?? getCurrentCount(); @@ -72,13 +72,13 @@ export function initNotificationCount() { type: 'start', url: `${window.location.origin}${appSubUrl}/user/events`, }); - worker.port.addEventListener('message', (event) => { + worker.port.addEventListener('message', (event: MessageEvent) => { if (!event.data || !event.data.type) { console.error('unknown worker message event', event); return; } if (event.data.type === 'notification-count') { - const _promise = receiveUpdateCount(event.data); + receiveUpdateCount(event); // no await } else if (event.data.type === 'no-event-source') { // browser doesn't support EventSource, falling back to periodic poller if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); @@ -118,10 +118,10 @@ export function initNotificationCount() { } function getCurrentCount() { - return document.querySelector('.notification_count').textContent; + return Number(document.querySelector('.notification_count').textContent ?? '0'); } -async function updateNotificationCountWithCallback(callback, timeout, lastCount) { +async function updateNotificationCountWithCallback(callback: (timeout: number, newCount: number) => void, timeout: number, lastCount: number) { const currentCount = getCurrentCount(); if (lastCount !== currentCount) { callback(notificationSettings.MinTimeout, currentCount); @@ -149,10 +149,9 @@ async function updateNotificationTable() { if (notificationDiv) { try { const params = new URLSearchParams(window.location.search); - params.set('div-only', true); - params.set('sequence-number', ++notificationSequenceNumber); - const url = `${appSubUrl}/notifications?${params.toString()}`; - const response = await GET(url); + params.set('div-only', String(true)); + params.set('sequence-number', String(++notificationSequenceNumber)); + const response = await GET(`${appSubUrl}/notifications?${params.toString()}`); if (!response.ok) { throw new Error('Failed to fetch notification table'); @@ -169,7 +168,7 @@ async function updateNotificationTable() { } } -async function updateNotificationCount() { +async function updateNotificationCount(): Promise { try { const response = await GET(`${appSubUrl}/notifications/new`); @@ -185,9 +184,9 @@ async function updateNotificationCount() { el.textContent = `${data.new}`; } - return `${data.new}`; + return data.new as number; } catch (error) { console.error(error); - return '0'; + return 0; } } diff --git a/web_src/js/features/oauth2-settings.ts b/web_src/js/features/oauth2-settings.ts index 1e62ca00964..a206bc8912f 100644 --- a/web_src/js/features/oauth2-settings.ts +++ b/web_src/js/features/oauth2-settings.ts @@ -1,5 +1,7 @@ export function initOAuth2SettingsDisableCheckbox() { - for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => { - document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked); - }); + for (const el of document.querySelectorAll('.disable-setting')) { + el.addEventListener('change', (e: Event & {target: HTMLInputElement}) => { + document.querySelector(e.target.getAttribute('data-target')).classList.toggle('disabled', e.target.checked); + }); + } } diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts index 9a052207d50..36fe4bc4dfb 100644 --- a/web_src/js/features/pull-view-file.ts +++ b/web_src/js/features/pull-view-file.ts @@ -34,7 +34,7 @@ export function countAndUpdateViewedFiles() { export function initViewedCheckboxListenerFor() { for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) { // To prevent double addition of listeners - form.setAttribute('data-has-viewed-checkbox-listener', true); + form.setAttribute('data-has-viewed-checkbox-listener', String(true)); // The checkbox consists of a div containing the real checkbox with its label and the CSRF token, // hence the actual checkbox first has to be found @@ -67,7 +67,7 @@ export function initViewedCheckboxListenerFor() { // Unfortunately, actual forms cause too many problems, hence another approach is needed const files = {}; files[fileName] = this.checked; - const data = {files}; + const data: Record = {files}; const headCommitSHA = form.getAttribute('data-headcommit'); if (headCommitSHA) data.headCommitSHA = headCommitSHA; POST(form.getAttribute('data-link'), {data}); diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index 96b08250fb1..32d0b84f4c9 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -35,7 +35,7 @@ function initEditPreviewTab(elForm: HTMLFormElement) { } export function initRepoEditor() { - const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone'); + const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone'); if (dropzoneUpload) initDropzone(dropzoneUpload); const editArea = document.querySelector('.page-content.repository.editor textarea#edit_area'); diff --git a/web_src/js/features/repo-search.ts b/web_src/js/features/repo-search.ts index 9cc2dd42239..7f111dce33f 100644 --- a/web_src/js/features/repo-search.ts +++ b/web_src/js/features/repo-search.ts @@ -5,9 +5,10 @@ export function initRepositorySearch() { repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => { e.preventDefault(); - const formData = new FormData(repositorySearchForm); - const params = new URLSearchParams(formData); - + const params = new URLSearchParams(); + for (const [key, value] of new FormData(repositorySearchForm).entries()) { + params.set(key, value.toString()); + } if (e.target.name === 'clear-filter') { params.delete('archived'); params.delete('fork'); diff --git a/web_src/js/features/repo-settings-branches.test.ts b/web_src/js/features/repo-settings-branches.test.ts index c4609999bef..32ab54e4c2c 100644 --- a/web_src/js/features/repo-settings-branches.test.ts +++ b/web_src/js/features/repo-settings-branches.test.ts @@ -2,6 +2,7 @@ import {beforeEach, describe, expect, test, vi} from 'vitest'; import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; import {POST} from '../modules/fetch.ts'; import {createSortable} from '../modules/sortable.ts'; +import type {SortableEvent} from 'sortablejs'; vi.mock('../modules/fetch.ts', () => ({ POST: vi.fn(), @@ -54,8 +55,8 @@ describe('Repository Branch Settings', () => { vi.mocked(POST).mockResolvedValue({ok: true} as Response); // Mock createSortable to capture and execute the onEnd callback - vi.mocked(createSortable).mockImplementation((_el, options) => { - options.onEnd(); + vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => { + options.onEnd(new Event('SortableEvent') as SortableEvent); return {destroy: vi.fn()}; }); diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index 44588c00646..fa65bcbb280 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -51,6 +51,7 @@ function makeCollections({mentions, emoji}) { export async function attachTribute(element, {mentions, emoji}) { const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); const collections = makeCollections({mentions, emoji}); + // @ts-expect-error TS2351: This expression is not constructable (strange, why) const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); tribute.attach(element); return tribute; diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 9780a1cf3cf..a5ec29a83f6 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -8,6 +8,17 @@ declare module '*.css' { export default value; } +declare module '*.vue' { + import type {DefineComponent} from 'vue'; + const component: DefineComponent; + export default component; + // List of named exports from vue components, used to make `tsc` output clean. + // To actually lint .vue files, `vue-tsc` is used because `tsc` can not parse them. + export function initRepoBranchTagSelector(selector: string): void; + export function initDashboardRepoList(): void; + export function initRepositoryActionView(): void; +} + declare let __webpack_public_path__: string; declare module 'htmx.org/dist/htmx.esm.js' { @@ -16,8 +27,8 @@ declare module 'htmx.org/dist/htmx.esm.js' { } declare module 'uint8-to-base64' { - export function encode(arrayBuffer: ArrayBuffer): string; - export function decode(base64str: string): ArrayBuffer; + export function encode(arrayBuffer: Uint8Array): string; + export function decode(base64str: string): Uint8Array; } declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index ce0b3cbc398..4e7f1ac0933 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -16,7 +16,6 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance { // because we should use our own wrapper functions to handle them, do not let the user override them const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts; - // @ts-expect-error: wrong type derived by typescript const instance: Instance = tippy(target, { appendTo: document.body, animation: false, diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index bd872f094ca..997a4d1ff3f 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -134,16 +134,16 @@ export function toAbsoluteUrl(url: string): string { return `${window.location.origin}${url}`; } -// Encode an ArrayBuffer into a URLEncoded base64 string. -export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string { - return encode(arrayBuffer) +// Encode an Uint8Array into a URLEncoded base64 string. +export function encodeURLEncodedBase64(uint8Array: Uint8Array): string { + return encode(uint8Array) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } -// Decode a URLEncoded base64 to an ArrayBuffer. -export function decodeURLEncodedBase64(base64url: string): ArrayBuffer { +// Decode a URLEncoded base64 to an Uint8Array. +export function decodeURLEncodedBase64(base64url: string): Uint8Array { return decode(base64url .replace(/_/g, '/') .replace(/-/g, '+')); From 42090844ed2de5e615abc6ece351c152d3344295 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 15 Dec 2024 13:38:39 -0800 Subject: [PATCH 22/49] Fix bug on action list deleted branch (#32848) Fix https://github.com/go-gitea/gitea/issues/32761#issuecomment-2540946064 --------- Co-authored-by: wxiaoguang --- models/fixtures/action_run.yml | 19 +++++++++++++++++++ models/fixtures/branch.yml | 12 ++++++++++++ routers/web/repo/actions/actions.go | 9 +++++---- routers/web/repo/actions/actions_test.go | 22 ++++++++++++++++++++++ routers/web/repo/actions/main_test.go | 14 ++++++++++++++ 5 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 routers/web/repo/actions/main_test.go diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 0747c46d2f0..1db849352f2 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -55,3 +55,22 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 794 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 190 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index c7bdff77339..17b1869ab63 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -81,3 +81,15 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 15 + repo_id: 4 + name: 'master' + commit_id: 'c7cd3cd144e6d23c9d6f3d07e52b2c1a956e0338' + commit_message: 'add Readme' + commit_time: 1588147171 + pusher_id: 13 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 7ed37ea26b2..1de18359365 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -5,6 +5,7 @@ package actions import ( "bytes" + stdCtx "context" "fmt" "net/http" "slices" @@ -245,7 +246,7 @@ func List(ctx *context.Context) { return } - if err := loadIsRefDeleted(ctx, runs); err != nil { + if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, runs); err != nil { log.Error("LoadIsRefDeleted", err) } @@ -273,7 +274,7 @@ func List(ctx *context.Context) { // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. // TODO: move this function to models/actions/run_list.go but now it will result in a circular import. -func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error { +func loadIsRefDeleted(ctx stdCtx.Context, repoID int64, runs actions_model.RunList) error { branches := make(container.Set[string], len(runs)) for _, run := range runs { refName := git.RefName(run.Ref) @@ -285,14 +286,14 @@ func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error { return nil } - branchInfos, err := git_model.GetBranches(ctx, ctx.Repo.Repository.ID, branches.Values(), false) + branchInfos, err := git_model.GetBranches(ctx, repoID, branches.Values(), false) if err != nil { return err } branchSet := git_model.BranchesToNamesSet(branchInfos) for _, run := range runs { refName := git.RefName(run.Ref) - if refName.IsBranch() && !branchSet.Contains(run.Ref) { + if refName.IsBranch() && !branchSet.Contains(refName.ShortName()) { run.IsRefDeleted = true } } diff --git a/routers/web/repo/actions/actions_test.go b/routers/web/repo/actions/actions_test.go index 194704d14eb..6a976ed65c0 100644 --- a/routers/web/repo/actions/actions_test.go +++ b/routers/web/repo/actions/actions_test.go @@ -7,6 +7,10 @@ import ( "strings" "testing" + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + unittest "code.gitea.io/gitea/models/unittest" + act_model "github.com/nektos/act/pkg/model" "github.com/stretchr/testify/assert" ) @@ -154,3 +158,21 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) { Type: "boolean", }, workflowDispatch.Inputs[2]) } + +func Test_loadIsRefDeleted(t *testing.T) { + unittest.PrepareTestEnv(t) + + runs, total, err := db.FindAndCount[actions_model.ActionRun](db.DefaultContext, + actions_model.FindRunOptions{RepoID: 4, Ref: "refs/heads/test"}) + assert.NoError(t, err) + assert.Len(t, runs, 1) + assert.EqualValues(t, 1, total) + for _, run := range runs { + assert.False(t, run.IsRefDeleted) + } + + assert.NoError(t, loadIsRefDeleted(db.DefaultContext, 4, runs)) + for _, run := range runs { + assert.True(t, run.IsRefDeleted) + } +} diff --git a/routers/web/repo/actions/main_test.go b/routers/web/repo/actions/main_test.go new file mode 100644 index 00000000000..a82f9c6672e --- /dev/null +++ b/routers/web/repo/actions/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} From 5e22e511de5ad16d49a3ad4d04bb6ee60cd14e65 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 16 Dec 2024 00:35:55 +0000 Subject: [PATCH 23/49] [skip ci] Updated licenses and gitignores --- options/license/MIPS | 4 ++++ options/license/ThirdEye | 7 +++++++ 2 files changed, 11 insertions(+) create mode 100644 options/license/MIPS create mode 100644 options/license/ThirdEye diff --git a/options/license/MIPS b/options/license/MIPS new file mode 100644 index 00000000000..cf57a056391 --- /dev/null +++ b/options/license/MIPS @@ -0,0 +1,4 @@ +Copyright (c) 1992, 1991, 1990 MIPS Computer Systems, Inc. +MIPS Computer Systems, Inc. grants reproduction and use +rights to all parties, PROVIDED that this comment is +maintained in the copy. diff --git a/options/license/ThirdEye b/options/license/ThirdEye new file mode 100644 index 00000000000..ce75b566e37 --- /dev/null +++ b/options/license/ThirdEye @@ -0,0 +1,7 @@ +(C) Copyright 1984 by Third Eye Software, Inc. + +Third Eye Software, Inc. grants reproduction and use rights to +all parties, PROVIDED that this comment is maintained in the copy. + +Third Eye makes no claims about the applicability of this +symbol table to a particular use. From 300b724abfa96d32d63dee97eca3782912cbc841 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Mon, 16 Dec 2024 09:38:18 +0900 Subject: [PATCH 24/49] Leave MAINTAINERS and the organization (#32820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why join I didn’t talk about myself before, so some people may think that I am an employee from the company. So I think it is necessary to talk about why and how I joined. At the begining, my boss gave me a task to find a git software which can self hosted in on-premise. Then I found that there are not many project which meet our needs. But finally, I found Gitea. A easy use, easy maintenance, and without a good machine you can also run it. At that time, I just finished my previous work which is using helm to deploy something in K8s. So I tried to use Gitea’s helm chart to deploy in my work PC to see whether we can use it. But soon, I found a bug, and reported it (https://gitea.com/gitea/helm-chart/issues/382), but after about 1 month, there’s no fix. So I try to check the source code, and I found that it is caused by Gitea’s code and it is easy to fix it. So I created an issue (https://github.com/go-gitea/gitea/issues/22523) in Gitea. But unfortunately, after a long time again, it is still not fixed. So I tried to finish it by myself. I’m not a pro programmer, coding is just my hobby since I was 13 or 14 years old. (I will tell the reason later), I even don’t know the workflow about the contribution of OSS, so maybe I did some bad things at the early time, I apologize. But the people here are very kind, at that time, I start to consider whether it has worth to recommend to my boss. So I started to use it, but I found more and more bugs in a short time. Japanese company is very sensitive to it, so I gave up to recommend. But I can try to fix them! Because I can learn too many things during the contribution, not just about the programing but also the usage of other tools and the general contribution rule in the world of OSS. It let me grow up, and to become (maybe) a perfect full-stack engineer which is my dream. (Why it is my dream? I made a wrong decision in my college, I took/followed the advice of my parent, choosed communications engineering instead of computer science which is my favorite thing) # Why leave Several days ago, there’s an [article](https://juejin.cn/post/7446578471901626420#comment) came into my eye. Something about JiHu (GitLab Ltd in China) start to file a lawsuit to the company which is using GitLab CE version which is under MIT License. So people start to find other git service/application to avoid it. And in the this article, a project called Fogejo is mentioned. It says it is a hard-fork of Gitea. But I don’t know the meaning of `hard-fork`, so I access the home page of this project to find where it comes from. Finally, I found it here: https://forgejo.org/compare-to-gitea/#why-was-forgejo-created. They said: > As of early 2024, Forgejo is developed independently of Gitea, as a “hard-fork”. `hard-fork` has a quotation, so the meaning is not the original meaning of it, but they said `as`, which means `like` or `similar` I think. So just focus on the words before `as` is ok, because `hard-fork` is a simile, `As of early 2024, Forgejo is developed independently of Gitea` is what they want to say. In my mind, this means: since early 2024 Forgejo’s codes (new changes) are all written by themselves, and emphasize that these changes are not related to Gitea, because they can simply say `As of early 2024, Forgejo is developed independently, as a “hard-fork”` But after I check the commit history, I can still find some strange commits in recent month: https://codeberg.org/forgejo/forgejo/commits/branch/forgejo/search?q=author%3Ayp05327&all= The author is me, but the commit is signed by someone I even never heard. Considering the words they said above, it feels/sounds like my work has become their work. Although Gitea is under MIT license, is this allowed in the OSS world? Even it is allowed, I can not accept it personally. So I created a issue to ask them: https://codeberg.org/forgejo/forgejo/issues/6236 https://codeberg.org/forgejo/discussions/issues/251 Finally, it seems that they understood the problem and promised to improve it. But I also required a public statement to explain it which means they need to apologize, otherwise it is hard to the users who believe these are all their work know it, and it seems they ignored some of my words again? So it is hard for me to believe they will really make changes and post the apologize. If they did, I will consider to come back. Otherwise, I think there’s no worth to continually contribute to any OSS project, so I decided to leave. ps: TOC voting is still ongoing, please remove me from the list. And I will leave the organization after the merge. At the end, thanks to all people who have helped me to finish the contribution and teach me new knowledges. --- MAINTAINERS | 1 - 1 file changed, 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index 426181cbcf1..ad02ecc7557 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -46,7 +46,6 @@ Wim (@42wim) Jason Song (@wolfogre) Yarden Shoham (@yardenshoham) Yu Tian (@Zettat123) -Eddie Yang <576951401@qq.com> (@yp05327) Dong Ge (@sillyguodong) Xinyi Gong (@HesterG) wxiaoguang (@wxiaoguang) From 276f43330cb86e2ce6bc5a902a43f02727e009e9 Mon Sep 17 00:00:00 2001 From: hiifong Date: Mon, 16 Dec 2024 10:22:49 +0800 Subject: [PATCH 25/49] Do not display `attestation-manifest` and use short sha256 instead of full sha256 (#32851) Related: #24973 Before: ![image](https://github.com/user-attachments/assets/bca17900-5075-4d15-af7a-c71bf8979c04) After: ![image](https://github.com/user-attachments/assets/c5a24e3b-763b-4463-80db-d4dbd89f7dc4) Index: ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:5967afffdfde104ca1459286a72346baaef8b70ac153325d7a6cd85c7734ac6e", "size": 672, "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:f9abfcc55320f9ff1f38eeb7dbb4bea10b29c7febfa49ccd7aab9fa02403b9f0", "size": 672, "platform": { "architecture": "arm64", "os": "linux" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:d70ad19d00c19e40691045cbddc3e8a5a4454c31cc454d1132b13bcaf35b6d46", "size": 566, "annotations": { "vnd.docker.reference.digest": "sha256:5967afffdfde104ca1459286a72346baaef8b70ac153325d7a6cd85c7734ac6e", "vnd.docker.reference.type": "attestation-manifest" }, "platform": { "architecture": "unknown", "os": "unknown" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:73bc233bf4eac96a404ce3e0430b698831a4ea7050c878d5f76d1d1f133751dd", "size": 566, "annotations": { "vnd.docker.reference.digest": "sha256:f9abfcc55320f9ff1f38eeb7dbb4bea10b29c7febfa49ccd7aab9fa02403b9f0", "vnd.docker.reference.type": "attestation-manifest" }, "platform": { "architecture": "unknown", "os": "unknown" } } ] } ``` --------- Co-authored-by: silverwind --- modules/templates/util_string.go | 4 ++++ templates/package/content/container.tmpl | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 2ae27d08336..382e2de13f1 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -60,3 +60,7 @@ func (su *StringUtils) EllipsisString(s string, maxLength int) string { func (su *StringUtils) ToUpper(s string) string { return strings.ToUpper(s) } + +func (su *StringUtils) TrimPrefix(s, prefix string) string { + return strings.TrimPrefix(s, prefix) +} diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index aaed25bfbd6..207774bfefd 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -36,11 +36,13 @@ {{range .PackageDescriptor.Metadata.Manifests}} - - {{.Digest}} - {{.Platform}} - {{FileSize .Size}} - + {{if ne .Platform "unknown/unknown"}} + + {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + {{.Platform}} + {{FileSize .Size}} + + {{end}} {{end}} From d28a4843b8de5d5e01ef3d7b2ad25f22853247ad Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 16 Dec 2024 11:18:00 +0800 Subject: [PATCH 26/49] Fix incomplete Actions status aggregations (#32859) fix #32857 --- models/actions/run_job.go | 43 ++++++++------- models/actions/run_job_status_test.go | 64 +++++++++++++++++++++++ templates/repo/actions/status.tmpl | 18 +++---- web_src/js/components/ActionRunStatus.vue | 13 ++--- web_src/js/components/RepoActionView.vue | 4 +- web_src/js/svg.ts | 2 + 6 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 models/actions/run_job_status_test.go diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 2319af8e085..8c131351d5d 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -153,28 +153,33 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col } func AggregateJobStatus(jobs []*ActionRunJob) Status { - allDone := true - allWaiting := true - hasFailure := false + allSuccessOrSkipped := true + var hasFailure, hasCancelled, hasSkipped, hasWaiting, hasRunning, hasBlocked bool for _, job := range jobs { - if !job.Status.IsDone() { - allDone = false - } - if job.Status != StatusWaiting && !job.Status.IsDone() { - allWaiting = false - } - if job.Status == StatusFailure || job.Status == StatusCancelled { - hasFailure = true - } + allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped) + hasFailure = hasFailure || job.Status == StatusFailure + hasCancelled = hasCancelled || job.Status == StatusCancelled + hasSkipped = hasSkipped || job.Status == StatusSkipped + hasWaiting = hasWaiting || job.Status == StatusWaiting + hasRunning = hasRunning || job.Status == StatusRunning + hasBlocked = hasBlocked || job.Status == StatusBlocked } - if allDone { - if hasFailure { - return StatusFailure - } + switch { + case allSuccessOrSkipped: return StatusSuccess - } - if allWaiting { + case hasFailure: + return StatusFailure + case hasRunning: + return StatusRunning + case hasWaiting: return StatusWaiting + case hasBlocked: + return StatusBlocked + case hasCancelled: + return StatusCancelled + case hasSkipped: + return StatusSkipped + default: + return StatusUnknown // it shouldn't happen } - return StatusRunning } diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go new file mode 100644 index 00000000000..bac480a96b2 --- /dev/null +++ b/models/actions/run_job_status_test.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAggregateJobStatus(t *testing.T) { + testStatuses := func(expected Status, statuses []Status) { + var jobs []*ActionRunJob + for _, v := range statuses { + jobs = append(jobs, &ActionRunJob{Status: v}) + } + actual := AggregateJobStatus(jobs) + if !assert.Equal(t, expected, actual) { + var statusStrings []string + for _, s := range statuses { + statusStrings = append(statusStrings, s.String()) + } + t.Errorf("AggregateJobStatus(%v) = %v, want %v", statusStrings, statusNames[actual], statusNames[expected]) + } + } + + cases := []struct { + statuses []Status + expected Status + }{ + // success with other status + {[]Status{StatusSuccess}, StatusSuccess}, + {[]Status{StatusSuccess, StatusSkipped}, StatusSuccess}, // skipped doesn't affect success + {[]Status{StatusSuccess, StatusFailure}, StatusFailure}, + {[]Status{StatusSuccess, StatusCancelled}, StatusCancelled}, + {[]Status{StatusSuccess, StatusWaiting}, StatusWaiting}, + {[]Status{StatusSuccess, StatusRunning}, StatusRunning}, + {[]Status{StatusSuccess, StatusBlocked}, StatusBlocked}, + + // failure with other status, fail fast + // Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast. + {[]Status{StatusFailure}, StatusFailure}, + {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, + {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, + {[]Status{StatusFailure, StatusCancelled}, StatusFailure}, + {[]Status{StatusFailure, StatusWaiting}, StatusFailure}, + {[]Status{StatusFailure, StatusRunning}, StatusFailure}, + {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, + + // skipped with other status + {[]Status{StatusSkipped}, StatusSuccess}, + {[]Status{StatusSkipped, StatusSuccess}, StatusSuccess}, + {[]Status{StatusSkipped, StatusFailure}, StatusFailure}, + {[]Status{StatusSkipped, StatusCancelled}, StatusCancelled}, + {[]Status{StatusSkipped, StatusWaiting}, StatusWaiting}, + {[]Status{StatusSkipped, StatusRunning}, StatusRunning}, + {[]Status{StatusSkipped, StatusBlocked}, StatusBlocked}, + } + + for _, c := range cases { + testStatuses(c.expected, c.statuses) + } +} diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl index a0e02cf8d76..64c25433028 100644 --- a/templates/repo/actions/status.tmpl +++ b/templates/repo/actions/status.tmpl @@ -2,28 +2,22 @@ Please also update the vue file above if this template is modified. action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown --> -{{- $size := 16 -}} -{{- if .size -}} -{{- $size = .size -}} -{{- end -}} - -{{- $className := "" -}} -{{- if .className -}} -{{- $className = .className -}} -{{- end -}} - - +{{- $size := Iif .size .size 16 -}} +{{- $className := Iif .className .className "" -}} + {{if eq .status "success"}} {{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}} {{else if eq .status "skipped"}} {{svg "octicon-skip" $size (printf "text grey %s" $className)}} +{{else if eq .status "cancelled"}} + {{svg "octicon-stop" $size (printf "text grey %s" $className)}} {{else if eq .status "waiting"}} {{svg "octicon-clock" $size (printf "text yellow %s" $className)}} {{else if eq .status "blocked"}} {{svg "octicon-blocked" $size (printf "text yellow %s" $className)}} {{else if eq .status "running"}} {{svg "octicon-meter" $size (printf "text yellow job-status-rotate %s" $className)}} -{{else if or (eq .status "failure") or (eq .status "cancelled") or (eq .status "unknown")}} +{{else}}{{/*failure, unknown*/}} {{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}} {{end}} diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue index 558b881dfe0..deab5f64690 100644 --- a/web_src/js/components/ActionRunStatus.vue +++ b/web_src/js/components/ActionRunStatus.vue @@ -6,24 +6,25 @@ import {SvgIcon} from '../svg.ts'; withDefaults(defineProps<{ - status: '', - size?: number, - className?: string, + status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'unknown', + size: number, + className: string, localeStatus?: string, }>(), { size: 16, - className: undefined, + className: '', localeStatus: undefined, }); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index eece2efaf86..cb65a98eddc 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -551,11 +551,13 @@ export function initRepositoryActionView() { .action-info-summary-title { display: flex; + align-items: center; + gap: 0.5em; } .action-info-summary-title-text { font-size: 20px; - margin: 0 0 0 8px; + margin: 0; flex: 1; overflow-wrap: anywhere; } diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 3a0f2ed53c7..90b12fa87d7 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -65,6 +65,7 @@ import octiconSidebarCollapse from '../../public/assets/img/svg/octicon-sidebar- import octiconSidebarExpand from '../../public/assets/img/svg/octicon-sidebar-expand.svg'; import octiconSkip from '../../public/assets/img/svg/octicon-skip.svg'; import octiconStar from '../../public/assets/img/svg/octicon-star.svg'; +import octiconStop from '../../public/assets/img/svg/octicon-stop.svg'; import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethrough.svg'; import octiconSync from '../../public/assets/img/svg/octicon-sync.svg'; import octiconTable from '../../public/assets/img/svg/octicon-table.svg'; @@ -140,6 +141,7 @@ const svgs = { 'octicon-sidebar-expand': octiconSidebarExpand, 'octicon-skip': octiconSkip, 'octicon-star': octiconStar, + 'octicon-stop': octiconStop, 'octicon-strikethrough': octiconStrikethrough, 'octicon-sync': octiconSync, 'octicon-table': octiconTable, From 22c4599542ee3e10bcab4c9136467bbac8e90ba0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 16 Dec 2024 21:49:53 +0800 Subject: [PATCH 27/49] Improve Actions status aggregations (#32860) Make the result the same as GitHub: * all skipped, then result is skipped * any cancelled, then result cancelled --- models/actions/run_job.go | 15 ++++++++------- models/actions/run_job_status_test.go | 25 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 8c131351d5d..de4b6aab667 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -153,20 +153,25 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col } func AggregateJobStatus(jobs []*ActionRunJob) Status { - allSuccessOrSkipped := true - var hasFailure, hasCancelled, hasSkipped, hasWaiting, hasRunning, hasBlocked bool + allSuccessOrSkipped := len(jobs) != 0 + allSkipped := len(jobs) != 0 + var hasFailure, hasCancelled, hasWaiting, hasRunning, hasBlocked bool for _, job := range jobs { allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped) + allSkipped = allSkipped && job.Status == StatusSkipped hasFailure = hasFailure || job.Status == StatusFailure hasCancelled = hasCancelled || job.Status == StatusCancelled - hasSkipped = hasSkipped || job.Status == StatusSkipped hasWaiting = hasWaiting || job.Status == StatusWaiting hasRunning = hasRunning || job.Status == StatusRunning hasBlocked = hasBlocked || job.Status == StatusBlocked } switch { + case allSkipped: + return StatusSkipped case allSuccessOrSkipped: return StatusSuccess + case hasCancelled: + return StatusCancelled case hasFailure: return StatusFailure case hasRunning: @@ -175,10 +180,6 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusWaiting case hasBlocked: return StatusBlocked - case hasCancelled: - return StatusCancelled - case hasSkipped: - return StatusSkipped default: return StatusUnknown // it shouldn't happen } diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go index bac480a96b2..04fd9ceba7b 100644 --- a/models/actions/run_job_status_test.go +++ b/models/actions/run_job_status_test.go @@ -11,6 +11,7 @@ import ( func TestAggregateJobStatus(t *testing.T) { testStatuses := func(expected Status, statuses []Status) { + t.Helper() var jobs []*ActionRunJob for _, v := range statuses { jobs = append(jobs, &ActionRunJob{Status: v}) @@ -29,6 +30,16 @@ func TestAggregateJobStatus(t *testing.T) { statuses []Status expected Status }{ + // unknown cases, maybe it shouldn't happen in real world + {[]Status{}, StatusUnknown}, + {[]Status{StatusUnknown, StatusSuccess}, StatusUnknown}, + {[]Status{StatusUnknown, StatusSkipped}, StatusUnknown}, + {[]Status{StatusUnknown, StatusFailure}, StatusFailure}, + {[]Status{StatusUnknown, StatusCancelled}, StatusCancelled}, + {[]Status{StatusUnknown, StatusWaiting}, StatusWaiting}, + {[]Status{StatusUnknown, StatusRunning}, StatusRunning}, + {[]Status{StatusUnknown, StatusBlocked}, StatusBlocked}, + // success with other status {[]Status{StatusSuccess}, StatusSuccess}, {[]Status{StatusSuccess, StatusSkipped}, StatusSuccess}, // skipped doesn't affect success @@ -38,18 +49,28 @@ func TestAggregateJobStatus(t *testing.T) { {[]Status{StatusSuccess, StatusRunning}, StatusRunning}, {[]Status{StatusSuccess, StatusBlocked}, StatusBlocked}, + // any cancelled, then cancelled + {[]Status{StatusCancelled}, StatusCancelled}, + {[]Status{StatusCancelled, StatusSuccess}, StatusCancelled}, + {[]Status{StatusCancelled, StatusSkipped}, StatusCancelled}, + {[]Status{StatusCancelled, StatusFailure}, StatusCancelled}, + {[]Status{StatusCancelled, StatusWaiting}, StatusCancelled}, + {[]Status{StatusCancelled, StatusRunning}, StatusCancelled}, + {[]Status{StatusCancelled, StatusBlocked}, StatusCancelled}, + // failure with other status, fail fast // Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast. {[]Status{StatusFailure}, StatusFailure}, {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, - {[]Status{StatusFailure, StatusCancelled}, StatusFailure}, + {[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, {[]Status{StatusFailure, StatusWaiting}, StatusFailure}, {[]Status{StatusFailure, StatusRunning}, StatusFailure}, {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, // skipped with other status - {[]Status{StatusSkipped}, StatusSuccess}, + // TODO: need to clarify whether a PR with "skipped" job status is considered as "mergeable" or not. + {[]Status{StatusSkipped}, StatusSkipped}, {[]Status{StatusSkipped, StatusSuccess}, StatusSuccess}, {[]Status{StatusSkipped, StatusFailure}, StatusFailure}, {[]Status{StatusSkipped, StatusCancelled}, StatusCancelled}, From 9f9bff0f04e7104fb9db5d2a039aa6d1a2fae08e Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 17 Dec 2024 00:45:49 +0000 Subject: [PATCH 28/49] [skip ci] Updated translations via Crowdin --- options/locale/locale_fr-FR.ini | 1 - options/locale/locale_ga-IE.ini | 1 - options/locale/locale_pt-PT.ini | 2 +- options/locale/locale_zh-CN.ini | 169 ++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 4d4940cff5a..0870983c986 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1676,7 +1676,6 @@ issues.timetracker_timer_stop=Arrêter le minuteur issues.timetracker_timer_discard=Annuler le minuteur issues.timetracker_timer_manually_add=Pointer du temps -issues.time_estimate_placeholder=1h 2m issues.time_estimate_set=Définir le temps estimé issues.time_estimate_display=Estimation : %s issues.change_time_estimate_at=a changé le temps estimé à %s %s diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index f40e0037d2c..0b74776c174 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -1676,7 +1676,6 @@ issues.timetracker_timer_stop=Stop an t-amadóir issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh issues.timetracker_timer_manually_add=Cuir Am leis -issues.time_estimate_placeholder=1u 2n issues.time_estimate_set=Socraigh am measta issues.time_estimate_display=Meastachán: %s issues.change_time_estimate_at=d'athraigh an meastachán ama go %s %s diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 776d2bdc2bf..b959e7fdba2 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1679,7 +1679,6 @@ issues.timetracker_timer_stop=Parar cronómetro issues.timetracker_timer_discard=Descartar cronómetro issues.timetracker_timer_manually_add=Adicionar tempo -issues.time_estimate_placeholder=1h 2m issues.time_estimate_set=Definir tempo estimado issues.time_estimate_display=Estimativa: %s issues.change_time_estimate_at=alterou a estimativa de tempo para %s %s @@ -2632,6 +2631,7 @@ release.new_release=Novo lançamento release.draft=Rascunho release.prerelease=Pré-lançamento release.stable=Estável +release.latest=Mais recente release.compare=Comparar release.edit=editar release.ahead.commits=%d cometimentos diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3add7f8be38..8c6392a4dc9 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -93,6 +93,7 @@ remove_all=移除所有 remove_label_str=`删除标签 "%s"` edit=编辑 view=查看 +test=测试 enabled=启用 disabled=禁用 @@ -103,6 +104,7 @@ copy_url=复制网址 copy_hash=复制哈希值 copy_content=复制内容 copy_branch=复制分支名 +copy_path=复制路径 copy_success=复制成功! copy_error=复制失败 copy_type_unsupported=无法复制此类型的文件内容 @@ -143,6 +145,7 @@ confirm_delete_selected=确认删除所有选中项目? name=名称 value=值 +readme=自述文档 filter=过滤 filter.clear=清除筛选器 @@ -178,6 +181,7 @@ package_kind=搜索软件包... project_kind=搜索项目... branch_kind=搜索分支... tag_kind=搜索标签... +tag_tooltip=搜索匹配的标签。使用“%”来匹配任何序列的数字 commit_kind=搜索提交记录... runner_kind=搜索runners... no_results=未找到匹配结果 @@ -223,16 +227,20 @@ string.desc=Z - A [error] occurred=发生了一个错误 +report_message=如果您确定这是一个 Gitea bug,请在 这里 搜索问题,或在必要时创建一个新工单。 not_found=找不到目标。 network_error=网络错误 [startpage] app_desc=一款极易搭建的自助 Git 服务 install=易安装 +install_desc=通过 二进制 来运行;或者通过 Docker 来运行;或者通过 安装包 来运行。 platform=跨平台 +platform_desc=任何 Go 语言 支持的平台都可以运行 Gitea,包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行! lightweight=轻量级 lightweight_desc=一个廉价的树莓派的配置足以满足 Gitea 的最低系统硬件要求。最大程度上节省您的服务器资源! license=开源化 +license_desc=所有的代码都开源在 %[2]s 上,赶快加入我们来共同发展这个伟大的项目!还等什么?成为贡献者吧! [install] install=安装页面 @@ -346,6 +354,7 @@ enable_update_checker=启用更新检查 enable_update_checker_helper=通过连接到 gitea.io 定期检查新版本发布。 env_config_keys=环境配置 env_config_keys_prompt=以下环境变量也将应用于您的配置文件: +config_write_file_prompt=这些配置选项将写入以下位置: %s [home] nav_menu=导航菜单 @@ -386,6 +395,7 @@ relevant_repositories=只显示相关的仓库, 显示未过滤 [auth] create_new_account=注册帐号 +already_have_account=已有账号? sign_in_now=立即登录 disable_register_prompt=对不起,注册功能已被关闭。请联系网站管理员。 disable_register_mail=已禁用注册的电子邮件确认。 @@ -394,6 +404,7 @@ remember_me=记住此设备 remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 forgot_password_title=忘记密码 forgot_password=忘记密码? +need_account=需要一个帐户? sign_up_now=还没账号?马上注册。 sign_up_successful=帐户创建成功。欢迎! confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。 @@ -449,12 +460,15 @@ authorize_application=应用授权 authorize_redirect_notice=如果您授权此应用,您将会被重定向到 %s。 authorize_application_created_by=此应用由%s创建。 authorize_application_description=如果您允许,它将能够读取和修改您的所有帐户信息,包括私人仓库和组织。 +authorize_application_with_scopes=范围: %s authorize_title=授权 %s 访问您的帐户? authorization_failed=授权失败 authorization_failed_desc=因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。 sspi_auth_failed=SSPI 认证失败 +password_pwned=此密码出现在 被盗密码 列表上并且曾经被公开。 请使用另一个密码再试一次。 password_pwned_err=无法完成对 HaveIBeenPwned 的请求 last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。 +signin_passkey=使用密钥登录 back_to_sign_in=返回登录页面 [mail] @@ -574,6 +588,8 @@ lang_select_error=从列表中选出语言 username_been_taken=用户名已被使用。 username_change_not_local_user=非本地用户不允许更改用户名。 +change_username_disabled=更改用户名已被禁用。 +change_full_name_disabled=更改用户全名已禁用 username_has_not_been_changed=用户名未更改 repo_name_been_taken=仓库名称已被使用。 repository_force_private=“强制私有”已启用:私有仓库不能被公开。 @@ -623,6 +639,7 @@ org_still_own_repo=该组织仍然是某些仓库的拥有者,请先删除或 org_still_own_packages=该组织仍然是一个或多个软件包的拥有者,请先删除它们。 target_branch_not_exist=目标分支不存在。 +target_ref_not_exist=目标引用 %s 不存在 admin_cannot_delete_self=当您是管理员时,您不能删除自己。请先移除您的管理员权限 @@ -688,14 +705,18 @@ applications=应用 orgs=管理组织 repos=仓库列表 delete=删除帐户 +twofa=两步验证(TOTP) account_link=已绑定帐户 organization=组织 uid=UID +webauthn=两步验证(安全密钥) public_profile=公开信息 biography_placeholder=告诉我们一点您自己! (您可以使用Markdown) location_placeholder=与他人分享你的大概位置 profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作 +password_username_disabled=不允许非本地用户更改他们的用户名。更多详情请联系您的系统管理员。 +password_full_name_disabled=您无权更改他们的全名。请联系您的站点管理员了解更多详情。 full_name=自定义名称 website=个人网站 location=所在地区 @@ -745,6 +766,7 @@ uploaded_avatar_not_a_image=上传的文件不是一张图片。 uploaded_avatar_is_too_big=上传的文件大小(%d KiB) 超过最大限制(%d KiB)。 update_avatar_success=您的头像已更新。 update_user_avatar_success=用户头像已更新。 +cropper_prompt=您可以在保存前编辑图像。编辑的图像将被保存为 PNG 格式。 change_password=更新密码 old_password=当前密码 @@ -787,6 +809,7 @@ add_email_success=新的电子邮件地址已添加。 email_preference_set_success=电子邮件首选项已成功设置。 add_openid_success=新的 OpenID 地址已添加。 keep_email_private=隐藏电子邮件地址 +keep_email_private_popup=这将会隐藏您的电子邮件地址,不仅在您的个人资料中,还在您使用Web界面创建合并请求或编辑文件时。已推送的提交将不会被修改。在提交中使用 %s 以和您的账号关联。 openid_desc=OpenID 让你可以将认证转发到外部服务。 manage_ssh_keys=管理 SSH 密钥 @@ -905,6 +928,7 @@ create_oauth2_application_success=您已成功创建了一个新的 OAuth2 应 update_oauth2_application_success=您已成功更新了此 OAuth2 应用。 oauth2_application_name=应用名称 oauth2_confidential_client=机密客户端。是否是能够维持凭据机密性的应用,比如网页应用程序。如果是本地应用程序请不要勾选,包括桌面和移动端应用。 +oauth2_skip_secondary_authorization=首次授权后允许公共客户端跳过授权步骤。 可能会带来安全风险。 oauth2_redirect_uris=重定向 URI。每行一个 URI。 save_application=保存 oauth2_client_id=客户端ID @@ -915,6 +939,7 @@ oauth2_client_secret_hint=您离开或刷新此页面后将不会再显示此密 oauth2_application_edit=编辑 oauth2_application_create_description=OAuth2 应用允许您的第三方应用程序访问此实例的用户帐户。 oauth2_application_remove_description=移除一个OAuth2应用将会阻止它访问此实例上的已授权用户账户。是否继续? +oauth2_application_locked=如果配置启用,Gitea预注册一些OAuth2应用程序。 为了防止意外的行为, 这些应用既不能编辑也不能删除。请参阅OAuth2文档以获取更多信息。 authorized_oauth2_applications=已授权的 OAuth2 应用 authorized_oauth2_applications_description=您已授予这些第三方应用程序访问您的个人 Gitea 账户的权限。请撤销那些您不再需要的应用程序的访问权限。 @@ -923,20 +948,26 @@ revoke_oauth2_grant=撤回权限 revoke_oauth2_grant_description=确定撤销此三方应用程序的授权,并阻止此应用程序访问您的数据? revoke_oauth2_grant_success=成功撤销了访问权限。 +twofa_desc=为保护你的账号密码安全,你可以使用智能手机或其它设备来接收时间强相关的一次性密码(TOTP)。 twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性恢复密钥来重新获得对您账户的访问。 twofa_is_enrolled=你的账号已启用了两步验证。 twofa_not_enrolled=你的账号未开启两步验证。 twofa_disable=禁用两步认证 +twofa_scratch_token_regenerate=重新生成初始令牌 +twofa_scratch_token_regenerated=您的初始令牌现在是 %s。将其存放在安全的地方,它将不会再次显示。 twofa_enroll=启用两步验证 twofa_disable_note=如果需要, 可以禁用双因素身份验证。 twofa_disable_desc=关掉两步验证会使得您的账号不安全,继续执行? +regenerate_scratch_token_desc=如果您丢失了您的恢复密钥或已经使用它登录, 您可以在这里重置它。 twofa_disabled=两步验证已被禁用。 scan_this_image=使用您的授权应用扫描这张图片: or_enter_secret=或者输入密钥:%s then_enter_passcode=并输入应用程序中显示的密码: passcode_invalid=密码不正确。再试一次。 +twofa_enrolled=你的账号已经启用了两步验证。请保存初始令牌(%s)到一个安全的地方,此令牌仅显示一次。 twofa_failed_get_secret=获取 secret 失败。 +webauthn_desc=安全密钥是包含加密密钥的硬件设备。它们可以用于双因素身份验证。安全密钥必须支持 WebAuthn 身份验证器 标准。 webauthn_register_key=添加安全密钥 webauthn_nickname=昵称 webauthn_delete_key=移除安全密钥 @@ -1002,6 +1033,8 @@ fork_to_different_account=派生到其他账号 fork_visibility_helper=无法更改派生仓库的可见性。 fork_branch=要克隆到 Fork 的分支 all_branches=所有分支 +view_all_branches=查看所有分支 +view_all_tags=查看所有标签 fork_no_valid_owners=这个代码仓库无法被派生,因为没有有效的所有者。 fork.blocked_user=无法克隆仓库,因为您被仓库所有者屏蔽。 use_template=使用此模板 @@ -1013,6 +1046,8 @@ generate_repo=生成仓库 generate_from=生成自 repo_desc=仓库描述 repo_desc_helper=输入简要描述 (可选) +repo_no_desc=无详细信息 +repo_lang=编程语言 repo_gitignore_helper=选择 .gitignore 模板。 repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。 issue_labels=工单标签 @@ -1074,6 +1109,7 @@ delete_preexisting_success=删除 %s 中未收录的文件 blame_prior=查看此更改前的 blame blame.ignore_revs=忽略 .git-blame-ignore-revs 的修订。点击 绕过 并查看正常的 Blame 视图。 blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 版本失败。 +user_search_tooltip=最多显示30名用户 tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在 tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。 @@ -1222,6 +1258,7 @@ releases=版本发布 tag=Git标签 released_this=发布 tagged_this=已标记 +file.title=%s 位于 %s file_raw=原始文件 file_history=文件历史 file_view_source=源码模式 @@ -1238,6 +1275,7 @@ ambiguous_runes_header=`此文件含有模棱两可的 Unicode 字符` ambiguous_runes_description=`此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。` invisible_runes_line=`此行含有不可见的 unicode 字符` ambiguous_runes_line=`此行有模棱两可的 unicode 字符` +ambiguous_character=`%[1]c [U+%04[1]X] 容易和 %[2]c [U+%04[2]X] 混淆` escape_control_characters=Escape unescape_control_characters=Unescape @@ -1429,6 +1467,7 @@ issues.new.clear_milestone=取消选中里程碑 issues.new.assignees=指派成员 issues.new.clear_assignees=取消指派成员 issues.new.no_assignees=未指派成员 +issues.new.no_reviewers=无审核者 issues.new.blocked_user=无法创建工单,因为您已被仓库所有者屏蔽。 issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 issues.edit.blocked_user=无法编辑内容,因为您已被仓库所有者或工单创建者屏蔽。 @@ -1457,6 +1496,7 @@ issues.remove_labels=于 %[2]s 删除了标签 %[1]s issues.add_remove_labels=于 %[3]s 添加了标签 %[1]s ,删除了标签 %[2]s issues.add_milestone_at=`于 %[2]s 添加了里程碑 %[1]s` issues.add_project_at=`于 %[2]s 将此添加到 %[1]s 项目` +issues.move_to_column_of_project=`将此对象移至 %s 的 %s 中在 %s 上` issues.change_milestone_at=`%[3]s 修改了里程碑从 %[1]s%[2]s` issues.change_project_at=于 %[3]s 将此从项目 %[1]s 移到 %[2]s issues.remove_milestone_at=`%[2]s 删除了里程碑 %[1]s` @@ -1488,6 +1528,8 @@ issues.filter_assignee=指派人筛选 issues.filter_assginee_no_select=所有指派成员 issues.filter_assginee_no_assignee=未指派 issues.filter_poster=作者 +issues.filter_user_placeholder=搜索用户 +issues.filter_user_no_select=所有用户 issues.filter_type=类型筛选 issues.filter_type.all_issues=所有工单 issues.filter_type.assigned_to_you=指派给您的 @@ -1541,7 +1583,9 @@ issues.no_content=没有提供说明。 issues.close=关闭工单 issues.comment_pull_merged_at=已合并提交 %[1]s 到 %[2]s %[3]s issues.comment_manually_pull_merged_at=手动合并提交 %[1]s 到 %[2]s %[3]s +issues.close_comment_issue=评论并关闭 issues.reopen_issue=重新开启 +issues.reopen_comment_issue=评论并重新开启 issues.create_comment=评论 issues.comment.blocked_user=无法创建或编辑评论,因为您已被仓库所有者或工单创建者屏蔽。 issues.closed_at=`于 %[2]s 关闭此工单` @@ -1630,12 +1674,25 @@ issues.delete.title=是否删除工单? issues.delete.text=您真的要删除这个工单吗?(该操作将会永久删除所有内容。如果您需要保留,请关闭它) issues.tracker=时间跟踪 +issues.timetracker_timer_start=启动计时器 +issues.timetracker_timer_stop=停止计时器 +issues.timetracker_timer_discard=删除计时器 +issues.timetracker_timer_manually_add=添加时间 +issues.time_estimate_set=设置预计时间 +issues.time_estimate_display=预计: %s +issues.change_time_estimate_at=将预计时间修改为 %s %s +issues.remove_time_estimate_at=删除预计时间 %s +issues.time_estimate_invalid=预计时间格式无效 +issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracking_already_started=`你已经开始对 另一个工单 进行时间跟踪!` +issues.stop_tracking_history=`停止工作 %s` issues.cancel_tracking_history=`取消时间跟踪 %s` issues.del_time=删除此时间跟踪日志 +issues.add_time_history=`添加计时 %s` issues.del_time_history=`已删除时间 %s` +issues.add_time_manually=手动添加时间 issues.add_time_hours=小时 issues.add_time_minutes=分钟 issues.add_time_sum_to_small=没有输入时间。 @@ -1695,6 +1752,7 @@ issues.dependency.add_error_dep_not_same_repo=这两个工单必须在同一仓 issues.review.self.approval=您不能批准您自己的合并请求。 issues.review.self.rejection=您不能请求对您自己的合并请求进行更改。 issues.review.approve=于 %s 批准此合并请求 +issues.review.comment=评审于 %s issues.review.dismissed=于 %[2]s 取消了 %[1]s 的评审 issues.review.dismissed_label=已取消 issues.review.left_comment=留下了一条评论 @@ -1720,6 +1778,10 @@ issues.review.resolve_conversation=已解决问题 issues.review.un_resolve_conversation=未解决问题 issues.review.resolved_by=标记问题为已解决 issues.review.commented=评论 +issues.review.official=已批准 +issues.review.requested=等待审核 +issues.review.rejected=请求变更 +issues.review.stale=批准后已更新 issues.review.unofficial=非官方审批数 issues.assignee.error=因为未知原因,并非所有的指派都成功。 issues.reference_issue.body=内容 @@ -1837,7 +1899,9 @@ pulls.unrelated_histories=合并失败:两个分支没有共同历史。提示 pulls.merge_out_of_date=合并失败:在生成合并时,主分支已更新。提示:再试一次。 pulls.head_out_of_date=合并失败:在生成合并时,head 已更新。提示:再试一次。 pulls.has_merged=失败:合并请求已经被合并,您不能再次合并或更改目标分支。 +pulls.push_rejected=推送失败:推送被拒绝。审查此仓库的 Git 钩子。 pulls.push_rejected_summary=详细拒绝信息 +pulls.push_rejected_no_message=推送失败:此推送被拒绝但未提供其他信息。请检查此仓库的 Git 钩子。 pulls.open_unmerged_pull_exists=`您不能执行重新打开操作, 因为已经存在相同的合并请求 (#%d)。` pulls.status_checking=一些检测仍在等待运行 pulls.status_checks_success=所有检测均成功 @@ -1881,6 +1945,10 @@ pulls.delete.title=删除此合并请求? pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s +pulls.upstream_diverging_prompt_behind_1=该分支落后于 %s %d 个提交 +pulls.upstream_diverging_prompt_behind_n=该分支落后于 %s %d 个提交 +pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改 +pulls.upstream_diverging_merge=同步派生 pull.deleted_branch=(已删除): %s pull.agit_documentation=查看有关 AGit 的文档 @@ -1894,6 +1962,7 @@ milestones.no_due_date=暂无截止日期 milestones.open=开启 milestones.close=关闭 milestones.new_subheader=里程碑可以帮助您组织工单并跟踪其进度。 +milestones.completeness=%d%% 已完成 milestones.create=创建里程碑 milestones.title=标题 milestones.desc=描述 @@ -2116,6 +2185,7 @@ settings.pulls.default_delete_branch_after_merge=默认合并后删除合并请 settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑 settings.releases_desc=启用发布 settings.packages_desc=启用仓库软件包注册中心 +settings.projects_desc=启用项目 settings.projects_mode_desc=项目模式 (要显示的项目类型) settings.projects_mode_repo=仅仓库项目 settings.projects_mode_owner=仅限用户或组织项目 @@ -2155,6 +2225,7 @@ settings.transfer_in_progress=当前正在进行转让。 如果你想将此代 settings.transfer_notices_1=- 如果将此仓库转移给其他用户, 您将失去对此仓库的访问权限。 settings.transfer_notices_2=-如果将其转移到您 (共同) 拥有的组织,您可以继续访问该仓库。 settings.transfer_notices_3=- 如果仓库是私有的并且被转移给某个用户,那么此操作可以确保该用户至少具有读权限(以及必要时的更改权限)。 +settings.transfer_notices_4=- 如果存储库属于某个组织,而您将其转移给另一个组织或个人,那么您将失去存储库工单与其组织项目系统之间的链接。 settings.transfer_owner=新拥有者 settings.transfer_perform=执行转让 settings.transfer_started=该代码库已被标记为转让并等待来自 %s 的确认 @@ -2291,6 +2362,7 @@ settings.event_pull_request_merge=合并请求合并 settings.event_package=软件包 settings.event_package_desc=软件包已在仓库中被创建或删除。 settings.branch_filter=分支过滤 +settings.branch_filter_desc=推送、创建,删除分支事件的分支白名单,使用 glob 模式匹配指定。若为空或 *,则将报告所有分支的事件。语法文档见 %[2]s。示例:master,{master,release*}。 settings.authorization_header=授权标头 settings.authorization_header_desc=当存在时将被作为授权标头包含在内。例如: %s。 settings.active=激活 @@ -2336,25 +2408,53 @@ settings.deploy_key_deletion=删除部署密钥 settings.deploy_key_deletion_desc=删除部署密钥将取消此密钥对此仓库的访问权限。继续? settings.deploy_key_deletion_success=部署密钥已删除。 settings.branches=分支 +settings.protected_branch=分支保护 settings.protected_branch.save_rule=保存规则 settings.protected_branch.delete_rule=删除规则 +settings.protected_branch_can_push=是否允许推送? settings.protected_branch_can_push_yes=你可以推 +settings.protected_branch_can_push_no=你不能推送 +settings.branch_protection=分支 '%s' 的保护规则 settings.protect_this_branch=启用分支保护 settings.protect_this_branch_desc=阻止删除并限制Git推送和合并到分支。 settings.protect_disable_push=禁用推送 settings.protect_disable_push_desc=此分支不允许推送。 +settings.protect_disable_force_push=禁用强制推送 +settings.protect_disable_force_push_desc=此分支禁止强制推送。 settings.protect_enable_push=启用推送 settings.protect_enable_push_desc=任何拥有写访问权限的人将被允许推送到此分支(但不能强行推送)。 +settings.protect_enable_force_push_all=启用强制推送 +settings.protect_enable_force_push_all_desc=任何拥有推送权限的人都可以对这个分支进行强制推送。 +settings.protect_enable_force_push_allowlist=强制推送白名单 +settings.protect_enable_force_push_allowlist_desc=只有白名单中的用户或团队才能强制推送到这个分支。 settings.protect_enable_merge=启用合并 settings.protect_enable_merge_desc=任何具有写入权限的人都可以将合并请求合并到此分支中。 +settings.protect_whitelist_committers=受白名单限制的推送 +settings.protect_whitelist_committers_desc=只有列入白名单的用户或团队才能被允许推送到此分支(但不能强行推送)。 +settings.protect_whitelist_deploy_keys=具有推送权限的部署密钥白名单。 +settings.protect_whitelist_users=推送白名单用户: +settings.protect_whitelist_teams=推送白名单团队: +settings.protect_force_push_allowlist_users=允许强制推送的白名单用户: +settings.protect_force_push_allowlist_teams=允许强制推送的白名单团队: +settings.protect_force_push_allowlist_deploy_keys=允许白名单中的部署密钥进行强制推送。 +settings.protect_merge_whitelist_committers=启用合并白名单 +settings.protect_merge_whitelist_committers_desc=仅允许白名单用户或团队合并合并请求到此分支。 +settings.protect_merge_whitelist_users=合并白名单用户: +settings.protect_merge_whitelist_teams=合并白名单团队: settings.protect_check_status_contexts=启用状态检查 settings.protect_status_check_patterns=状态检查模式: settings.protect_status_check_patterns_desc=输入模式,指定哪些状态检查必须通过,才能将分支合并到符合此规则的分支中去。每一行指定一个模式,模式不能为空。 +settings.protect_check_status_contexts_desc=要求状态检查通过才能合并。如果启用,提交必须先推送到另一个分支,然后再合并或推送到匹配这些保护规则的分支。如果没有选择具体的状态检查上下文,则所有的状态检查都通过才能合并。 settings.protect_check_status_contexts_list=此仓库上周进行过的状态检查 settings.protect_status_check_matched=匹配 settings.protect_invalid_status_check_pattern=无效的状态检查规则:“%s”。 settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。 settings.protect_required_approvals=所需的批准: +settings.protect_required_approvals_desc=只允许合并有足够审核的合并请求。要求的审核必须来自白名单或者有权限的用户或团队。 +settings.protect_approvals_whitelist_enabled=仅列入白名单的用户或团队才可批准 +settings.protect_approvals_whitelist_enabled_desc=只有白名单用户或团队的审核才能计数。 如果规则没有批准白名单,来自任何有写访问权限的人的审核都将计数。 +settings.protect_approvals_whitelist_users=审查者白名单: +settings.protect_approvals_whitelist_teams=审查团队白名单: settings.dismiss_stale_approvals=取消过时的批准 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。 settings.ignore_stale_approvals=忽略过期批准 @@ -2362,12 +2462,18 @@ settings.ignore_stale_approvals_desc=对旧提交(过期审核)的批准将 settings.require_signed_commits=需要签名提交 settings.require_signed_commits_desc=拒绝推送未签名或无法验证的提交到分支 settings.protect_branch_name_pattern=受保护的分支名称模式 +settings.protect_branch_name_pattern_desc=分支保护的名称匹配规则。语法请参阅 文档 。如:main, release/** settings.protect_patterns=规则 settings.protect_protected_file_patterns=受保护的文件模式(使用分号 ';' 分隔): +settings.protect_protected_file_patterns_desc=即使用户有权添加、编辑或删除此分支中的文件,也不允许直接更改受保护的文件。 可以使用分号 (';') 分隔多个模式。 见%[2]s文档了解模式语法。例如: .drone.yml, /docs/**/*.txt settings.protect_unprotected_file_patterns=不受保护的文件模式(使用分号 ';' 分隔): +settings.protect_unprotected_file_patterns_desc=如果用户有写权限,则允许直接更改的不受保护的文件,以绕过推送限制。可以使用分号分隔多个模式 (';')。 见 %[2]s 文档了解模式语法。例如: .drone.yml, /docs/**/*.txt +settings.add_protected_branch=启用保护 +settings.delete_protected_branch=禁用保护 settings.update_protect_branch_success=分支保护规则 %s 更新成功。 settings.remove_protected_branch_success=移除分支保护规则"%s"成功。 settings.remove_protected_branch_failed=移除分支保护规则"%s"失败。 +settings.protected_branch_deletion=删除分支保护 settings.protected_branch_deletion_desc=禁用分支保护允许具有写入权限的用户推送提交到此分支。继续? settings.block_rejected_reviews=拒绝审核阻止了此合并 settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动,即使有足够的批准,合并也不允许。 @@ -2375,8 +2481,11 @@ settings.block_on_official_review_requests=有官方审核阻止了代码合并 settings.block_on_official_review_requests_desc=处于评审状态时,即使有足够的批准,也不能合并。 settings.block_outdated_branch=如果合并请求已经过时,阻止合并 settings.block_outdated_branch_desc=当头部分支落后基础分支时,不能合并。 +settings.block_admin_merge_override=管理员须遵守分支保护规则 +settings.block_admin_merge_override_desc=管理员须遵守分支保护规则,不能规避该规则。 settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交: settings.merge_style_desc=合并方式 +settings.default_merge_style_desc=默认合并风格 settings.choose_branch=选择一个分支... settings.no_protected_branch=没有受保护的分支 settings.edit_protected_branch=编辑 @@ -2392,12 +2501,25 @@ settings.tags.protection.allowed.teams=允许的团队 settings.tags.protection.allowed.noone=无 settings.tags.protection.create=保护Git标签 settings.tags.protection.none=没有受保护的Git标签 +settings.tags.protection.pattern.description=你可以使用单个名称或 glob 模式匹配或正则表达式来匹配多个标签。了解详情请访问 保护Git标签指南。 settings.bot_token=Bot 令牌 settings.chat_id=聊天 ID settings.thread_id=线程 ID settings.matrix.homeserver_url=主服务器网址 settings.matrix.room_id=房间ID settings.matrix.message_type=消息类型 +settings.visibility.private.button=设为私有 +settings.visibility.private.text=将可见性更改为私有不仅会使仓库仅对允许的成员可见,而且可能会消除它与派生仓库、关注者和点赞之间的关系。 +settings.visibility.private.bullet_title=将可见性改为私有将会: +settings.visibility.private.bullet_one=使仓库只对允许的成员可见。 +settings.visibility.private.bullet_two=可能会删除它与 派生仓库关注者点赞 之间的关系。 +settings.visibility.public.button=设为公开 +settings.visibility.public.text=将可见性更改为公开会使任何人都可见。 +settings.visibility.public.bullet_title=将可见性改为公开将会: +settings.visibility.public.bullet_one=使仓库让任何人都可见。 +settings.visibility.success=仓库可见性已更改。 +settings.visibility.error=试图更改仓库可见性时出错。 +settings.visibility.fork_error=无法更改派生仓库的可见性。 settings.archive.button=归档仓库 settings.archive.header=归档此仓库 settings.archive.text=归档仓库将使其完全只读。它将在首页隐藏。没有人(甚至你!)能够进行新的提交,或打开工单及合并请求。 @@ -2509,6 +2631,7 @@ release.new_release=发布新版 release.draft=草稿 release.prerelease=预发行 release.stable=稳定 +release.latest=最新版本 release.compare=比较 release.edit=编辑 release.ahead.commits=%d 次提交 @@ -2593,6 +2716,7 @@ tag.create_success=标签"%s"已存在 topic.manage_topics=管理主题 topic.done=保存 +topic.count_prompt=您最多选择25个主题 topic.format_prompt=主题必须以字母或数字开头,可以包含连字符 ('-') 和句点 ('.'),长度不得超过35个字符。字符必须为小写。 find_file.go_to_file=转到文件 @@ -2665,6 +2789,7 @@ settings.delete_prompt=删除操作会永久清除该组织的信息,并且 所有仓库。 settings.labels_desc=添加能够被该组织下的 所有仓库 的工单使用的标签。 @@ -2689,6 +2814,7 @@ teams.leave.detail=离开 %s? teams.can_create_org_repo=创建仓库 teams.can_create_org_repo_helper=成员可以在组织中创建仓库。创建者将自动获得创建的仓库的管理员权限。 teams.none_access=无访问权限 +teams.none_access_helper=成员无法查看此单元或对其执行任何其他操作。对公开仓库无此影响。 teams.general_access=常规访问 teams.general_access_helper=成员权限将由以下权限表决定。 teams.read_access=可读 @@ -2757,6 +2883,7 @@ last_page=末页 total=总计:%d settings=管理设置 +dashboard.new_version_hint=Gitea %s 现已可用,您正在运行 %s。查看 博客 了解详情。 dashboard.statistic=摘要 dashboard.maintenance_operations=运维 dashboard.system_status=系统状态 @@ -2799,6 +2926,7 @@ dashboard.reinit_missing_repos=重新初始化所有丢失的 Git 仓库存在 dashboard.sync_external_users=同步外部用户数据 dashboard.cleanup_hook_task_table=清理 hook_task 表 dashboard.cleanup_packages=清理过期的软件包 +dashboard.cleanup_actions=清理过期的 Actions 资源 dashboard.server_uptime=服务运行时间 dashboard.current_goroutine=当前 Goroutines 数量 dashboard.current_memory_usage=当前内存使用量 @@ -2828,12 +2956,19 @@ dashboard.total_gc_time=GC 暂停时间总量 dashboard.total_gc_pause=GC 暂停时间总量 dashboard.last_gc_pause=上次 GC 暂停时间 dashboard.gc_times=GC 执行次数 +dashboard.delete_old_actions=从数据库中删除所有旧操作记录 +dashboard.delete_old_actions.started=已开始从数据库中删除所有旧操作记录。 dashboard.update_checker=更新检查器 dashboard.delete_old_system_notices=从数据库中删除所有旧系统通知 dashboard.gc_lfs=垃圾回收 LFS 元数据 +dashboard.stop_zombie_tasks=停止僵尸任务 +dashboard.stop_endless_tasks=停止无法停止的任务 +dashboard.cancel_abandoned_jobs=取消丢弃的任务 +dashboard.start_schedule_tasks=开始Actions调度任务 dashboard.sync_branch.started=分支同步已开始 dashboard.sync_tag.started=标签同步已开始 dashboard.rebuild_issue_indexer=重建工单索引 +dashboard.sync_repo_licenses=重新仓库许可证探测 users.user_manage_panel=用户帐户管理 users.new_account=创建新帐户 @@ -2905,6 +3040,10 @@ emails.not_updated=无法更新请求的电子邮件地址: %v emails.duplicate_active=此电子邮件地址已被另一个用户激活使用。 emails.change_email_header=更新电子邮件属性 emails.change_email_text=您确定要更新该电子邮件地址吗? +emails.delete=删除电子邮件 +emails.delete_desc=您确定要删除该电子邮件地址? +emails.deletion_success=电子邮件地址已被删除。 +emails.delete_primary_email_error=您不能删除主电子邮件。 orgs.org_manage_panel=组织管理 orgs.name=名称 @@ -2937,10 +3076,12 @@ packages.size=大小 packages.published=已发布 defaulthooks=默认Web钩子 +defaulthooks.desc=当某些 Gitea 事件触发时,Web 钩子自动向服务器发出 HTTP POST 请求。这里定义的 Web 钩子是默认配置,将被复制到所有新的仓库中。详情请访问 Web 钩子指南。 defaulthooks.add_webhook=添加默认Web 钩子 defaulthooks.update_webhook=更新默认 Web 钩子 systemhooks=系统 Web 钩子 +systemhooks.desc=当某些 Gitea 事件触发时,Web 钩子自动向服务器发出HTTP POST请求。这里定义的 Web 钩子将作用于系统上的所有仓库,所以请考虑这可能带来的任何性能影响。了解详情请访问 Web 钩子指南。 systemhooks.add_webhook=添加系统 Web 钩子 systemhooks.update_webhook=更新系统 Web 钩子 @@ -3035,8 +3176,18 @@ auths.tips=帮助提示 auths.tips.oauth2.general=OAuth2 认证 auths.tips.oauth2.general.tip=当注册新的 OAuth2 身份验证时,回调/重定向 URL 应该是: auths.tip.oauth2_provider=OAuth2 提供程序 +auths.tip.bitbucket=在 %s 注册新的 OAuth 使用者同时添加权限“账号”-“读取” auths.tip.nextcloud=使用下面的菜单“设置(Settings) -> 安全(Security) -> OAuth 2.0 client”在您的实例上注册一个新的 OAuth 客户端。 +auths.tip.dropbox=在 %s 上创建一个新的应用程序 +auths.tip.facebook=`在 %s 注册一个新的应用,并添加产品"Facebook 登录"` +auths.tip.github=在 %s 注册一个 OAuth 应用程序 +auths.tip.gitlab_new=在 %s 注册一个新的应用 +auths.tip.google_plus=从谷歌 API 控制台 %s 获得 OAuth2 客户端凭据 auths.tip.openid_connect=使用 OpenID 连接发现 URL ({server}/.well-known/openid-configuration) 来指定终点 +auths.tip.twitter=访问 %s,创建应用并确保启用了"允许此应用程序用于登录 Twitter"的选项。 +auths.tip.discord=在 %s 上注册新应用程序 +auths.tip.gitea=注册一个新的 OAuth2 应用程序。可以访问 %s 查看帮助 +auths.tip.yandex=在 %s 上创建一个新的应用程序。在“ Yandex.Passport API”这部分中选择以下权限:“访问电子邮件地址(Access to email address)”,“访问用户头像(Access to user avatar)”和“访问用户名,名字和姓氏,性别(Access to username, first name and surname, genderAccess to username, first name and surname, gender)” auths.tip.mastodon=输入您想要认证的 mastodon 实例的自定义 URL (或使用默认值) auths.edit=修改认证源 auths.activated=该认证源已经启用 @@ -3153,6 +3304,9 @@ config.cache_interval=Cache 周期 config.cache_conn=Cache 连接字符串 config.cache_item_ttl=缓存项目 TTL config.cache_test=测试缓存 +config.cache_test_failed=缓存测试失败: %v。 +config.cache_test_slow=缓存测试成功,但响应缓慢: %s。 +config.cache_test_succeeded=缓存测试成功,在 %s 时间内得到响应。 config.session_config=Session 配置 config.session_provider=Session 提供者 @@ -3199,6 +3353,7 @@ monitor.next=下次执行时间 monitor.previous=上次执行时间 monitor.execute_times=执行次数 monitor.process=运行中进程 +monitor.stacktrace=调用栈踪迹 monitor.processes_count=%d 个进程 monitor.download_diagnosis_report=下载诊断报告 monitor.desc=进程描述 @@ -3206,6 +3361,8 @@ monitor.start=开始时间 monitor.execute_time=执行时长 monitor.last_execution_result=结果 monitor.process.cancel=中止进程 +monitor.process.cancel_desc=中止一个进程可能导致数据丢失 +monitor.process.cancel_notices=中止:%s ? monitor.process.children=子进程 monitor.queues=队列 @@ -3307,6 +3464,8 @@ raw_minutes=分钟 [dropzone] default_message=拖动文件或者点击此处上传。 +invalid_input_type=您不能上传该类型的文件 +file_too_big=文件体积({{filesize}} MB)超过了最大允许体积({{maxFilesize}} MB) remove_file=移除文件 [notification] @@ -3378,6 +3537,8 @@ alpine.repository=仓库信息 alpine.repository.branches=分支 alpine.repository.repositories=仓库管理 alpine.repository.architectures=架构 +arch.registry=添加具有相关仓库和架构的服务器到 /etc/pacman.conf 中: +arch.install=使用 pacman 同步软件包: arch.repository=仓库信息 arch.repository.repositories=仓库管理 arch.repository.architectures=架构 @@ -3456,6 +3617,7 @@ settings.link=将此软件包链接到仓库 settings.link.description=如果您将一个软件包与一个代码库链接起来,软件包将显示在代码库的软件包列表中。 settings.link.select=选择仓库 settings.link.button=更新仓库链接 +settings.link.success=仓库链接已成功更新。 settings.link.error=更新仓库链接失败。 settings.delete=删除软件包 settings.delete.description=删除软件包是永久性的,无法撤消。 @@ -3579,12 +3741,18 @@ runs.no_workflows.quick_start=不知道如何使用 Gitea Actions吗?请查看 runs.no_workflows.documentation=关于Gitea Actions的更多信息,请参阅 文档。 runs.no_runs=工作流尚未运行过。 runs.empty_commit_message=(空白的提交消息) +runs.expire_log_message=旧的日志已被清除 workflow.disable=禁用工作流 workflow.disable_success=工作流 '%s' 已成功禁用。 workflow.enable=启用工作流 workflow.enable_success=工作流 '%s' 已成功启用。 workflow.disabled=工作流已禁用。 +workflow.run=运行工作流 +workflow.not_found=工作流 %s 未找到。 +workflow.run_success=工作流 %s 已成功运行。 +workflow.from_ref=使用工作流从 +workflow.has_workflow_dispatch=此 Workflow 有一个 Workflow_dispatch 事件触发器。 need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。 @@ -3605,6 +3773,7 @@ variables.update.failed=编辑变量失败。 variables.update.success=该变量已被编辑。 [projects] +deleted.display_name=已删除项目 type-1.display_name=个人项目 type-2.display_name=仓库项目 type-3.display_name=组织项目 From 2d7e6e9482da19aae4660823f38c2153cf942bb8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 17 Dec 2024 09:15:18 +0800 Subject: [PATCH 29/49] Fix various trivial problems (#32861) 1. add/improve comments to help future readers could understand the problem more easily. 2. add an error log to LDAP with username fallback 3. use `or` instead of `Iif` for "repo/branch_dropdown" (`Iif` was a mistake, but it doesn't really affect the UI) 4. add `tw-font-mono` style to container digest to match dockerhub 5. fix a bug in RepoBranchTagSelector: the form is not updated when there is no click to an item --------- Co-authored-by: delvh --- models/actions/run_job_status_test.go | 2 +- modules/markup/markdown/markdown.go | 3 ++- modules/repository/fork.go | 3 +++ routers/web/repo/actions/view.go | 3 +++ services/auth/source/ldap/source_authenticate.go | 6 ++++++ templates/package/content/container.tmpl | 3 ++- templates/repo/commit_page.tmpl | 4 +++- templates/repo/issue/filter_item_label.tmpl | 3 +++ templates/user/auth/signin_inner.tmpl | 1 + templates/user/auth/signup_inner.tmpl | 2 ++ templates/user/dashboard/issues.tmpl | 2 ++ web_src/js/components/RepoBranchTagSelector.vue | 10 ++++++++++ 12 files changed, 38 insertions(+), 4 deletions(-) diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go index 04fd9ceba7b..523d38327e4 100644 --- a/models/actions/run_job_status_test.go +++ b/models/actions/run_job_status_test.go @@ -69,7 +69,7 @@ func TestAggregateJobStatus(t *testing.T) { {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, // skipped with other status - // TODO: need to clarify whether a PR with "skipped" job status is considered as "mergeable" or not. + // "all skipped" is also considered as "mergeable" by "services/actions.toCommitStatus", the same as GitHub {[]Status{StatusSkipped}, StatusSkipped}, {[]Status{StatusSkipped, StatusSuccess}, StatusSuccess}, {[]Status{StatusSkipped, StatusFailure}, StatusFailure}, diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index a14c0cad597..b5fffccdb98 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -129,7 +129,8 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { Enabled: setting.Markdown.EnableMath, ParseDollarInline: true, ParseDollarBlock: true, - ParseSquareBlock: true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options) + ParseSquareBlock: true, // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options) + // ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future }), meta.Meta, ), diff --git a/modules/repository/fork.go b/modules/repository/fork.go index d5306340716..84ed4b23d67 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -12,6 +12,9 @@ import ( "code.gitea.io/gitea/modules/setting" ) +// CanUserForkBetweenOwners returns true if user can fork between owners. +// By default, a user can fork a repository from another owner, but not from themselves. +// Many users really like to fork their own repositories, so add an experimental setting to allow this. func CanUserForkBetweenOwners(id1, id2 int64) bool { if id1 != id2 { return true diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index b711038da06..ba17fa427d1 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -205,8 +205,11 @@ func ViewPost(ctx *context_module.Context) { } } + // TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does. + // need to be refactored together in the future metas := ctx.Repo.Repository.ComposeMetas(ctx) + // the title for the "run" is from the commit message resp.State.Run.Title = run.Title resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas) resp.State.Run.Link = run.Link() diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 020e5784dcf..6a6c60cd40e 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" auth_module "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" asymkey_service "code.gitea.io/gitea/services/asymkey" source_service "code.gitea.io/gitea/services/auth/source" @@ -31,7 +32,12 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u return nil, user_model.ErrUserNotExist{Name: loginName} } // Fallback. + // FIXME: this fallback would cause problems when the "Username" attribute is not set and a user inputs their email. + // In this case, the email would be used as the username, and will cause the "CreateUser" failure for the first login. if sr.Username == "" { + if strings.Contains(userName, "@") { + log.Error("No username in search result (Username Attribute is not set properly?), using email as username might cause problems") + } sr.Username = userName } if sr.Mail == "" { diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 207774bfefd..04732d276ae 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -36,9 +36,10 @@ {{range .PackageDescriptor.Metadata.Manifests}} + {{/* "unknown/unknown" is attestation-manifest, so we should skip it */}} {{if ne .Platform "unknown/unknown"}} - {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} {{.Platform}} {{FileSize .Size}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 8792c4e1b2b..71f77154fb9 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -68,11 +68,13 @@

+ {{/*FIXME: CurrentRefShortName seems not making sense here (old code), + because the "commit page" has no "$.BranchName" info, so only using DefaultBranch should be enough */}} {{template "repo/branch_dropdown" dict "Repository" .Repository "ShowTabBranches" true "CurrentRefType" "branch" - "CurrentRefShortName" (Iif $.BranchName $.Repository.DefaultBranch) + "CurrentRefShortName" (or $.BranchName $.Repository.DefaultBranch) "RefFormActionTemplate" (print "{RepoLink}/_cherrypick/" .CommitID "/{RefShortName}") }}
diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl index 927328ba14a..88e2e431205 100644 --- a/templates/repo/issue/filter_item_label.tmpl +++ b/templates/repo/issue/filter_item_label.tmpl @@ -23,6 +23,9 @@
{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}} {{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}} + {{/* The logic here is not the same as the label selector in the issue sidebar. + The one in the issue sidebar renders "repo labels | divider | org labels". + Maybe the logic should be updated to be consistent.*/}} {{$previousExclusiveScope := "_no_scope"}} {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 9daa051e061..dd608e5aa1e 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -48,6 +48,7 @@
{{end}}{{/*if .EnablePasswordSignInForm*/}} + {{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}} {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}} {{if and $showOAuth2Methods .EnablePasswordSignInForm}}
{{ctx.Locale.Tr "sign_in_or"}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index ea8d0bafe40..b3b2a4205e1 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -47,6 +47,8 @@
{{end}} + {{/* "oauth_container" contains not only "oauth2" methods, but also "OIDC" and "SSPI" methods */}} + {{/* TODO: it seems that "EnableSSPI" is only set in "sign-in" handlers, but it should use the same logic to control its display */}} {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}} {{if $showOAuth2Methods}}
{{ctx.Locale.Tr "sign_in_or"}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 7f960a4709a..7924dd23057 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -61,6 +61,7 @@ {{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLinkWithFilter "SupportArchivedLabel" true}} {{end}} + {{/* at the moment there is no easy way to get poster candidates on this page, so only show a username input, search for what the end user enters */}} {{if ne $.ViewType "created_by"}} {{template "repo/issue/filter_item_user_fetch" dict "QueryParamKey" "poster" @@ -70,6 +71,7 @@ }} {{end}} + {{/* at the moment there is no easy way to get assignee candidates on this page, so only show a username input, search for what the end user enters */}} {{if ne $.ViewType "assigned"}} {{template "repo/issue/filter_item_user_fetch" dict "QueryParamKey" "assignee" diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 2f66336a66f..4b7ca1429dc 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -113,6 +113,16 @@ const sfc = { if (this.menuVisible) this.menuVisible = false; }); }, + + mounted() { + if (this.refFormActionTemplate) { + // if the selector is used in a form and needs to change the form action, + // make a mock item and select it to update the form action + const item: ListItem = {selected: true, refType: this.currentRefType, refShortName: this.currentRefShortName, rssFeedLink: ''}; + this.selectItem(item); + } + }, + methods: { selectItem(item: ListItem) { this.menuVisible = false; From e6186bc447e174bcbdb697d2b99c3767b8f3a321 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 18 Dec 2024 00:33:18 +0000 Subject: [PATCH 30/49] [skip ci] Updated translations via Crowdin --- options/locale/locale_id-ID.ini | 48 +++ options/locale/locale_zh-TW.ini | 680 +++++++++++++++++++++++++++++++- 2 files changed, 710 insertions(+), 18 deletions(-) diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 237323a0fc4..00a406aae45 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -22,11 +22,25 @@ toc=Daftar Isi username=Nama Pengguna email=Alamat Email password=Kata Sandi +re_type=Konfirmasi Kata Sandi captcha=CAPTCHA twofa=Otentikasi Dua Faktor twofa_scratch=Kode Awal Dua Faktor passcode=Kode Akses +webauthn_insert_key=Masukkan kunci keamanan anda +webauthn_sign_in=Tekan tombol pada kunci keamanan Anda. Jika kunci keamanan Anda tidak memiliki tombol, masukkan kembali. +webauthn_press_button=Silakan tekan tombol pada kunci keamanan Anda… +webauthn_use_twofa=Gunakan kode dua faktor dari telepon Anda +webauthn_error=Tidak dapat membaca kunci keamanan Anda. +webauthn_unsupported_browser=Browser Anda saat ini tidak mendukung WebAuthn. +webauthn_error_unknown=Terdapat kesalahan yang tidak diketahui. Mohon coba lagi. +webauthn_error_insecure=`WebAuthn hanya mendukung koneksi aman. Untuk pengujian melalui HTTP, Anda dapat menggunakan "localhost" atau "127.0.0.1"` +webauthn_error_unable_to_process=Server tidak dapat memproses permintaan Anda. +webauthn_error_duplicated=Kunci keamanan tidak diperbolehkan untuk permintaan ini. Pastikan bahwa kunci ini belum terdaftar sebelumnya. +webauthn_error_empty=Anda harus menetapkan nama untuk kunci ini. +webauthn_error_timeout=Waktu habis sebelum kunci Anda dapat dibaca. Mohon muat ulang halaman ini dan coba lagi. +webauthn_reload=Muat ulang repository=Repositori organization=Organisasi @@ -36,6 +50,8 @@ new_migrate=Migrasi Baru new_mirror=Duplikat Baru new_fork=Fork Repositori Baru new_org=Organisasi Baru +new_project=Proyek Baru +new_project_column=Kolom Baru manage_org=Mengelola Organisasi admin_panel=Administrasi Situs account_settings=Pengaturan Akun @@ -55,29 +71,58 @@ pull_requests=Tarik Permintaan issues=Masalah milestones=Tonggak +ok=Oke cancel=Batal +retry=Coba lagi +rerun=Jalankan ulang +rerun_all=Jalankan ulang semua job save=Simpan add=Tambah add_all=Tambah Semua remove=Buang remove_all=Buang Semua +remove_label_str=`Hapus item "%s"` edit=Edit +view=Tampilan +test=Pengujian enabled=Aktif disabled=Nonaktif +locked=Terkunci +copy=Salin +copy_url=Salin URL +copy_hash=Salin hash +copy_content=Salin konten +copy_branch=Salin nama branch +copy_success=Tersalin! +copy_error=Gagal menyalin +copy_type_unsupported=Tipe berkas ini tidak dapat disalin write=Tulis preview=Pratinjau loading=Memuat… +error=Gangguan +error404=Halaman yang akan kamu akses tidak dapat ditemukan atau kamu tidak memiliki akses untuk melihatnya. +go_back=Kembali +invalid_data=Data invalid: %v +never=Tidak Pernah +unknown=Tidak diketahui +rss_feed=Umpan Berita +pin=Sematkan +unpin=Lepas sematan +artifacts=Artefak +confirm_delete_artifact=Apakah Anda yakin ingin menghapus artefak '%s' ? archived=Diarsipkan +concept_system_global=Global +concept_user_individual=Perorangan concept_code_repository=Repositori show_full_screen=Tampilkan layar penuh @@ -682,13 +727,16 @@ commits.newer=Terbaru commits.signed_by=Ditandai oleh +commitstatus.error=Gangguan projects.description_placeholder=Deskripsi projects.title=Judul +projects.new=Proyek Baru projects.template.desc=Contoh projects.column.edit_title=Nama projects.column.new_title=Nama +projects.column.new=Kolom Baru issues.new=Masalah Baru issues.new.labels=Label diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 3b1d37a322a..fb93a2ff076 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -4,6 +4,7 @@ explore=探索 help=說明 logo=標誌 sign_in=登入 +sign_in_with_provider=使用 %s 帳戶登入 sign_in_or=或 sign_out=登出 sign_up=註冊 @@ -16,6 +17,7 @@ template=模板 language=語言 notifications=通知 active_stopwatch=進行中的時間追蹤 +tracked_time_summary=目前的 issue 累計時長 create_new=建立... user_profile_and_more=個人資料和設定... signed_in_as=已登入 @@ -23,6 +25,7 @@ enable_javascript=本網站需要 JavaScript。 toc=目錄 licenses=授權條款 return_to_gitea=返回 Gitea +more_items=更多項目 username=帳號 email=電子信箱 @@ -80,6 +83,8 @@ milestones=里程碑 ok=確認 cancel=取消 retry=重試 +rerun=重新執行 +rerun_all=重新執行所有工作 save=儲存 add=增加 add_all=全部增加 @@ -87,14 +92,19 @@ remove=移除 remove_all=全部移除 remove_label_str=移除項目「%s」 edit=編輯 +view=檢視 +test=測試 enabled=已啟用 disabled=已停用 +locked=已上鎖 copy=複製 copy_url=複製 URL +copy_hash=複製哈希值 copy_content=複製內容 copy_branch=複製分支名稱 +copy_path=複製路徑 copy_success=複製成功! copy_error=複製失敗 copy_type_unsupported=無法複製此類型的檔案 @@ -105,6 +115,8 @@ loading=載入中… error=錯誤 error404=您正嘗試訪問的頁面 不存在您尚未被授權 查看該頁面。 +go_back=返回 +invalid_data=無效的資料: %v never=從來沒有 unknown=未知 @@ -114,25 +126,67 @@ rss_feed=RSS 摘要 pin=固定 unpin=取消固定 +artifacts=檔案或工件 +confirm_delete_artifact=你確定要刪除這個檔案 '%s' 嗎? archived=已封存 +concept_system_global=全域 +concept_user_individual=個人 concept_code_repository=儲存庫 concept_user_organization=組織 +show_timestamps=顯示時間戳記 +show_log_seconds=顯示秒數 +show_full_screen=全螢幕顯示 +download_logs=下載記錄 +confirm_delete_selected=確定要刪除所有已選取的項目嗎? name=名稱 value=值 filter=篩選 +filter.clear=清除篩選器 filter.is_archived=已封存 +filter.not_archived=未歸檔 +filter.is_fork=已分支 +filter.not_fork=未分支 +filter.is_mirror=已鏡像 +filter.not_mirror=不是鏡像 filter.is_template=模板 +filter.not_template=不是範本 filter.public=公開 filter.private=私有 +no_results_found=找不到結果。 +internal_error_skipped=已略過內部錯誤:%s [search] +search=搜尋… +type_tooltip=搜尋類型 +fuzzy=模糊 +fuzzy_tooltip=包含與搜尋詞接近的結果 +exact=精確 +exact_tooltip=只包含完全符合關鍵字的結果 +repo_kind=搜尋儲存庫… +user_kind=搜尋使用者… +org_kind=搜尋組織… +team_kind=搜尋團隊… +code_kind=搜尋代碼… +code_search_unavailable=現在無法使用原始碼搜尋。請與網站管理員聯絡。 +code_search_by_git_grep=目前的原始碼搜尋結果是由「git grep」提供。如果網站管理者啟用 Repository Indexer 可能可以提供更好的結果。 +package_kind=搜尋軟體包... +project_kind=搜尋專案… +branch_kind=搜尋分支… +tag_kind=搜尋標籤… +tag_tooltip=搜尋符合的標籤。使用「%」以比對任意長度的數字。 +commit_kind=搜尋提交歷史… +runner_kind=搜尋 Runner... +no_results=找不到符合的結果。 +issue_kind=搜尋議題… +pull_kind=搜尋合併請求... +keyword_search_unavailable=現在無法使用關鍵字搜尋。請與網站管理員聯絡。 [aria] navbar=導航列 @@ -156,9 +210,13 @@ buttons.link.tooltip=新增連結 buttons.list.unordered.tooltip=新增項目符號清單 buttons.list.ordered.tooltip=新增編號清單 buttons.list.task.tooltip=新增工作項目清單 +buttons.table.add.tooltip=新增表格 buttons.table.add.insert=增加 +buttons.table.rows=行 +buttons.table.cols=列 buttons.mention.tooltip=提及使用者或團隊 buttons.ref.tooltip=參考問題或合併請求 +buttons.switch_to_legacy.tooltip=使用舊版編輯器代替 buttons.enable_monospace_font=啟用等寬字型 buttons.disable_monospace_font=停用等寬字型 @@ -168,16 +226,20 @@ string.desc=Z - A [error] occurred=發生錯誤 +report_message=如果你確定這是一個 Gitea 的 bug,請去 GitHub 搜尋相關的問題,如果有需要你也可以開一個新的問題 not_found=找不到目標。 network_error=網路錯誤 [startpage] app_desc=一套極易架設的 Git 服務 install=安裝容易 +install_desc=直接用 執行檔安裝,還可以透過 Docker部屬,或是取得 套件。 platform=跨平台 +platform_desc=Gitea 可以在所有能編譯 Go 語言的平台上執行: Windows, macOS, Linux, ARM 等等。挑一個您喜歡的吧! lightweight=輕量級 lightweight_desc=一片便宜的 Raspberry Pi 就可以滿足 Gitea 的最低需求。節省您的機器資源! license=開放原始碼 +license_desc=取得 code.gitea.io/gitea !成為一名貢獻者和我們一起讓 Gitea 更好,快點加入我們吧! [install] install=安裝頁面 @@ -216,6 +278,7 @@ repo_path_helper=所有遠端 Git 儲存庫會儲存到此目錄。 lfs_path=Git LFS 根目錄 lfs_path_helper=以 Git LFS 儲存檔案時會被儲存在此目錄中。請留空以停用 LFS 功能。 run_user=以使用者名稱執行 +run_user_helper=輸入 Gitea 執行的作業系統使用者名稱。請注意,此使用者必須擁有存儲庫根目錄的存取權限。 domain=伺服器域名 domain_helper=伺服器的域名或主機位置。 ssh_port=SSH 伺服器埠 @@ -232,6 +295,7 @@ email_title=電子郵件設定 smtp_addr=SMTP 主機 smtp_port=SMTP 連接埠 smtp_from=電子郵件寄件者 +smtp_from_invalid=「以此電子信箱寄送」的地址無效 smtp_from_helper=Gitea 將會使用的電子信箱,直接輸入電子信箱或使用「"名稱" 」的格式。 mailer_user=SMTP 帳號 mailer_password=SMTP 密碼 @@ -287,8 +351,12 @@ invalid_password_algorithm=無效的密碼雜湊演算法 password_algorithm_helper=設定密碼雜湊演算法。演算法有不同的需求與強度。argon2 演算法雖然較安全但會使用大量記憶體,可能不適用於小型系統。 enable_update_checker=啟用更新檢查器 enable_update_checker_helper=定期連線到 gitea.io 檢查更新。 +env_config_keys=環境組態設定 +env_config_keys_prompt=下列環境變數也會套用到您的組態檔: +config_write_file_prompt=這些配置選項將被寫入到: %s [home] +nav_menu=導覽選單 uname_holder=帳號或電子信箱 password_holder=密碼 switch_dashboard_context=切換資訊主頁帳戶 @@ -318,6 +386,7 @@ issues.in_your_repos=在您的儲存庫中 repos=儲存庫 users=使用者 organizations=組織 +go_to=前往 code=程式碼 code_last_indexed_at=最後索引 %s relevant_repositories_tooltip=已隱藏缺少主題、圖示、說明、Fork 的儲存庫。 @@ -325,27 +394,38 @@ relevant_repositories=只顯示相關的儲存庫,顯示未篩選 [auth] create_new_account=註冊帳戶 +already_have_account=已經有帳號嗎? +sign_in_now=立即登入! disable_register_prompt=註冊功能已停用。 請聯繫您的網站管理員。 disable_register_mail=已停用註冊確認電子郵件。 manual_activation_only=請聯絡您的網站管理員以完成啟用程序。 remember_me=記得這個裝置 +remember_me.compromised=這個登入 token 已經失效,可能代表著帳號已被入侵。請確認您的帳號是否有不尋常的活動。 forgot_password_title=忘記密碼 forgot_password=忘記密碼? +need_account=需要一個帳號? +sign_up_now=還沒有帳戶?馬上註冊。 +sign_up_successful=帳戶已成功建立。歡迎您! +confirmation_mail_sent_prompt_ex=新的確認信已寄到%s。請在接下來的 %s 確認您的收件夾來完成註冊手續。如果您的註冊地址有錯誤,您可以再次登入並修改它。 must_change_password=更新您的密碼 allow_password_change=要求使用者更改密碼 (推薦) reset_password_mail_sent_prompt=確認信已發送至 %s。請在 %s內檢查您的收件匣並完成帳戶救援作業。 active_your_account=啟用您的帳戶 account_activated=帳戶已啟用 prohibit_login=禁止登入 +prohibit_login_desc=您的帳戶被禁止登入,請聯絡網站管理員 resent_limit_prompt=抱歉,您請求發送驗證電子郵件太過頻繁,請等待 3 分鐘後再試一次。 has_unconfirmed_mail=%s 您好,您有一封發送至( %s) 但未被確認的郵件。如果您未收到啟用郵件,或需要重新發送,請單擊下方的按鈕。 +change_unconfirmed_mail_address=如果您註冊的電子郵件地址有錯誤,您可以在這邊更正,並重新寄送確認郵件。 resend_mail=單擊此處重新發送確認郵件 email_not_associate=此電子信箱未與任何帳戶連結 send_reset_mail=發送帳戶救援信 reset_password=帳戶救援 invalid_code=您的確認代碼無效或已過期。 +invalid_code_forgot_password=您的確認代碼無效或已過期。開啟 這個連結 開始新的 session。 invalid_password=您的密碼和用來建立帳戶的不符。 reset_password_helper=帳戶救援 +reset_password_wrong_user=您已經使用 %s 的帳戶登入,但帳戶救援連結是給 %s 的 password_too_short=密碼長度不能少於 %d 個字! non_local_account=非本地帳戶無法透過 Gitea 的網頁介面更改密碼。 verify=驗證 @@ -365,11 +445,13 @@ oauth_signin_submit=連結帳戶 oauth.signin.error=處理授權請求時發生錯誤。如果這個問題持續發生,請聯絡網站管理員。 oauth.signin.error.access_denied=授權請求被拒絕。 oauth.signin.error.temporarily_unavailable=授權失敗,因為認證伺服器暫時無法使用。請稍後再試。 +oauth_callback_unable_auto_reg=自助註冊已啟用,但是 OAuth2 提供者 %[1]s 回傳的結果缺少欄位:%[2]s,導致無法自動建立帳號。請建立新帳號或是連結至既存的帳號,或是聯絡網站管理者。 openid_connect_submit=連接 openid_connect_title=連接到現有帳戶 openid_connect_desc=所選的 OpenID URI 未知。在這裡連結一個新帳戶。 openid_register_title=建立新帳戶 openid_register_desc=所選的 OpenID URI 未知。在這裡連結一個新帳戶。 +openid_signin_desc=輸入您的 OpenID 位址。例如:alice.openid.example.org 或是 https://openid.example.org/alice。 disable_forgot_password_mail=由於未設定電子郵件功能,帳戶救援功能已被停用。請與網站管理員聯絡。 disable_forgot_password_mail_admin=帳戶救援功能需要設定電子郵件功能才能使用。請設定電子郵件功能以啟用帳戶救援功能。 email_domain_blacklisted=您無法使用您的電子信箱註冊帳號。 @@ -379,8 +461,13 @@ authorize_application_created_by=此應用程式是由 %s 建立的。 authorize_application_description=如果您允許,它將能夠讀取和修改您的所有帳戶資訊,包括私有儲存庫和組織。 authorize_title=授權「%s」存取您的帳戶? authorization_failed=授權失效 +authorization_failed_desc=授權失敗,因為我們偵測到無效的請求。請聯絡您欲授權之應用程式的維護人員。 sspi_auth_failed=SSPI 認證失敗 +password_pwned=您選擇的密碼已被列於被盜密碼清單中,該清單因公共資料外洩而暴露。請試試其他密碼。 password_pwned_err=無法完成對 HaveIBeenPwned 的請求。 +last_admin=您無法移除最後一個管理員。至少需要保留一個管理員帳戶。 +signin_passkey=使用 Passkey 登入 +back_to_sign_in=返回至登入 [mail] view_it_on=在 %s 上查看 @@ -394,8 +481,10 @@ activate_account.text_1=%[1]s 您好,感謝您註冊 %[2]s! activate_account.text_2=請在 %s內點擊下列連結以啟用您的帳戶: activate_email=請驗證您的電子信箱 +activate_email.title=%s,請驗證您的電子信箱 activate_email.text=請在 %s內點擊下列連結以驗證您的電子信箱: +register_notify=歡迎來到 Gitea register_notify.title=%[1]s,歡迎來到 %[2]s register_notify.text_1=這是您在 %s 的註冊確認信! register_notify.text_2=您現在可以用帳號 %s 登入。 @@ -497,6 +586,9 @@ lang_select_error=從清單中選擇一個語言。 username_been_taken=帳號已被使用 username_change_not_local_user=非本地使用者不允許更改他們的帳號。詳細資訊請聯絡您的系統管理員。 +change_username_disabled=更改使用者名稱功能已被停用。 +change_full_name_disabled=更改完整名稱功能已被停用。 +username_has_not_been_changed=使用者名稱並未變更 repo_name_been_taken=儲存庫名稱已被使用。 repository_force_private=已啟用「強制私有」:私有儲存庫不能被公開。 repository_files_already_exist=此儲存庫的檔案已存在,請聯絡系統管理有。 @@ -510,6 +602,7 @@ team_name_been_taken=團隊名稱已被使用。 team_no_units_error=請至少選擇一個儲存庫區域。 email_been_used=此電子信箱已被使用 email_invalid=此電子信箱無效。 +email_domain_is_not_allowed=使用者的電子郵件地址 %s 與電子郵件域名允許清單或是電子郵件域名禁止清單有衝突。請確認您預期執行這個動作。 openid_been_used=OpenID 位址「%s」已被使用。 username_password_incorrect=帳號或密碼不正確 password_complexity=密碼複雜度沒有通過以下的要求: @@ -521,6 +614,8 @@ enterred_invalid_repo_name=您輸入的儲存庫名稱不正確。 enterred_invalid_org_name=您輸入的組織名稱不正確。 enterred_invalid_owner_name=新的擁有者名稱無效。 enterred_invalid_password=您輸入的密碼不正確。 +unset_password=登入的使用者並未設定密碼。 +unsupported_login_type=這個登入方式並不支援刪除帳號。 user_not_exist=該用戶名不存在 team_not_exist=團隊不存在 last_org_owner=你不能從「Owners」團隊中刪除最後一個使用者。每個組織中至少要有一個擁有者。 @@ -542,10 +637,13 @@ org_still_own_repo=此組織仍然擁有一個以上的儲存庫,請先刪除 org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除它們。 target_branch_not_exist=目標分支不存在 +target_ref_not_exist=目標參考不存在 %s +admin_cannot_delete_self=當您是管理者時,您無法移除自己。請先移除您的管理者權限。 [user] change_avatar=更改大頭貼... +joined_on=加入於 %s repositories=儲存庫 activity=公開動態 followers=追蹤者 @@ -561,11 +659,36 @@ user_bio=個人簡介 disabled_public_activity=這個使用者已對外隱藏動態 email_visibility.limited=所有已驗證的使用者都可以看到您的電子信箱地址 email_visibility.private=只有您和系統管理員可以看到您的電子信箱地址 +show_on_map=在地圖上顯示此位置 +settings=使用者設定 form.name_reserved=「%s」是保留的帳號。 form.name_pattern_not_allowed=帳號不可包含字元「%s」。 form.name_chars_not_allowed=帳號「%s」包含無效字元。 +block.block=封鎖 +block.block.user=封鎖使用者 +block.block.org=為組織阻擋使用者 +block.block.failure=無法封鎖使用者: %s +block.unblock=解除封鎖 +block.unblock.failure=無法解除封鎖使用者: %s +block.blocked=您已封鎖該使用者。 +block.title=封鎖使用者 +block.info=阻擋使用者可避免他們與儲存庫互動,如在建立合併請求或是問題以及於其上留言。了解更多關於阻擋一個使用者。 +block.info_1=阻擋一個使用者可避免其對您的帳號或是儲存庫進行以下動作: +block.info_2=正在追蹤你的帳戶 +block.info_3=透過 @mentioning 標記您的使用者名稱來通知您 +block.info_4=邀請您成為他們的儲存庫的協作者 +block.info_5=對儲存庫加上星號、建立分之或是關注儲存庫 +block.info_6=建立問題或是合併請求,或是對其進行留言 +block.info_7=對您在問題或是合併請求中的留言送出反應 +block.user_to_block=欲阻擋的使用者 +block.note=備註 +block.note.title=選用附註: +block.note.info=被阻擋的使用者不會看到這個附註。 +block.note.edit=編輯備註 +block.list=已封鎖的使用者 +block.list.none=您尚未封鎖任何使用者。 [settings] profile=個人資料 @@ -580,10 +703,17 @@ applications=應用程式 orgs=管理組織 repos=儲存庫 delete=刪除帳戶 +twofa=兩步驟驗證 account_link=已連結帳號 organization=組織 +webauthn=安全金鑰 public_profile=公開的個人資料 +biography_placeholder=告訴我們一些關於您的事情吧! (您可以使用 Markdown) +location_placeholder=與其他人分享您的大概位置 +profile_desc=控制您的個人檔案會如何呈現給其她使用者。您的主要電子郵件地址會被用於通知、密碼救援以及網頁上的 Git 操作。 +password_username_disabled=非本地使用者不允許更改他們的帳號。詳細資訊請聯絡您的系統管理員。 +password_full_name_disabled=您不被允許更改他們的全名。詳細資訊請聯絡您的系統管理員。 full_name=全名 website=個人網站 location=所在地區 @@ -594,11 +724,16 @@ update_language_not_found=無法使用語言「%s」。 update_language_success=已更新語言。 update_profile_success=已更新您的個人資料。 change_username=您的帳號已更改。 +change_username_prompt=註:更改您的使用名稱也會更改您的帳號網址。 +change_username_redirect_prompt=舊的帳號被領用前,會重新導向您的新帳號。 continue=繼續 cancel=取消 language=語言 ui=佈景主題 hidden_comment_types=隱藏的留言類型 +hidden_comment_types_description=此處勾選的評論類型將不會顯示在問題頁面內。例如勾選「標籤」將移除所有的"{user} 新增/移除{label}" 評論。 +hidden_comment_types.ref_tooltip=當這個問題在其他的問題、提交…等地方被引用時的留言 +hidden_comment_types.issue_ref_tooltip=當使用者更改與這個問題相關聯的分支、標籤時的留言 comment_type_group_reference=參考 comment_type_group_label=標籤 comment_type_group_milestone=里程碑 @@ -615,6 +750,7 @@ comment_type_group_project=專案 comment_type_group_issue_ref=問題參考 saved_successfully=您的設定已成功儲存。 privacy=隱私 +keep_activity_private=隱藏個人檔案上的動態 keep_activity_private_popup=讓動態只有你和管理員看得到 lookup_avatar_by_mail=以電子信箱查詢大頭貼 @@ -624,8 +760,10 @@ choose_new_avatar=選擇新的大頭貼 update_avatar=更新大頭貼 delete_current_avatar=刪除目前的大頭貼 uploaded_avatar_not_a_image=上傳的檔案不是圖片 +uploaded_avatar_is_too_big=上傳檔案大小 (%d KiB) 超過大小上限 (%d KiB) update_avatar_success=您的大頭貼已更新 update_user_avatar_success=已更新使用者的大頭貼。 +cropper_prompt=您可以在儲存前編輯圖片。編輯後的圖片將以 PNG 格式儲存。 change_password=更新密碼 old_password=目前的密碼 @@ -639,13 +777,17 @@ emails=電子信箱 manage_emails=管理電子信箱 manage_themes=選擇預設佈景主題 manage_openid=管理 OpenID 位址 +email_desc=您的主要電子信箱將用於通知、密碼恢復以及(如果未隱藏)基於網頁的 Git 操作。 theme_desc=這將是您在整個網站上的預設佈景主題。 +theme_colorblindness_help=色盲主題支援 +theme_colorblindness_prompt=Gitea 剛取得了一些具有基本色盲支援的主題,其中僅定義了幾種顏色。這項工作仍在進行中。透過在主題 CSS 檔案中定義更多顏色可以完成更多改進。 primary=主要 activated=已啟用 requires_activation=需要啟動 primary_email=設為主要 activate_email=寄出啟用信 activations_pending=等待啟用中 +can_not_add_email_activations_pending=有一個待處理的啟用,若要新增電子信箱,請幾分鐘後再試。 delete_email=移除 email_deletion=移除電子信箱 email_deletion_desc=電子信箱和相關資訊將從您的帳戶中刪除,由此電子信箱所提交的 Git 將保持不變,是否繼續? @@ -659,11 +801,13 @@ add_new_email=新增電子信箱 add_new_openid=新增 OpenID URI add_email=新增電子信箱 add_openid=新增 OpenID URI +add_email_confirmation_sent=確認信已發送至「%s」,請在 %s內檢查您的收件匣並確認您的電子信箱。 add_email_success=已加入新的電子信箱。 email_preference_set_success=已套用郵件偏好設定 add_openid_success=已加入新的 OpenID 地址。 keep_email_private=隱藏電子信箱 -openid_desc=OpenID 讓你可以授權認證給外部服務。 +keep_email_private_popup=這將隱藏您的電子郵件地址,無論是在您的個人資料中,還是在您使用網頁介面進行 pull request 或編輯文件時。推送的提交將不會被修改。使用 %s 在提交中將其與您的帳戶關聯。 +openid_desc=OpenID 讓您可以授權認證給外部服務。 manage_ssh_keys=管理 SSH 金鑰 manage_ssh_principals=管理 SSH 認證主體 @@ -724,6 +868,8 @@ ssh_principal_deletion_desc=移除 SSH 認證主體將撤銷其對您帳戶的 ssh_key_deletion_success=SSH 金鑰已被移除。 gpg_key_deletion_success=GPG 金鑰已被移除。 ssh_principal_deletion_success=已移除主體。 +added_on=新增於 %s +valid_until_date=有效直到 %s valid_forever=永遠有效 last_used=上次使用在 no_activity=沒有近期動態 @@ -735,9 +881,12 @@ principal_state_desc=此主體在過去 7 天內曾被使用 show_openid=在個人資料顯示 hide_openid=從個人資料隱藏 ssh_disabled=已停用 SSH +ssh_signonly=目前已停用 SSH,因此這些金鑰僅用於 commit 簽章驗證。 ssh_externally_managed=此 SSH 金鑰由此使用者的外部服務所管理 manage_social=管理關聯的社群帳戶 +social_desc=這些帳號可以用已登入你的帳號,請確認你知道它們。 unbind=解除連結 +unbind_success=社群帳號刪除成功 manage_access_token=管理 Access Token generate_new_token=產生新的 Token @@ -752,8 +901,17 @@ access_token_deletion_cancel_action=取消 access_token_deletion_confirm_action=刪除 access_token_deletion_desc=刪除 Token 後,使用此 Token 的應用程式將無法再存取您的帳戶,此動作不可還原。是否繼續? delete_token_success=已刪除 Token。使用此 Token 的應用程式無法再存取您的帳戶。 +repo_and_org_access=儲存庫和組織存取 +permissions_public_only=僅公開 +permissions_access_all=全部 (公開、私有與受限) +select_permissions=選擇權限 +permission_not_set=尚未設定 permission_no_access=沒有權限 permission_read=讀取 +permission_write=讀取和寫入 +access_token_desc=選擇的 token 權限僅限於對應的 API 路徑授權。閱讀 文件 以了解更多資訊。 +at_least_one_permission=您必須選擇至少一個權限來建立 token +permissions_list=權限: manage_oauth2_applications=管理 OAuth2 應用程式 edit_oauth2_application=編輯 OAuth2 應用程式 @@ -763,38 +921,56 @@ remove_oauth2_application_desc=刪除 OAuth2 應用程式將會撤銷所有已 remove_oauth2_application_success=已刪除應用程式。 create_oauth2_application=新增 OAuth2 應用程式 create_oauth2_application_button=建立應用程式 +create_oauth2_application_success=您已成功建立新的 OAuth2 應用程式。 +update_oauth2_application_success=您已成功更新 OAuth2 應用程式。 oauth2_application_name=應用程式名稱 oauth2_confidential_client=機密客戶端 (Confidential Client)。請為能保持機密性的程式勾選,例如網頁應用程式。使用原生程式時不要勾選,包含桌面、行動應用程式。 +oauth2_skip_secondary_authorization=授權一次後,跳過公用客戶端的二次授權。可能會帶來安全風險。 +oauth2_redirect_uris=重新導向 URI,每行一個 URI。 save_application=儲存 oauth2_client_id=客戶端 ID oauth2_client_secret=客戶端密鑰 oauth2_regenerate_secret=重新產生密鑰 oauth2_regenerate_secret_hint=遺失您的密鑰? +oauth2_client_secret_hint=離開或重新整理此頁面後將不會再顯示密鑰。請確保您已保存它。 oauth2_application_edit=編輯 oauth2_application_create_description=OAuth2 應用程式讓您的第三方應用程式可以存取此 Gitea 上的帳戶。 +oauth2_application_remove_description=移除 OAuth2 應用程式將阻止其存取此實例上的授權使用者帳戶。是否繼續? +oauth2_application_locked=Gitea 在啟動時會預先註冊一些 OAuth2 應用程式(如果在配置中啟用)。為防止意外行為,這些應用程式無法編輯或刪除。請參閱 OAuth2 文件以獲取更多資訊。 authorized_oauth2_applications=已授權的 OAuth2 應用程式 +authorized_oauth2_applications_description=您已授權這些第三方應用程式存取您的 Gitea 帳戶。請撤銷不再需要的應用程式的存取權。 revoke_key=撤銷 revoke_oauth2_grant=撤銷存取權 -revoke_oauth2_grant_description=撤銷此第三方應用程式的存取權,此應用程式就無法再存取您的資料?您確定嗎? +revoke_oauth2_grant_description=撤銷此第三方應用程式的存取權,此應用程式就無法再存取您的資料。您確定嗎? +revoke_oauth2_grant_success=成功撤銷存取權。 +twofa_desc=兩步驟驗證可以增強您的帳戶安全性。 +twofa_recovery_tip=如果您遺失設備,您可以使用一次性恢復密鑰重新獲取帳戶存取權。 twofa_is_enrolled=您的帳戶已經啟用兩步驟驗證。 twofa_not_enrolled=您的帳戶目前尚未啟用兩步驟驗證。 twofa_disable=停用兩步驟驗證 +twofa_scratch_token_regenerate=重新產生備用驗證碼 +twofa_scratch_token_regenerated=您的單次使用恢復密鑰現在是 %s。請將其存放在安全的地方,因為它不會再次顯示。 twofa_enroll=啟用兩步驟驗證 twofa_disable_note=如有需要,您可以停用兩步驟驗證。 twofa_disable_desc=關閉兩步驟驗證會使您的帳戶安全性降低,是否繼續? +regenerate_scratch_token_desc=如果您遺失了備用驗證碼或已經使用它登入,您可以在此重新設定。 twofa_disabled=兩步驟驗證已經被關閉。 scan_this_image=使用您的授權應用程式來掃瞄圖片: or_enter_secret=或者輸入密碼: %s then_enter_passcode=然後輸入應用程式中顯示的驗證碼: passcode_invalid=無效的驗證碼,請重試。 +twofa_enrolled=您的帳戶已經啟用了兩步驟驗證。請將備用驗證碼 (%s) 保存到安全的地方,它只會顯示這麼一次! twofa_failed_get_secret=取得密鑰 (Secret) 失敗。 +webauthn_desc=安全金鑰是包含加密密鑰的硬體設備,它們可以用於兩步驟驗證。安全金鑰必須支援 WebAuthn Authenticator 標準。 webauthn_register_key=新增安全金鑰 webauthn_nickname=暱稱 webauthn_delete_key=移除安全金鑰 webauthn_delete_key_desc=如果您移除安全金鑰,將不能再使用它登入。是否繼續? +webauthn_key_loss_warning=如果您遺失了安全金鑰,您將無法存取您的帳戶。 +webauthn_alternative_tip=您可能需要設定其他的驗證方法。 manage_account_links=管理已連結的帳戶 manage_account_links_desc=這些外部帳戶已連結到您的 Gitea 帳戶。 @@ -804,8 +980,10 @@ remove_account_link=刪除已連結的帳戶 remove_account_link_desc=刪除連結帳戶將撤銷其對 Gitea 帳戶的存取權限。是否繼續? remove_account_link_success=已移除連結的帳戶。 +hooks.desc=新增 Webhook,當您擁有的所有儲存庫觸發事件時將會執行。 orgs_none=您尚未成為任一組織的成員。 +repos_none=您尚未擁有任何儲存庫。 delete_account=刪除您的帳戶 delete_prompt=此動作將永久刪除您的使用者帳戶,而且無法復原。 @@ -824,9 +1002,12 @@ visibility=使用者瀏覽權限 visibility.public=公開 visibility.public_tooltip=所有人都可以看到 visibility.limited=受限 +visibility.limited_tooltip=只有已授權的使用者可見 visibility.private=私人 +visibility.private_tooltip=僅對您已加入的組織成員可見 [repo] +new_repo_helper=儲存庫包含所有專案檔案,包括修訂歷史。已經在其他地方託管了嗎?遷移儲存庫。 owner=擁有者 owner_helper=組織可能因為儲存庫數量上限而未列入此選單。 repo_name=儲存庫名稱 @@ -838,6 +1019,7 @@ template_helper=將儲存庫設為範本 template_description=儲存庫範本讓使用者可新增相同目錄結構、檔案以及設定的儲存庫。 visibility=瀏覽權限 visibility_description=只有組織擁有者或有權限的組織成員才能看到。 +visibility_helper=將儲存庫設為私有 visibility_helper_forced=您的網站管理員強制新的存儲庫必需設定為私有。 visibility_fork_helper=(修改本值將會影響所有 fork 儲存庫) clone_helper=需要有關 Clone 的協助嗎?查看幫助 。 @@ -846,7 +1028,14 @@ fork_from=Fork 自 already_forked=您已經 fork 過 %s fork_to_different_account=Fork 到其他帳戶 fork_visibility_helper=無法更改 fork 儲存庫的瀏覽權限。 +fork_branch=要克隆到 fork 的分支 +all_branches=所有分支 +view_all_branches=查看所有分支 +view_all_tags=查看所有標籤 +fork_no_valid_owners=此儲存庫無法 fork,因為沒有有效的擁有者。 +fork.blocked_user=無法 fork 儲存庫,因為您被儲存庫擁有者封鎖。 use_template=使用此範本 +open_with_editor=以 %s 開啟 download_zip=下載 ZIP download_tar=下載 TAR.GZ download_bundle=下載 BUNDLE @@ -854,6 +1043,8 @@ generate_repo=產生儲存庫 generate_from=產生自 repo_desc=描述 repo_desc_helper=輸入簡介 (選用) +repo_no_desc=未提供描述 +repo_lang=儲存庫語言 repo_gitignore_helper=選擇 .gitignore 範本 repo_gitignore_helper_desc=從常見語言範本清單中挑選忽略追蹤的檔案。預設情況下各種語言建置工具產生的特殊檔案都包含在 .gitignore 中。 issue_labels=問題標籤 @@ -861,6 +1052,9 @@ issue_labels_helper=選擇問題標籤集 license=授權條款 license_helper=請選擇授權條款檔案 license_helper_desc=授權條款定義了他人使用您原始碼的允許和禁止事項。不確定哪個適用於您的專案?查看選擇授權條款。 +multiple_licenses=多重授權 +object_format=物件格式 +object_format_helper=儲存庫的物件格式。無法更改。SHA1 是最兼容的。 readme=讀我檔案 readme_helper=選擇讀我檔案範本。 readme_helper_desc=這是您能為專案撰寫完整描述的地方。 @@ -872,15 +1066,19 @@ trust_model_helper_collaborator_committer=協作者 + 提交者: 信任協作者 trust_model_helper_default=預設: 使用此 Gitea 的預設儲存庫信任模式 create_repo=建立儲存庫 default_branch=預設分支 +default_branch_label=預設 default_branch_helper=預設分支是合併請求和提交程式碼的基礎分支。 mirror_prune=裁減 mirror_prune_desc=刪除過時的遠端追蹤參考 mirror_interval=鏡像間隔 (有效時間單位為 'h'、'm'、's'),設為 0 以停用定期同步。(最小間隔: %s) mirror_interval_invalid=鏡像週期無效 +mirror_sync=已同步 mirror_sync_on_commit=推送提交後進行同步 mirror_address=從 URL Clone mirror_address_desc=在授權資訊中填入必要的資料。 -mirror_lfs=Large File Storage (LFS) +mirror_address_url_invalid=提供的 URL 無效。您必須正確轉義 URL 的所有組件。 +mirror_address_protocol_invalid=提供的 URL 無效。僅可使用 http(s):// 或 git:// 位置進行鏡像。 +mirror_lfs=大型檔案存儲 (LFS) mirror_lfs_desc=啟動 LFS 檔案的鏡像功能。 mirror_lfs_endpoint=LFS 端點 mirror_lfs_endpoint_desc=同步將會嘗試使用 Clone URL 來確認 LFS 伺服器。如果存儲庫的 LFS 資料放在其他地方,您也可以指定自訂的端點。 @@ -890,7 +1088,9 @@ mirror_password_blank_placeholder=(未設定) mirror_password_help=修改帳號以清除已儲存的密碼。 watchers=關注者 stargazers=占星術師 +stars_remove_warning=這將移除此儲存庫的所有星標。 forks=Fork +stars=星 reactions_more=再多添加 %d個 unit_disabled=網站管理員已經停用這個儲存庫區域。 language_other=其他 @@ -904,10 +1104,20 @@ delete_preexisting=刪除既有的檔案 delete_preexisting_content=刪除 %s 中的檔案 delete_preexisting_success=刪除 %s 中未接管的檔案 blame_prior=檢視此變更前的 Blame +blame.ignore_revs=忽略 .git-blame-ignore-revs 中的修訂。點擊 這裡 以繞過並查看正常的 Blame 視圖。 +blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 中的修訂失敗。 +user_search_tooltip=顯示最多 30 個使用者 +tree_path_not_found_commit=路徑 %[1]s 在提交 %[2]s 中不存在 +tree_path_not_found_branch=路徑 %[1]s 在分支 %[2]s 中不存在 +tree_path_not_found_tag=路徑 %[1]s 在標籤 %[2]s 中不存在 transfer.accept=同意轉移 +transfer.accept_desc=轉移到「%s」 transfer.reject=拒絕轉移 +transfer.reject_desc=取消轉移到「%s」 +transfer.no_permission_to_accept=您沒有權限接受此轉移。 +transfer.no_permission_to_reject=您沒有權限拒絕此轉移。 desc.private=私有 desc.public=公開 @@ -926,12 +1136,15 @@ template.issue_labels=問題標籤 template.one_item=至少須選擇一個範本項目 template.invalid=必須選擇一個儲存庫範本 -archive.issue.nocomment=此存儲庫已封存,您不能在問題上留言。 -archive.pull.nocomment=此存儲庫已封存,您不能在合併請求上留言。 +archive.title=此儲存庫已封存。您可以查看檔案並進行 Clone,但無法推送或開啟問題或合併請求。 +archive.title_date=此儲存庫已於 %s 封存。您可以查看檔案並進行 Clone,但無法推送或開啟問題或合併請求。 +archive.issue.nocomment=此儲存庫已封存,您不能在問題上留言。 +archive.pull.nocomment=此儲存庫已封存,您不能在合併請求上留言。 form.reach_limit_of_creation_1=您已經達到了您儲存庫的數量上限 (%d 個)。 form.reach_limit_of_creation_n=您已經達到了您儲存庫的數量上限 (%d 個)。 form.name_reserved=「%s」是保留的儲存庫名稱。 +form.name_pattern_not_allowed=儲存庫名稱不可包含字元「%s」。 need_auth=授權 migrate_options=遷移選項 @@ -941,6 +1154,7 @@ migrate_options_lfs=遷移 LFS 檔案 migrate_options_lfs_endpoint.label=LFS 端點 migrate_options_lfs_endpoint.description=遷移將會嘗試使用您的 Git Remote 來確認 LFS 伺服器。如果存儲庫的 LFS 資料放在其他地方,您也可以指定自訂的端點。 migrate_options_lfs_endpoint.description.local=同時也支援本地伺服器路徑。 +migrate_options_lfs_endpoint.placeholder=如果留空,端點將從 Clone URL 中推導出來 migrate_items=遷移項目 migrate_items_wiki=Wiki migrate_items_milestones=里程碑 @@ -965,6 +1179,7 @@ migrated_from_fake=已從 %[1]s 遷移 migrate.migrate=從 %s 遷移 migrate.migrating=正在從 %s 遷移... migrate.migrating_failed=從 %s 遷移失敗 +migrate.migrating_failed.error=遷移失敗: %s migrate.migrating_failed_no_addr=遷移失敗。 migrate.github.description=從 github.com 或其他 GitHub 執行個體遷移資料。 migrate.git.description=從任何 Git 服務遷移儲存庫。 @@ -974,6 +1189,9 @@ migrate.gogs.description=從 notabug.org 或其他 Gogs 執行個體遷移資料 migrate.onedev.description=從 code.onedev.io 或其他 OneDev 執行個體遷移資料。 migrate.codebase.description=從 codebasehq.com 遷移資料。 migrate.gitbucket.description=從 GitBucket 執行個體遷移資料。 +migrate.codecommit.description=從 AWS CodeCommit 遷移資料。 +migrate.codecommit.https_git_credentials_username=HTTPS Git 憑證使用者名稱 +migrate.codecommit.https_git_credentials_password=HTTPS Git 憑證密碼 migrate.migrating_git=正在遷移 Git 資料 migrate.migrating_topics=正在遷移主題 migrate.migrating_milestones=正在遷移里程碑 @@ -981,6 +1199,8 @@ migrate.migrating_labels=正在遷移標籤 migrate.migrating_releases=正在遷移版本發布 migrate.migrating_issues=正在遷移問題 migrate.migrating_pulls=正在遷移合併請求 +migrate.cancel_migrating_title=取消遷移 +migrate.cancel_migrating_confirm=您要取消遷移嗎? mirror_from=鏡像自 forked_from=fork 自 @@ -994,6 +1214,7 @@ watch=關注 unstar=移除星號 star=加上星號 fork=Fork +action.blocked_user=無法執行操作,因為您被儲存庫擁有者封鎖。 download_archive=下載此儲存庫 more_operations=更多操作 @@ -1031,6 +1252,7 @@ releases=版本發布 tag=標籤 released_this=發布了此版本 tagged_this=標記了此標籤 +file.title=%s 於 %s file_raw=原始文件 file_history=歷史記錄 file_view_source=檢視原始碼 @@ -1038,22 +1260,36 @@ file_view_rendered=檢視渲染圖 file_view_raw=查看原始文件 file_permalink=永久連結 file_too_large=檔案太大,無法顯示。 -invisible_runes_line=`這一行有看不見的 Unicode 字元` -ambiguous_runes_line=`這一行有易混淆的 Unicode 字元` +file_is_empty=檔案是空的。 +code_preview_line_from_to=第 %[1]d 行到第 %[2]d 行在 %[3]s +code_preview_line_in=第 %[1]d 行在 %[2]s +invisible_runes_header=此檔案包含不可見的 Unicode 字元 +invisible_runes_description=此檔案包含不可見的 Unicode 字元,這些字元對人類來說是無法區分的,但電腦可能會以不同方式處理。如果您認為這是有意的,可以安全地忽略此警告。使用 Escape 鍵來顯示它們。 +ambiguous_runes_header=此檔案包含易混淆的 Unicode 字元 +ambiguous_runes_description=此檔案包含可能與其他字元混淆的 Unicode 字元。如果您認為這是有意的,可以安全地忽略此警告。使用 Escape 鍵來顯示它們。 +invisible_runes_line=這一行有看不見的 Unicode 字元 +ambiguous_runes_line=這一行有易混淆的 Unicode 字元 +ambiguous_character=%[1]c [U+%04[1]X] 容易與 %[2]c [U+%04[2]X] 混淆 -escape_control_characters=Escape -unescape_control_characters=Unescape +escape_control_characters=轉義控制字元 +unescape_control_characters=取消轉義控制字元 file_copy_permalink=複製固定連結 view_git_blame=檢視 Git Blame video_not_supported_in_browser=您的瀏覽器不支援使用 HTML5 播放影片。 audio_not_supported_in_browser=您的瀏覽器不支援 HTML5 的「audio」標籤 stored_lfs=已使用 Git LFS 儲存 symbolic_link=符號連結 +executable_file=可執行檔 +vendored=已供應 +generated=已產生 commit_graph=提交線圖 commit_graph.select=選擇分支 commit_graph.hide_pr_refs=隱藏合併請求 commit_graph.monochrome=單色 commit_graph.color=彩色 +commit.contained_in=此提交包含在: +commit.contained_in_default_branch=此提交是預設分支的一部分 +commit.load_referencing_branches_and_tags=載入引用此提交的分支和標籤 blame=Blame download_file=下載檔案 normal_view=標準檢視 @@ -1081,6 +1317,7 @@ editor.or=或 editor.cancel_lower=取消 editor.commit_signed_changes=提交簽署過的變更 editor.commit_changes=提交變更 +editor.add_tmpl=新增「」 editor.add=新增 %s editor.update=更新 %s editor.delete=刪除 %s @@ -1101,8 +1338,15 @@ editor.filename_cannot_be_empty=檔案名稱不能為空。 editor.filename_is_invalid=檔名無效:「%s」。 editor.branch_does_not_exist=此儲存庫沒有名為「%s」的分支。 editor.branch_already_exists=此儲存庫已有名為「%s」的分支。 +editor.directory_is_a_file=目錄名稱「%s」已被此儲存庫的檔案使用。 +editor.file_is_a_symlink=`"%s" 是一個符號連結。符號連結無法在網頁編輯器中編輯` +editor.filename_is_a_directory=檔名「%s」已被此儲存庫的目錄名稱使用。 +editor.file_editing_no_longer_exists=正要編輯的檔案「%s」已不存在此儲存庫中。 +editor.file_deleting_no_longer_exists=正要刪除的檔案「%s」已不存在此儲存庫中。 editor.file_changed_while_editing=檔案內容在您編輯的途中已被變更。按一下此處查看更動的地方或再次提交以覆蓋這些變更。 editor.file_already_exists=此儲存庫已有名為「%s」的檔案。 +editor.commit_id_not_matching=提交 ID 與您開始編輯時的 ID 不匹配。請提交到一個補丁分支然後合併。 +editor.push_out_of_date=推送似乎已過時。 editor.commit_empty_file_header=提交空白檔案 editor.commit_empty_file_text=你準備提交的檔案是空白的,是否繼續? editor.no_changes_to_show=沒有可以顯示的變更。 @@ -1127,6 +1371,7 @@ commits.commits=次程式碼提交 commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。 commits.nothing_to_compare=這些分支是相同的。 commits.search.tooltip=你可以用「author:」、「committer:」、「after:」、「before:」等作為關鍵字的前綴,例如: 「revert author:Alice before:2019-01-13」。 +commits.search_branch=此分支 commits.search_all=所有分支 commits.author=作者 commits.message=備註 @@ -1138,6 +1383,7 @@ commits.signed_by_untrusted_user=由不信任的使用者簽署 commits.signed_by_untrusted_user_unmatched=由不受信任且與提交者不相符的使用者簽署 commits.gpg_key_id=GPG 金鑰 ID commits.ssh_key_fingerprint=SSH 金鑰指紋 +commits.view_path=檢視此歷史時刻 commit.operations=操作 commit.revert=還原 @@ -1155,6 +1401,7 @@ commitstatus.success=成功 ext_issues=存取外部問題 ext_issues.desc=連結到外部問題追蹤器。 +projects.desc=在專案看板中管理問題與合併請求。 projects.description=描述 (選用) projects.description_placeholder=描述 projects.create=建立專案 @@ -1182,6 +1429,7 @@ projects.column.new=新增欄位 projects.column.set_default=設為預設 projects.column.set_default_desc=將此欄位設定為未分類問題及合併請求的預設預設值 projects.column.delete=刪除欄位 +projects.column.deletion_desc=刪除專案欄位會將所有相關的問題移動到「未分類」,是否繼續? projects.column.color=顏色 projects.open=開啟 projects.close=關閉 @@ -1213,6 +1461,10 @@ issues.new.clear_milestone=清除已選取里程碑 issues.new.assignees=負責人 issues.new.clear_assignees=清除負責人 issues.new.no_assignees=沒有負責人 +issues.new.no_reviewers=沒有審核者 +issues.new.blocked_user=無法建立問題,因為您被儲存庫擁有者封鎖。 +issues.edit.already_changed=無法儲存問題的變更。看起來內容已被其他使用者更改。請重新整理頁面並再次嘗試編輯以避免覆蓋他們的變更。 +issues.edit.blocked_user=無法編輯內容,因為您被發文者或儲存庫擁有者封鎖。 issues.choose.get_started=開始 issues.choose.open_external_link=開啟 issues.choose.blank=預設 @@ -1238,6 +1490,7 @@ issues.remove_labels=移除了 %s 標籤 %s issues.add_remove_labels=加入了 %s 並移除了 %s 標籤 %s issues.add_milestone_at=`新增到 %s 里程碑 %s` issues.add_project_at=`將此加入到 %s 專案 %s` +issues.move_to_column_of_project=`將此移動到 %s 的 %s 中 %s` issues.change_milestone_at=`%[3]s 修改了里程碑 %[1]s%[2]s` issues.change_project_at=`將專案從 %s 修改為 %s %s` issues.remove_milestone_at=`從 %s 里程碑移除 %s` @@ -1258,6 +1511,10 @@ issues.filter_label_exclude=`使用 alt + click/enter issues.filter_label_no_select=所有標籤 issues.filter_label_select_no_label=沒有標籤 issues.filter_milestone=里程碑 +issues.filter_milestone_all=所有里程碑 +issues.filter_milestone_none=無里程碑 +issues.filter_milestone_open=開放中的里程碑 +issues.filter_milestone_closed=已關閉的里程碑 issues.filter_project=專案 issues.filter_project_all=所有專案 issues.filter_project_none=未選擇專案 @@ -1265,6 +1522,8 @@ issues.filter_assignee=負責人 issues.filter_assginee_no_select=所有負責人 issues.filter_assginee_no_assignee=沒有負責人 issues.filter_poster=作者 +issues.filter_user_placeholder=搜尋使用者 +issues.filter_user_no_select=所有使用者 issues.filter_type=類型 issues.filter_type.all_issues=所有問題 issues.filter_type.assigned_to_you=指派給您的 @@ -1305,6 +1564,7 @@ issues.next=下一頁 issues.open_title=開放中 issues.closed_title=已關閉 issues.draft_title=草稿 +issues.num_comments_1=%d 則評論 issues.num_comments=%d 則留言 issues.commented_at=`已留言 %s` issues.delete_comment_confirm=您確定要刪除這則留言嗎? @@ -1313,9 +1573,15 @@ issues.context.quote_reply=引用回覆 issues.context.reference_issue=新增問題並參考 issues.context.edit=編輯 issues.context.delete=刪除 +issues.no_content=沒有提供描述。 issues.close=關閉問題 +issues.comment_pull_merged_at=合併提交 %[1]s 到 %[2]s %[3]s +issues.comment_manually_pull_merged_at=手動合併提交 %[1]s 到 %[2]s %[3]s +issues.close_comment_issue=留言並關閉 issues.reopen_issue=重新開放 +issues.reopen_comment_issue=留言並重新開放 issues.create_comment=留言 +issues.comment.blocked_user=無法建立或編輯留言,因為您被發文者或儲存庫擁有者封鎖。 issues.closed_at=`關閉了這個問題 %[2]s` issues.reopened_at=`重新開放了這個問題 %[2]s` issues.commit_ref_at=`在提交中關聯了這個問題 %[2]s` @@ -1327,8 +1593,17 @@ issues.ref_closed_from=`關閉了這個問題 %[4]s 重新開放了這個問題 %[4]s %[2]s` issues.ref_from=`自 %[1]s` issues.author=作者 +issues.author_helper=此使用者是作者。 issues.role.owner=擁有者 +issues.role.owner_helper=此使用者是此儲存庫的擁有者。 issues.role.member=普通成員 +issues.role.member_helper=此使用者是擁有此儲存庫的組織成員。 +issues.role.collaborator=協作者 +issues.role.collaborator_helper=此使用者已被邀請協作此儲存庫。 +issues.role.first_time_contributor=首次貢獻者 +issues.role.first_time_contributor_helper=此使用者是首次對此儲存庫進行貢獻。 +issues.role.contributor=貢獻者 +issues.role.contributor_helper=此使用者之前已提交過此儲存庫。 issues.re_request_review=再次請求審核 issues.is_stale=經過此審核以後,此合併請求有被修改 issues.remove_request_review=移除審核請求 @@ -1343,6 +1618,9 @@ issues.label_title=名稱 issues.label_description=描述 issues.label_color=顏色 issues.label_exclusive=互斥 +issues.label_archive=封存標籤 +issues.label_archived_filter=顯示封存標籤 +issues.label_archive_tooltip=封存標籤在搜尋標籤時預設會被排除在建議之外。 issues.label_exclusive_desc=請以此格式命名標籤: scope/item,使它和其他 scope/ (相同範圍) 標籤互斥。 issues.label_exclusive_warning=在編輯問題及合併請求的標籤時,將會刪除任何有相同範圍的標籤。 issues.label_count=%d 個標籤 @@ -1390,11 +1668,25 @@ issues.delete.title=刪除此問題? issues.delete.text=您真的要刪除此問題嗎?(這將會永久移除所有內容。若您還想保留,請考慮改為關閉它。) issues.tracker=時間追蹤 +issues.timetracker_timer_start=開始計時 +issues.timetracker_timer_stop=停止計時 +issues.timetracker_timer_discard=捨棄計時 +issues.timetracker_timer_manually_add=手動新增時間 +issues.time_estimate_set=設定預估時間 +issues.time_estimate_display=預估時間:%s +issues.change_time_estimate_at=將預估時間更改為 %s %s +issues.remove_time_estimate_at=移除預估時間 %s +issues.time_estimate_invalid=預估時間格式無效 +issues.start_tracking_history=`開始工作 %s` issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器 issues.tracking_already_started=`您已在另一個問題上開始時間追蹤!` +issues.stop_tracking_history=`結束工作 %s` +issues.cancel_tracking_history=`取消時間追蹤 %s` issues.del_time=刪除此時間記錄 +issues.add_time_history=`加入了花費時間 %s` issues.del_time_history=`刪除了花費時間 %s` +issues.add_time_manually=手動新增時間 issues.add_time_hours=小時 issues.add_time_minutes=分鐘 issues.add_time_sum_to_small=沒有輸入時間。 @@ -1413,6 +1705,7 @@ issues.due_date_form=yyyy年mm月dd日 issues.due_date_form_add=新增截止日期 issues.due_date_form_edit=編輯 issues.due_date_form_remove=移除 +issues.due_date_not_writer=您需要對此儲存庫的寫入權限才能更新問題的截止日期。 issues.due_date_not_set=未設定截止日期。 issues.due_date_added=新增了截止日期 %s %s issues.due_date_modified=將截止日期從 %[2]s 修改為 %[1]s %[3]s @@ -1436,6 +1729,7 @@ issues.dependency.issue_closing_blockedby=此問題被下列問題阻擋而無 issues.dependency.issue_close_blocks=因為此問題的阻擋,下列問題無法被關閉 issues.dependency.pr_close_blocks=因為此合併請求的阻擋,下列問題無法被關閉 issues.dependency.issue_close_blocked=在您關閉此問題以前,您必須先關閉所有阻擋它的問題。 +issues.dependency.issue_batch_close_blocked=無法批次關閉您選擇的問題,因為問題 #%d 還有開放中的先決條件。 issues.dependency.pr_close_blocked=在您合併以前,您必須先關閉所有阻擋它的問題。 issues.dependency.blocks_short=阻擋 issues.dependency.blocked_by_short=先決於 @@ -1452,6 +1746,7 @@ issues.dependency.add_error_dep_not_same_repo=這兩個問題必須在同一個 issues.review.self.approval=您不能核可自己的合併請求。 issues.review.self.rejection=您不能對自己的合併請求提出請求變更。 issues.review.approve=核可了這些變更 %s +issues.review.comment=已審核 %s issues.review.dismissed=取消 %s 的審核 %s issues.review.dismissed_label=已取消 issues.review.left_comment=留下了回應 @@ -1462,9 +1757,13 @@ issues.review.add_review_request=請求了 %s 來審核 %s issues.review.remove_review_request=移除了對 %s 的審核請求 %s issues.review.remove_review_request_self=拒絕了審核 %s issues.review.pending=待處理 +issues.review.pending.tooltip=目前其他使用者還不能看見此留言。要送出您待定的留言請在頁面最上方選擇「%s」->「%s/%s/%s」。 issues.review.review=審核 issues.review.reviewers=審核者 issues.review.outdated=過時的 +issues.review.outdated_description=此留言發表後內容已變更 +issues.review.option.show_outdated_comments=顯示過時的留言 +issues.review.option.hide_outdated_comments=隱藏過時的留言 issues.review.show_outdated=顯示過時的 issues.review.hide_outdated=隱藏過時的 issues.review.show_resolved=顯示已解決 @@ -1473,6 +1772,11 @@ issues.review.resolve_conversation=解決對話 issues.review.un_resolve_conversation=取消解決對話 issues.review.resolved_by=標記了此對話為已解決 issues.review.commented=留言 +issues.review.official=核准 +issues.review.requested=審核待處理 +issues.review.rejected=請求變更 +issues.review.stale=核准後已更新 +issues.review.unofficial=未計入的核准 issues.assignee.error=因為未預期的錯誤,未能成功加入所有負責人。 issues.reference_issue.body=內容 issues.content_history.deleted=刪除 @@ -1488,6 +1792,9 @@ compare.compare_head=比較 pulls.desc=啟用合併請求和程式碼審核。 pulls.new=建立合併請求 +pulls.new.blocked_user=無法建立合併請求,因為您被儲存庫擁有者封鎖。 +pulls.new.must_collaborator=您必須是協作者才能建立合併請求。 +pulls.edit.already_changed=無法儲存合併請求的變更。看起來內容已被其他使用者更改。請重新整理頁面並再次嘗試編輯以避免覆蓋他們的變更。 pulls.view=檢視合併請求 pulls.compare_changes=建立合併請求 pulls.allow_edits_from_maintainers=允許維護者編輯 @@ -1504,20 +1811,31 @@ pulls.compare_compare=拉取自 pulls.switch_comparison_type=切換比較類型 pulls.switch_head_and_base=切換 head 和 base pulls.filter_branch=過濾分支 +pulls.show_all_commits=顯示所有提交 +pulls.show_changes_since_your_last_review=顯示自上次審核以來的變更 +pulls.showing_only_single_commit=僅顯示提交 %[1]s 的變更 +pulls.showing_specified_commit_range=僅顯示介於 %[1]s 和 %[2]s 之間的變更 +pulls.select_commit_hold_shift_for_range=選擇提交。按住 Shift 並點擊以選擇範圍 +pulls.review_only_possible_for_full_diff=僅在查看完整差異時才能進行審核 +pulls.filter_changes_by_commit=按提交篩選變更 pulls.nothing_to_compare=這些分支的內容相同,無需建立合併請求。 +pulls.nothing_to_compare_have_tag=所選的分支/標籤相同。 pulls.nothing_to_compare_and_allow_empty_pr=這些分支的內容相同,此合併請求將會是空白的。 pulls.has_pull_request=`已有介於這些分支間的合併請求:%[2]s#%[3]d` pulls.create=建立合併請求 -pulls.title_desc=請求將 %[1]d 次程式碼提交從 %[2]s 合併至 %[3]s +pulls.title_desc=請求將 %[1]d 次提交從 %[2]s 合併至 %[3]s pulls.merged_title_desc=將 %[1]d 次提交從 %[2]s 合併至 %[3]s %[4]s pulls.change_target_branch_at=`將目標分支從 %s 更改為 %s %s` pulls.tab_conversation=對話內容 -pulls.tab_commits=程式碼提交 +pulls.tab_commits=提交 pulls.tab_files=檔案變動 pulls.reopen_to_merge=請重新開放此合併請求以進行合併作業。 pulls.cant_reopen_deleted_branch=無法重新開放此合併請求,因為該分支已刪除。 pulls.merged=已合併 +pulls.merged_success=合併請求已成功合併並關閉 +pulls.closed=關閉合併請求 pulls.manually_merged=手動合併 +pulls.merged_info_text=現在可以刪除分支 %s。 pulls.is_closed=合併請求已被關閉。 pulls.title_wip_desc=`標題用 %s 開頭以避免意外地合併此合併請求。` pulls.cannot_merge_work_in_progress=此合併請求被標記為還在進行中 (WIP)。 @@ -1532,6 +1850,13 @@ pulls.is_empty=在這個分支上的更動都已經套用在目標分支上。 pulls.required_status_check_failed=未通過某些必要的檢查。 pulls.required_status_check_missing=遺失某些必要的檢查。 pulls.required_status_check_administrator=身為系統管理員,您依然可以進行合併。 +pulls.blocked_by_approvals=此合併請求尚未獲得足夠的核可。已獲得 %d 個核可中的 %d 個。 +pulls.blocked_by_approvals_whitelisted=此合併請求尚未獲得足夠的核可。已獲得允許名單中的 %d 個核可中的 %d 個。 +pulls.blocked_by_rejection=此合併請求有官方審核者請求變更。 +pulls.blocked_by_official_review_requests=此合併請求有官方審核請求。 +pulls.blocked_by_outdated_branch=此合併請求被阻擋,因為它已過時。 +pulls.blocked_by_changed_protected_files_1=此合併請求被阻擋,因為它更改了受保護的檔案: +pulls.blocked_by_changed_protected_files_n=此合併請求被阻擋,因為它更改了受保護的檔案: pulls.can_auto_merge_desc=這個合併請求可以自動合併。 pulls.cannot_auto_merge_desc=此合併請求無法自動合併,因為有衝突。 pulls.cannot_auto_merge_helper=手動合併以解決此衝突。 @@ -1566,7 +1891,10 @@ pulls.rebase_conflict_summary=錯誤訊息 pulls.unrelated_histories=合併失敗:要合併的 HEAD 和基底分支沒有共同的歷史。 提示:請嘗試不同的策略 pulls.merge_out_of_date=合併失敗:產生合併時,基底已被更新。提示:再試一次。 pulls.head_out_of_date=合併失敗:產生合併時,head 已被更新。提示:再試一次。 +pulls.has_merged=失敗:此合併請求已被合併,您不能再次合併或更改目標分支。 +pulls.push_rejected=合併失敗:此推送被拒絕。請檢查此儲存庫的 Git Hook。 pulls.push_rejected_summary=完整的拒絕訊息 +pulls.push_rejected_no_message=合併失敗:此推送被拒絕但未提供其他資訊。
請檢查此儲存庫的 Git Hook。 pulls.open_unmerged_pull_exists=`您不能重新開放,因為目前有相同的合併請求 (#%d) 正在進行中。` pulls.status_checking=還在進行一些檢查 pulls.status_checks_success=已通過所有檢查 @@ -1575,6 +1903,8 @@ pulls.status_checks_failure=一些檢查失敗了 pulls.status_checks_error=一些檢查回報了錯誤 pulls.status_checks_requested=必要 pulls.status_checks_details=詳情 +pulls.status_checks_hide_all=隱藏所有檢查 +pulls.status_checks_show_all=顯示所有檢查 pulls.update_branch=以合併更新分支 pulls.update_branch_rebase=以 Rebase 更新分支 pulls.update_branch_success=分支更新成功 @@ -1583,6 +1913,12 @@ pulls.outdated_with_base_branch=相對於基底分支,此分支已過時 pulls.close=關閉合併請求 pulls.closed_at=`關閉了這個合併請求 %[2]s` pulls.reopened_at=`重新開放了這個合併請求 %[2]s` +pulls.cmd_instruction_hint=`檢視 命令列指示。` +pulls.cmd_instruction_checkout_title=檢出 +pulls.cmd_instruction_checkout_desc=從您的專案儲存庫中,檢出一個新分支並測試變更。 +pulls.cmd_instruction_merge_title=合併 +pulls.cmd_instruction_merge_desc=合併變更並在 Gitea 上更新。 +pulls.cmd_instruction_merge_warning=警告:此操作無法合併合併請求,因為未啟用「自動檢測手動合併」 pulls.clear_merge_message=清除合併訊息 pulls.clear_merge_message_hint=清除合併訊息將僅移除提交訊息內容,留下產生的 git 結尾,如「Co-Authored-By …」。 @@ -1601,8 +1937,16 @@ pulls.auto_merge_canceled_schedule_comment=`取消了在通過所有檢查後自 pulls.delete.title=刪除此合併請求? pulls.delete.text=您真的要刪除此合併請求嗎?(這將會永久移除所有內容。若您還想保留,請考慮改為關閉它。) +pulls.recently_pushed_new_branches=您在分支 %[1]s 上推送了 %[2]s +pulls.upstream_diverging_prompt_behind_1=此分支落後 %s %d 次提交 +pulls.upstream_diverging_prompt_behind_n=此分支落後 %s %d 次提交 +pulls.upstream_diverging_prompt_base_newer=基底分支 %s 有新變更 +pulls.upstream_diverging_merge=同步 fork +pull.deleted_branch=(已刪除): %s +pull.agit_documentation=查看 AGit 的文件 +comments.edit.already_changed=無法儲存留言的變更。看起來內容已被其他使用者更改。請重新整理頁面並再次嘗試編輯以避免覆蓋他們的變更 milestones.new=新增里程碑 milestones.closed=於 %s關閉 @@ -1610,6 +1954,8 @@ milestones.update_ago=已更新 %s milestones.no_due_date=暫無截止日期 milestones.open=開啟 milestones.close=關閉 +milestones.new_subheader=里程碑可用來組織問題和追蹤進度。 +milestones.completeness=%d%% 完成 milestones.create=建立里程碑 milestones.title=標題 milestones.desc=描述 @@ -1626,11 +1972,26 @@ milestones.deletion=刪除里程碑 milestones.deletion_desc=刪除里程碑會從所有相關的問題移除它。是否繼續? milestones.deletion_success=里程碑已刪除 milestones.filter_sort.name=名稱 +milestones.filter_sort.earliest_due_data=截止日期由遠到近 +milestones.filter_sort.latest_due_date=截止日期由近到遠 milestones.filter_sort.least_complete=完成度由低到高 milestones.filter_sort.most_complete=完成度由高到低 milestones.filter_sort.most_issues=問題由多到少 milestones.filter_sort.least_issues=問題由少到多 +signing.will_sign=此提交將使用金鑰「%s」簽署。 +signing.wont_sign.error=檢查提交是否可以簽署時發生錯誤。 +signing.wont_sign.nokey=沒有可用的金鑰來簽署此提交。 +signing.wont_sign.never=提交從不簽署。 +signing.wont_sign.always=提交總是簽署。 +signing.wont_sign.pubkey=提交不會被簽署,因為您的帳戶沒有關聯的公鑰。 +signing.wont_sign.twofa=您必須啟用雙因素驗證才能簽署提交。 +signing.wont_sign.parentsigned=提交不會被簽署,因為父提交未簽署。 +signing.wont_sign.basesigned=合併不會被簽署,因為基底提交未簽署。 +signing.wont_sign.headsigned=合併不會被簽署,因為 head 提交未簽署。 +signing.wont_sign.commitssigned=合併不會被簽署,因為所有相關的提交都未簽署。 +signing.wont_sign.approved=合併不會被簽署,因為 PR 未被核准。 +signing.wont_sign.not_signed_in=你還沒有登入。 ext_wiki=存取外部 Wiki ext_wiki.desc=連結外部 Wiki。 @@ -1660,8 +2021,13 @@ wiki.reserved_page=「%s」是保留的 Wiki 頁面名稱。 wiki.pages=所有頁面 wiki.last_updated=最後更新於 %s wiki.page_name_desc=輸入此 Wiki 頁面的名稱。一些特殊名稱有:「Home」、「_Sidebar」、「_Footer」等。 +wiki.original_git_entry_tooltip=檢視原始 Git 檔案而不是使用友善連結。 activity=動態 +activity.navbar.pulse=脈搏 +activity.navbar.code_frequency=程式碼頻率 +activity.navbar.contributors=貢獻者 +activity.navbar.recent_commits=最近提交 activity.period.filter_label=期間: activity.period.daily=1 天 activity.period.halfweekly=3 天 @@ -1727,7 +2093,10 @@ activity.git_stats_and_deletions=和 activity.git_stats_deletion_1=刪除 %d 行 activity.git_stats_deletion_n=刪除 %d 行 +contributors.contribution_type.filter_label=貢獻類型: contributors.contribution_type.commits=提交歷史 +contributors.contribution_type.additions=新增 +contributors.contribution_type.deletions=刪除 settings=設定 settings.desc=設定是您可以管理儲存庫設定的地方 @@ -1742,7 +2111,20 @@ settings.hooks=Webhook settings.githooks=Git Hook settings.basic_settings=基本設定 settings.mirror_settings=鏡像設定 +settings.mirror_settings.docs=設定您的儲存庫自動同步其他儲存庫的提交、標籤、分支。 +settings.mirror_settings.docs.disabled_pull_mirror.instructions=設定您的專案自動將提交、標籤、分支推送到其他儲存庫。您的網站管理員已停用了拉取鏡像。 +settings.mirror_settings.docs.disabled_push_mirror.instructions=設定您的專案自動從其他儲存庫拉取提交、標籤、分支。 +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=現在這個功能只能從「遷移外部儲存庫」進行設定,詳情請參考: +settings.mirror_settings.docs.disabled_push_mirror.info=您的網站管理員已停用了推送鏡像。 +settings.mirror_settings.docs.no_new_mirrors=您的儲存庫正在鏡像變更到或從另一個儲存庫。請注意,您目前無法建立任何新的鏡像。 +settings.mirror_settings.docs.can_still_use=雖然您無法修改現有的鏡像或建立新的鏡像,但您仍然可以使用現有的鏡像。 +settings.mirror_settings.docs.pull_mirror_instructions=設定拉取鏡像請參考: +settings.mirror_settings.docs.more_information_if_disabled=您可以在這裡找到有關推送和拉取鏡像的更多資訊: +settings.mirror_settings.docs.doc_link_title=如何鏡像儲存庫? +settings.mirror_settings.docs.doc_link_pull_section=文件中的「從遠端儲存庫拉取」部分。 +settings.mirror_settings.docs.pulling_remote_title=正在從遠端儲存庫拉取 settings.mirror_settings.mirrored_repository=已鏡像的儲存庫 +settings.mirror_settings.pushed_repository=推送的儲存庫 settings.mirror_settings.direction=方向 settings.mirror_settings.direction.pull=拉取 settings.mirror_settings.direction.push=推送 @@ -1750,15 +2132,23 @@ settings.mirror_settings.last_update=最近更新時間 settings.mirror_settings.push_mirror.none=未設定推送鏡像 settings.mirror_settings.push_mirror.remote_url=Git 遠端儲存庫 URL settings.mirror_settings.push_mirror.add=新增推送鏡像 +settings.mirror_settings.push_mirror.edit_sync_time=編輯鏡像同步間隔 settings.sync_mirror=立即同步 +settings.pull_mirror_sync_in_progress=目前正在從遠端 %s 拉取變更。 +settings.push_mirror_sync_in_progress=目前正在推送變更到遠端 %s。 settings.site=網站 settings.update_settings=更新設定 +settings.update_mirror_settings=更新鏡像設定 +settings.branches.switch_default_branch=切換預設分支 settings.branches.update_default_branch=更新預設分支 settings.branches.add_new_rule=加入新規則 settings.advanced_settings=進階設定 settings.wiki_desc=啟用儲存庫 Wiki settings.use_internal_wiki=使用內建 Wiki +settings.default_wiki_branch_name=預設 Wiki 分支名稱 +settings.default_wiki_everyone_access=登入使用者的預設存取權限: +settings.failed_to_change_default_wiki_branch=更改預設 Wiki 分支失敗。 settings.use_external_wiki=使用外部 Wiki settings.external_wiki_url=外部 Wiki 連結 settings.external_wiki_url_error=外部 Wiki 網址不是有效的網址。 @@ -1788,6 +2178,10 @@ settings.pulls.default_delete_branch_after_merge=預設在合併後刪除合併 settings.pulls.default_allow_edits_from_maintainers=預設允許維護者進行編輯 settings.releases_desc=啟用儲存庫版本發佈 settings.packages_desc=啟用儲存庫套件註冊中心 +settings.projects_desc=啟用儲存庫專案 +settings.projects_mode_desc=專案模式 (顯示哪種類型的專案) +settings.projects_mode_repo=僅儲存庫專案 +settings.projects_mode_owner=僅使用者或組織專案 settings.projects_mode_all=所有專案 settings.actions_desc=啟用儲存庫 Actions settings.admin_settings=管理員設定 @@ -1814,14 +2208,17 @@ settings.convert_fork_succeed=此 fork 已轉換成普通儲存庫。 settings.transfer=轉移儲存庫所有權 settings.transfer.rejected=儲存庫轉移被拒絕。 settings.transfer.success=儲存庫已成功轉移。 +settings.transfer.blocked_user=無法轉移儲存庫,因為您被新擁有者封鎖。 settings.transfer_abort=取消轉移 settings.transfer_abort_invalid=您無法取消不存在的儲存庫轉移。 +settings.transfer_abort_success=成功取消轉移儲存庫至 %s。 settings.transfer_desc=將此儲存庫轉移給其他使用者或受您管理的組織。 settings.transfer_form_title=輸入儲存庫名稱以確認: settings.transfer_in_progress=目前正在進行轉移。如果您想要將此儲存庫轉移給其他使用者,請取消他。 settings.transfer_notices_1=- 如果將此儲存庫轉移給個別使用者,您將會失去此儲存庫的存取權。 settings.transfer_notices_2=- 如果將此儲存庫轉移到您(共同)擁有的組織,您將能繼續保有此儲存庫的存取權。 settings.transfer_notices_3=- 如果此儲存庫為私有儲存庫且將轉移給個別使用者,此動作確保該使用者至少擁有讀取權限 (必要時將會修改權限)。 +settings.transfer_notices_4=- 如果此儲存庫屬於組織,並且您將其轉移給另一個組織或個人,您將失去儲存庫問題與組織專案板之間的連結。 settings.transfer_owner=新擁有者 settings.transfer_perform=進行轉移 settings.transfer_started=此儲存庫已被標記為待轉移且正在等待「%s」的確認 @@ -1851,12 +2248,14 @@ settings.delete_notices_2=- 此操作將永久刪除 %s 儲存 settings.delete_notices_fork_1=- 在此儲存庫刪除後,它的 fork 將會變成獨立儲存庫。 settings.deletion_success=這個儲存庫已被刪除。 settings.update_settings_success=已更新儲存庫的設定。 +settings.update_settings_no_unit=儲存庫應允許至少某種形式的互動。 settings.confirm_delete=刪除儲存庫 settings.add_collaborator=增加協作者 settings.add_collaborator_success=成功增加協作者! settings.add_collaborator_inactive_user=無法將未啟用的使用者加入為協作者。 settings.add_collaborator_owner=無法將擁有者加入為協作者。 settings.add_collaborator_duplicate=此協作者早已被加入此儲存庫。 +settings.add_collaborator.blocked_user=協作者被儲存庫擁有者封鎖或反之亦然。 settings.delete_collaborator=移除 settings.collaborator_deletion=移除協作者 settings.collaborator_deletion_desc=移除協作者將拒絕他存取此儲存庫。是否繼續? @@ -1879,12 +2278,14 @@ settings.webhook_deletion_desc=移除 Webhook 將刪除它的設定及傳送記 settings.webhook_deletion_success=Webhook 已移除。 settings.webhook.test_delivery=傳送測試資料 settings.webhook.test_delivery_desc=使用假事件測試此 Webhook。 +settings.webhook.test_delivery_desc_disabled=要使用假事件測試此 Webhook,請啟用它。 settings.webhook.request=請求 settings.webhook.response=回應 settings.webhook.headers=標頭 settings.webhook.payload=內容 settings.webhook.body=本體 settings.webhook.replay.description=再次執行此 Webhook。 +settings.webhook.replay.description_disabled=要重新執行此 Webhook,請啟用它。 settings.webhook.delivery.success=已將事件加入到傳送佇列,可能需要等待幾分鐘才會出現於傳送紀錄。 settings.githooks_desc=Git Hook 是 Git 本身提供的功能。您可以在下方編輯 hook 檔案以設定自訂作業。 settings.githook_edit_desc=如果 Hook 未啟動,則會顯示範例文件中的內容。如果想要刪除某個 Hook,則送出空白內容即可。 @@ -1894,8 +2295,8 @@ settings.update_githook=更新 Hook settings.add_webhook_desc=Gitea 會發送含有指定 Content Type 的 POST 請求到目標 URL。 在 Webhook 指南閱讀更多內容。 settings.payload_url=目標 URL settings.http_method=HTTP 請求方法 -settings.content_type=POST Content Type -settings.secret=Secret +settings.content_type=POST 內容類型 +settings.secret=密鑰 settings.slack_username=服務名稱 settings.slack_icon_url=圖示 URL settings.slack_color=顏色 @@ -1917,6 +2318,7 @@ settings.event_wiki_desc=建立、重新命名、編輯、刪除 Wiki 頁面。 settings.event_release=版本發布 settings.event_release_desc=在儲存庫中發布、更新或刪除版本。 settings.event_push=推送 +settings.event_force_push=強制推送 settings.event_push_desc=推送到儲存庫。 settings.event_repository=儲存庫 settings.event_repository_desc=建立或刪除儲存庫。 @@ -1946,9 +2348,14 @@ settings.event_pull_request_review=合併請求審核 settings.event_pull_request_review_desc=核准、退回或提出審核留言。 settings.event_pull_request_sync=合併請求同步 settings.event_pull_request_sync_desc=合併請求同步。 +settings.event_pull_request_review_request=合併請求審核請求 +settings.event_pull_request_review_request_desc=合併請求審核請求或審核請求已移除。 +settings.event_pull_request_approvals=合併請求核可 +settings.event_pull_request_merge=合併請求合併 settings.event_package=套件 settings.event_package_desc=套件已在儲存庫中建立或刪除。 settings.branch_filter=分支篩選 +settings.branch_filter_desc=推送、建立分支、刪除分支事件的白名單,請使用 glob 比對模式。如果留白或輸入*,所有分支的事件都會被回報。語法參見 github.com/gobwas/glob。範例:master, {master,release*}。 settings.authorization_header=Authorization 標頭 settings.authorization_header_desc=存在時將將包含此 Authorization 標頭在請求中。例: %s。 settings.active=啟用 @@ -1994,26 +2401,72 @@ settings.deploy_key_deletion=刪除部署金鑰 settings.deploy_key_deletion_desc=移除部署金鑰將拒絕它存取此儲存庫。是否繼續? settings.deploy_key_deletion_success=部署金鑰已移除。 settings.branches=分支 +settings.protected_branch=分支保護 settings.protected_branch.save_rule=儲存規則 settings.protected_branch.delete_rule=刪除規則 +settings.protected_branch_can_push=允許推送? +settings.protected_branch_can_push_yes=你可以推送 +settings.protected_branch_can_push_no=你不能推送 +settings.branch_protection=%s 的分支保護 settings.protect_this_branch=啟用分支保護 settings.protect_this_branch_desc=防止刪除分支,並限制 Git 推送與合併到分支。 settings.protect_disable_push=停用推送 settings.protect_disable_push_desc=不允許推送到此分支。 +settings.protect_disable_force_push=停用強制推送 +settings.protect_disable_force_push_desc=不允許強制推送到此分支。 settings.protect_enable_push=啟用推送 settings.protect_enable_push_desc=任何擁有寫入權限的使用者將可推送至該分支(但不可使用force push)。 +settings.protect_enable_force_push_all=啟用強制推送 +settings.protect_enable_force_push_all_desc=任何有推送權限的人都可以強制推送到此分支。 +settings.protect_enable_force_push_allowlist=允許名單限制強制推送 +settings.protect_enable_force_push_allowlist_desc=只有推送權限的允許名單內的使用者或團隊可以強制推送到此分支。 settings.protect_enable_merge=啟用合併 settings.protect_enable_merge_desc=任何有寫入權限的人都可將合併請求合併到此分支 +settings.protect_whitelist_committers=使用白名單控管推送 +settings.protect_whitelist_committers_desc=僅允許白名單內的使用者或團隊推送至該分支(但不可使用force push)。 +settings.protect_whitelist_deploy_keys=將擁有寫入權限的部署金鑰加入白名單。 +settings.protect_whitelist_users=允許推送的使用者: +settings.protect_whitelist_teams=允許推送的團隊: +settings.protect_force_push_allowlist_users=允許強制推送的使用者: +settings.protect_force_push_allowlist_teams=允許強制推送的團隊: +settings.protect_force_push_allowlist_deploy_keys=允許強制推送的部署金鑰。 +settings.protect_merge_whitelist_committers=啟用合併白名單 +settings.protect_merge_whitelist_committers_desc=僅允許白名單內的使用者或團隊將合併請求合併至該分支。 +settings.protect_merge_whitelist_users=允許合併的使用者: +settings.protect_merge_whitelist_teams=允許合併的團隊: settings.protect_check_status_contexts=啟用狀態檢查 +settings.protect_status_check_patterns=狀態檢查模式: +settings.protect_status_check_patterns_desc=輸入模式以指定其他分支在合併到受此規則保護的分支前必須通過的狀態檢查。每行指定一個模式,模式不得為空白。 +settings.protect_check_status_contexts_desc=合併前必須先通過狀態檢查。選擇合併前必須通過的檢查。啟用時,必須先將提交推送到另一個分支,通過狀態檢查後再合併或直接推送到符合規則的分支。如果未選擇任何項目,最一個提交必將成功通過狀態檢查。 settings.protect_check_status_contexts_list=此儲存庫一週內曾進行過狀態檢查 +settings.protect_status_check_matched=符合 +settings.protect_invalid_status_check_pattern=狀態檢查模式無效: 「%s」。 +settings.protect_no_valid_status_check_patterns=沒有有效的狀態檢查模式。 settings.protect_required_approvals=需要的核可數量: +settings.protect_required_approvals_desc=只有在獲得足夠數量的核可後才能進行合併。 +settings.protect_approvals_whitelist_enabled=使用白名單控管審核人員與團隊 +settings.protect_approvals_whitelist_enabled_desc=只有白名單內的使用者與團隊會被計入需要的核可數量。未使用白名單時,將計算任何有寫入權限之人的核可。 +settings.protect_approvals_whitelist_users=審核者白名單: +settings.protect_approvals_whitelist_teams=審核團隊白名單: settings.dismiss_stale_approvals=捨棄過時的核可 settings.dismiss_stale_approvals_desc=當新的提交有修改到合併請求的內容,並被推送到此分支時,將捨棄舊的核可。 +settings.ignore_stale_approvals=忽略過時的核可 +settings.ignore_stale_approvals_desc=不計算在較舊提交上進行的核可(過時的審核)作為合併請求的核可數量。如果過時的審核已經被捨棄,則無關緊要。 settings.require_signed_commits=僅接受經簽署的提交 settings.require_signed_commits_desc=拒絕未經簽署或未經驗證的提交推送到此分支。 settings.protect_branch_name_pattern=受保護的分支名稱模式 +settings.protect_branch_name_pattern_desc=受保護的分支名稱模式。請參閱 文件 以了解模式語法。範例:main, release/** +settings.protect_patterns=模式 settings.protect_protected_file_patterns=受保護的檔案模式 (以分號區隔「;」): +settings.protect_protected_file_patterns_desc=即便使用者有權限新增、修改、刪除此分支的檔案,仍不允許直接修改受保護的檔案。可以用半形分號「;」分隔多個模式。請於 github.com/gobwas/glob 文件查看模式格式。範例: .drone.yml, /docs/**/*.txt。 settings.protect_unprotected_file_patterns=未受保護的檔案模式 (以分號區隔「;」): +settings.protect_unprotected_file_patterns_desc=當使用者有寫入權限時,可繞過推送限制,直接修改未受保護的檔案。可以用半形分號「;」分隔多個模式。請於 github.com/gobwas/glob 文件查看模式格式。範例: .drone.yml, /docs/**/*.txt。 +settings.add_protected_branch=啟用保護 +settings.delete_protected_branch=停用保護 +settings.update_protect_branch_success=已更新「%s」的分支保護規則。 +settings.remove_protected_branch_success=已刪除「%s」的分支保護規則。 +settings.remove_protected_branch_failed=刪除分支保護規則「%s」失敗。 +settings.protected_branch_deletion=停用分支保護 settings.protected_branch_deletion_desc=停用分支保護將允許有寫入權限的使用者推送至該分支,是否繼續? settings.block_rejected_reviews=有退回的審核時阻擋合併 settings.block_rejected_reviews_desc=如果官方審核人員提出變更請求,即使有足夠的核可也不允許進行合併。 @@ -2021,6 +2474,8 @@ settings.block_on_official_review_requests=有官方的審核請求時阻擋合 settings.block_on_official_review_requests_desc=如果有官方的審核請求時,即使有足夠的核可也不允許進行合併。 settings.block_outdated_branch=如果合併請求已經過時則阻擋合併 settings.block_outdated_branch_desc=當 head 分支落後於基礎分支時不得合併。 +settings.block_admin_merge_override=管理員必須遵守分支保護規則 +settings.block_admin_merge_override_desc=管理員必須遵守分支保護規則,不能繞過它。 settings.default_branch_desc=請選擇用來提交程式碼和合併請求的預設分支。 settings.merge_style_desc=合併方式 settings.default_merge_style_desc=預設合併方式 @@ -2039,18 +2494,39 @@ settings.tags.protection.allowed.teams=允許的團隊 settings.tags.protection.allowed.noone=無 settings.tags.protection.create=保護標籤 settings.tags.protection.none=沒有受保護的標籤。 -settings.bot_token=Bot Token -settings.chat_id=Chat ID -settings.matrix.homeserver_url=Homeserver 網址 +settings.tags.protection.pattern.description=您可以使用單一名稱或 glob 模式或正則表達式來匹配多個標籤。詳情請參閱 受保護標籤指南。 +settings.bot_token=機器人 Token +settings.chat_id=聊天 ID +settings.thread_id=線程 ID +settings.matrix.homeserver_url=主伺服器網址 settings.matrix.room_id=聊天室 ID settings.matrix.message_type=訊息類型 +settings.visibility.private.button=設為私人 +settings.visibility.private.text=將可見性更改為私人不僅會使儲存庫僅對允許的成員可見,還可能會移除它與 fork、關注者和星標之間的關係。 +settings.visibility.private.bullet_title=更改可見性為私人將: +settings.visibility.private.bullet_one=使儲存庫僅對允許的成員可見。 +settings.visibility.private.bullet_two=可能會移除它與 fork關注者星標 之間的關係。 +settings.visibility.public.button=設為公開 +settings.visibility.public.text=將可見性更改為公開將使儲存庫對任何人可見。 +settings.visibility.public.bullet_title=更改可見性為公開將: +settings.visibility.public.bullet_one=使儲存庫對任何人可見。 +settings.visibility.success=儲存庫可見性已更改。 +settings.visibility.error=嘗試更改儲存庫可見性時發生錯誤。 +settings.visibility.fork_error=無法更改 fork 儲存庫的可見性。 settings.archive.button=封存儲存庫 settings.archive.header=封存本儲存庫 +settings.archive.text=封存儲存庫將使其完全變為唯讀。它將從儀表板中隱藏。沒有人(甚至包括您!)將能夠進行新的提交,或打開任何問題或合併請求。 settings.archive.success=此儲存庫已被封存 settings.archive.error=嘗試封存儲存庫時發生錯誤。查看日誌檔以獲得更多資訊。 settings.archive.error_ismirror=無法封存鏡像儲存庫。 settings.archive.branchsettings_unavailable=已封存的儲存庫無法使用分支設定。 settings.archive.tagsettings_unavailable=已封存的儲存庫無法使用標籤設定。 +settings.archive.mirrors_unavailable=如果儲存庫已封存,則無法使用鏡像。 +settings.unarchive.button=取消封存儲存庫 +settings.unarchive.header=取消封存此儲存庫 +settings.unarchive.text=取消封存儲存庫將恢復其接收提交和推送的能力,以及新問題和合併請求。 +settings.unarchive.success=儲存庫已成功取消封存。 +settings.unarchive.error=嘗試取消封存儲存庫時發生錯誤。查看日誌檔以獲得更多資訊。 settings.update_avatar_success=已更新儲存庫的大頭貼。 settings.lfs=LFS settings.lfs_filelist=存放在本儲存庫的 LFS 檔案 @@ -2115,8 +2591,9 @@ diff.file_suppressed_line_too_long=檔案差異因為一行或多行太長而無 diff.too_many_files=本差異變更的檔案數量過多導致部分檔案未顯示 diff.show_more=顯示更多 diff.load=載入差異 -diff.generated=generated -diff.vendored=vendored +diff.generated=已產生 +diff.vendored=已供應 +diff.comment.add_line_comment=新增行評論 diff.comment.placeholder=留言... diff.comment.add_single_comment=加入單獨的留言 diff.comment.add_review_comment=新增留言 @@ -2147,6 +2624,7 @@ release.new_release=發布新版本 release.draft=草稿 release.prerelease=預發布版本 release.stable=穩定 +release.latest=最新 release.compare=比較 release.edit=編輯 release.ahead.commits=%d 次提交 @@ -2160,7 +2638,9 @@ release.target=目標分支 release.tag_helper=新增或選擇現有的標籤。 release.tag_helper_new=新標籤,將在目標上建立此標籤。 release.tag_helper_existing=現有的標籤。 +release.title=版本標題 release.title_empty=標題不可為空。 +release.message=描述此版本 release.prerelease_desc=標記為 Pre-Release release.prerelease_helper=標記此版本不適合生產使用。 release.cancel=取消 @@ -2170,6 +2650,7 @@ release.edit_release=更新發布 release.delete_release=刪除發布 release.delete_tag=刪除標籤 release.deletion=刪除發布 +release.deletion_desc=刪除版本發布只會將其從 Gitea 中移除。它不會影響 Git 標籤、儲存庫的內容或其歷史。是否繼續? release.deletion_success=已刪除此版本發布。 release.deletion_tag_desc=即將從儲存庫移除此標籤。儲存庫內容和歷史將保持不變,是否繼續? release.deletion_tag_success=已刪除此標籤。 @@ -2189,6 +2670,7 @@ branch.already_exists=已存在名為「%s」的分支。 branch.delete_head=刪除 branch.delete=刪除分支「%s」 branch.delete_html=刪除分支 +branch.delete_desc=刪除分支是永久的。雖然被刪除的分支可能會在實際移除前繼續存在一段時間,但在大多數情況下無法撤銷。是否繼續? branch.deletion_success=已刪除分支「%s」。 branch.deletion_failed=刪除分支「%s」失敗。 branch.delete_branch_has_new_commits=因為合併後已加入了新的提交,「%s」分支無法被刪除。 @@ -2197,6 +2679,7 @@ branch.create_from=從「%s」 branch.create_success=已建立分支「%s」。 branch.branch_already_exists=此儲存庫已有名為「%s」的分支。 branch.branch_name_conflict=分支名稱「%s」與現有分支「%s」衝突。 +branch.tag_collision=無法建立「%s」分支,因為此儲存庫中已有同名的標籤。 branch.deleted_by=由 %s 刪除 branch.restore_success=已還原分支「%s」。 branch.restore_failed=還原分支「%s」失敗。 @@ -2204,10 +2687,13 @@ branch.protected_deletion_failed=分支「%s」已被保護,不能刪除。 branch.default_deletion_failed=分支「%s」為預設分支,不能刪除。 branch.restore=還原分支「%s」 branch.download=下載分支「%s」 +branch.rename=重新命名分支「%s」 branch.included_desc=此分支是預設分支的一部分 branch.included=包含 branch.create_new_branch=從下列分支建立分支: branch.confirm_create_branch=建立分支 +branch.warning_rename_default_branch=您正在重新命名預設分支。 +branch.rename_branch_to=重新命名「%s」為: branch.confirm_rename_branch=重新命名分支 branch.create_branch_operation=建立分支 branch.new_branch=建立新分支 @@ -2223,6 +2709,8 @@ tag.create_success=已建立標籤「%s」。 topic.manage_topics=管理主題 topic.done=完成 +topic.count_prompt=您最多能選擇 25 個主題 +topic.format_prompt=主題必須以字母或數字開頭,可以包含破折號 ('-') 和點 ('.'),最多可以有 35 個字元。字母必須是小寫。 find_file.go_to_file=移至檔案 find_file.no_matching=找不到符合的檔案 @@ -2230,8 +2718,16 @@ find_file.no_matching=找不到符合的檔案 error.csv.too_large=無法渲染此檔案,因為它太大了。 error.csv.unexpected=無法渲染此檔案,因為它包含了未預期的字元,於第 %d 行第 %d 列。 error.csv.invalid_field_count=無法渲染此檔案,因為它第 %d 行的欄位數量有誤。 +error.broken_git_hook=此儲存庫的 Git hooks 似乎已損壞。請按照 文件 進行修復,然後推送一些提交以刷新狀態。 [graphs] +component_loading=正在載入 %s... +component_loading_failed=無法載入 %s +component_loading_info=這可能需要一點時間… +component_failed_to_load=發生意外錯誤。 +code_frequency.what=程式碼頻率 +contributors.what=貢獻 +recent_commits.what=最近提交 [org] org_name_holder=組織名稱 @@ -2257,11 +2753,13 @@ team_unit_desc=允許存取的儲存庫區域 team_unit_disabled=(已停用) form.name_reserved=「%s」是保留的組織名稱。 +form.name_pattern_not_allowed=組織名稱不可包含字元「%s」。 form.create_org_not_allowed=此帳號禁止建立組織。 settings=設定 settings.options=組織 settings.full_name=組織全名 +settings.email=聯絡電子郵件 settings.website=官方網站 settings.location=所在地區 settings.permission=權限 @@ -2275,6 +2773,7 @@ settings.visibility.private_shortname=私有 settings.update_settings=更新設定 settings.update_setting_success=組織設定已更新。 +settings.change_orgname_prompt=注意:更改組織名稱將同時更改組織的 URL 並釋放舊名稱。 settings.change_orgname_redirect_prompt=舊的名稱被領用前,會重新導向新名稱。 settings.update_avatar_success=已更新組織的大頭貼。 settings.delete=刪除組織 @@ -2342,6 +2841,7 @@ teams.add_nonexistent_repo=您嘗試新增的儲存庫不存在,請先建立 teams.add_duplicate_users=使用者已經是團隊成員了。 teams.repos.none=這個團隊沒有可以存取的儲存庫。 teams.members.none=這個團隊沒有任何成員。 +teams.members.blocked_user=無法新增使用者,因為它被組織封鎖。 teams.specific_repositories=指定儲存庫 teams.specific_repositories_helper=成員只能存取明確加入此團隊的儲存庫。選擇這個選項不會自動移除透過所有儲存庫加入的儲存庫。 teams.all_repositories=所有儲存庫 @@ -2349,15 +2849,21 @@ teams.all_repositories_helper=團隊擁有可存取所有儲存庫。選擇此 teams.all_repositories_read_permission_desc=這個團隊擁有所有儲存庫讀取 權限:成員可以查看和 Clone 儲存庫。 teams.all_repositories_write_permission_desc=這個團隊擁有所有儲存庫寫入 權限:成員可以讀取和推送到儲存庫。 teams.all_repositories_admin_permission_desc=這個團隊擁有所有儲存庫管理員 權限:成員可以讀取、推送和增加協作者到儲存庫。 +teams.invite.title=您已被邀請加入組織 %s 中的團隊 %s。 teams.invite.by=邀請人 %s teams.invite.description=請點擊下方按鈕加入團隊。 [admin] +maintenance=維護 dashboard=資訊主頁 +self_check=自我檢查 +identity_access=身份與存取 users=使用者帳戶 organizations=組織 +assets=程式碼資產 repositories=儲存庫 hooks=Webhook +integrations=整合 authentication=認證來源 emails=使用者電子信箱 config=組態 @@ -2368,8 +2874,11 @@ monitor=應用監控面版 first_page=首頁 last_page=末頁 total=總計:%d +settings=管理員設定 +dashboard.new_version_hint=現已推出 Gitea %s,您正在執行 %s。詳情請參閱部落格的說明。 dashboard.statistic=摘要 +dashboard.maintenance_operations=維護操作 dashboard.system_status=系統狀態 dashboard.operation_name=作業名稱 dashboard.operation_switch=開關 @@ -2378,11 +2887,13 @@ dashboard.clean_unbind_oauth=清理未綁定的 OAuth 連結 dashboard.clean_unbind_oauth_success=所有未綁定的 OAuth 連結已刪除。 dashboard.task.started=已開始的任務: %[1]s dashboard.task.process=任務: %[1]s +dashboard.task.cancelled=任務: %[1]s 已取消: %[3]s dashboard.task.error=任務中的錯誤: %[1]s: %[3]s dashboard.task.finished=任務: 已完成由 %[2]s 啟動的 %[1]s dashboard.task.unknown=未知的任務: %[1]s dashboard.cron.started=已開始的 Cron: %[1]s dashboard.cron.process=Cron: %[1]s +dashboard.cron.cancelled=Cron: %[1]s 已取消: %[3]s dashboard.cron.error=Cron 中的錯誤: %s: %[3]s dashboard.cron.finished=Cron: %[1]s 已完成 dashboard.delete_inactive_accounts=刪除所有未啟用帳戶 @@ -2392,6 +2903,8 @@ dashboard.delete_repo_archives.started=刪除所有儲存庫存檔的任務已 dashboard.delete_missing_repos=刪除所有遺失 Git 檔案的儲存庫 dashboard.delete_missing_repos.started=刪除所有遺失 Git 檔案的儲存庫的任務已啟動。 dashboard.delete_generated_repository_avatars=刪除產生的儲存庫大頭貼 +dashboard.sync_repo_branches=從 Git 資料同步遺漏的分支到資料庫 +dashboard.sync_repo_tags=從 Git 資料同步標籤到資料庫 dashboard.update_mirrors=更新鏡像 dashboard.repo_health_check=對所有儲存庫進行健康檢查 dashboard.check_repo_stats=檢查所有儲存庫的統計資料 @@ -2406,6 +2919,7 @@ dashboard.reinit_missing_repos=重新初始化所有記錄存在但遺失的 Git dashboard.sync_external_users=同步外部使用者資料 dashboard.cleanup_hook_task_table=清理 hook_task 資料表 dashboard.cleanup_packages=清理已過期的套件 +dashboard.cleanup_actions=清理過期的操作資源 dashboard.server_uptime=服務執行時間 dashboard.current_goroutine=目前的 Goroutines 數量 dashboard.current_memory_usage=目前記憶體使用量 @@ -2435,9 +2949,19 @@ dashboard.total_gc_time=總 GC 暫停時間 dashboard.total_gc_pause=總 GC 暫停時間 dashboard.last_gc_pause=上次 GC 暫停時間 dashboard.gc_times=GC 執行次數 +dashboard.delete_old_actions=從資料庫刪除所有舊行為 +dashboard.delete_old_actions.started=從資料庫刪除所有舊行為的任務已啟動。 dashboard.update_checker=更新檢查器 dashboard.delete_old_system_notices=從資料庫刪除所有舊系統提示 dashboard.gc_lfs=對 LFS meta objects 進行垃圾回收 +dashboard.stop_zombie_tasks=停止殭屍任務 +dashboard.stop_endless_tasks=停止永不停止的任務 +dashboard.cancel_abandoned_jobs=取消已放棄的工作 +dashboard.start_schedule_tasks=啟動動作排程任務 +dashboard.sync_branch.started=分支同步已開始 +dashboard.sync_tag.started=標籤同步已開始 +dashboard.rebuild_issue_indexer=重建問題索引器 +dashboard.sync_repo_licenses=同步儲存庫許可證 users.user_manage_panel=使用者帳戶管理 users.new_account=建立使用者帳戶 @@ -2446,6 +2970,9 @@ users.full_name=全名 users.activated=已啟用 users.admin=管理員 users.restricted=受限 +users.reserved=保留 +users.bot=機器人 (Bot) +users.remote=遠端 users.2fa=兩步驟驗證 users.repos=儲存庫數 users.created=建立時間 @@ -2492,6 +3019,7 @@ users.list_status_filter.is_prohibit_login=禁止登入 users.list_status_filter.not_prohibit_login=允許登入 users.list_status_filter.is_2fa_enabled=已啟用兩步驟驗證 users.list_status_filter.not_2fa_enabled=未啟用兩步驟驗證 +users.details=使用者詳細資訊 emails.email_manage_panel=使用者電子信箱管理 emails.primary=主要 @@ -2504,6 +3032,11 @@ emails.updated=信箱已更新 emails.not_updated=電子信箱更新失敗: %v emails.duplicate_active=此信箱已被其他使用者使用 emails.change_email_header=更新電子信箱屬性 +emails.change_email_text=您確定要更新此電子郵件地址嗎? +emails.delete=刪除電子郵件 +emails.delete_desc=您確定要刪除此電子郵件地址嗎? +emails.deletion_success=電子郵件地址已被刪除。 +emails.delete_primary_email_error=您不能刪除主要的電子郵件地址。 orgs.org_manage_panel=組織管理 orgs.name=名稱 @@ -2519,10 +3052,13 @@ repos.name=名稱 repos.private=私有 repos.issues=問題數 repos.size=大小 +repos.lfs_size=LFS 大小 packages.package_manage_panel=套件管理 packages.total_size=總大小: %s packages.unreferenced_size=未參考大小: %s +packages.cleanup=清理已逾期的資料 +packages.cleanup.success=已成功清理過期的資料 packages.owner=擁有者 packages.creator=建立者 packages.name=名稱 @@ -2533,10 +3069,12 @@ packages.size=大小 packages.published=已發布 defaulthooks=預設 Webhook +defaulthooks.desc=當某些 Gitea 事件觸發時,Webhook 會自動發出 HTTP POST 請求到伺服器。此處定義的 Webhook 是預設值,將會複製到所有新儲存庫中。詳情請參閱 webhooks 指南。 defaulthooks.add_webhook=新增預設 Webhook defaulthooks.update_webhook=更新預設 Webhook systemhooks=系統 Webhook +systemhooks.desc=當某些 Gitea 事件觸發時,Webhook 會自動發出 HTTP POST 請求到伺服器。此處定義的 Webhook 將作用於系統上的所有儲存庫,因此請考慮這可能對效能產生的影響。詳情請參閱 webhooks 指南。 systemhooks.add_webhook=新增系統 Webhook systemhooks.update_webhook=更新系統 Webhook @@ -2629,8 +3167,20 @@ auths.sspi_default_language=使用者預設語言 auths.sspi_default_language_helper=SSPI 認證方法自動建立之使用者的預設語言,留白以自動偵測。 auths.tips=幫助提示 auths.tips.oauth2.general=OAuth2 認證 +auths.tips.oauth2.general.tip=註冊新的 OAuth2 認證時,回調/重定向 URL 應為: auths.tip.oauth2_provider=OAuth2 提供者 +auths.tip.bitbucket=註冊新的 OAuth 客戶端並加入權限「Account - Read」。網址:https://bitbucket.org/account/user//oauth-consumers/new auths.tip.nextcloud=在您的執行個體中,於選單「設定 -> 安全性 -> OAuth 2.0 客戶端」註冊新的 OAuth 客戶端 +auths.tip.dropbox=建立新的 App。網址:https://www.dropbox.com/developers/apps +auths.tip.facebook=註冊新的應用程式並新增產品「Facebook 登入」。網址:https://developers.facebook.com/apps +auths.tip.github=註冊新的 OAuth 應用程式。網址:https://github.com/settings/applications/new +auths.tip.gitlab_new=註冊新的應用程式。網址:https://discordapp.com/developers/applications/me +auths.tip.google_plus=從 Google API 控制台取得 OAuth2 用戶端憑證。網址:https://console.developers.google.com/ +auths.tip.openid_connect=使用 OpenID 連接探索 URL (/.well-known/openid-configuration) 來指定節點 +auths.tip.twitter=建立應用程式並確保有啟用「Allow this application to be used to Sign in with Twitter」。網址:https://dev.twitter.com/apps +auths.tip.discord=註冊新的應用程式。網址:https://discordapp.com/developers/applications/me +auths.tip.gitea=註冊新的 OAuth2 應用程式。指南可在 %s 找到 +auths.tip.yandex=建立新的應用程式,從「Yandex.Passport API」區塊選擇「Access to email address」、「Access to user avatar」和「Access to username, first name and surname, gender」。網址:https://oauth.yandex.com/client/new auths.tip.mastodon=輸入您欲認證的 Mastodon 執行個體的自訂網址 (或使用預設值) auths.edit=修改認證來源 auths.activated=該認證來源已啟用 @@ -2659,6 +3209,7 @@ config.disable_router_log=關閉路由日誌 config.run_user=以使用者名稱執行 config.run_mode=執行模式 config.git_version=Git 版本 +config.app_data_path=應用程式資料路徑 config.repo_root_path=儲存庫目錄 config.lfs_root_path=LFS 根目錄 config.log_file_root_path=日誌路徑 @@ -2733,6 +3284,7 @@ config.mailer_sendmail_timeout=Sendmail 逾時 config.mailer_use_dummy=Dummy config.test_email_placeholder=電子信箱 (例:test@example.com) config.send_test_mail=傳送測試郵件 +config.send_test_mail_submit=傳送 config.test_mail_failed=傳送測試郵件到「%s」時失敗: %v config.test_mail_sent=測試郵件已傳送到「%s」。 @@ -2744,6 +3296,10 @@ config.cache_adapter=Cache 適配器 config.cache_interval=Cache 週期 config.cache_conn=Cache 連接字符串 config.cache_item_ttl=快取項目 TTL +config.cache_test=測試快取 +config.cache_test_failed=測試快取失敗: %v +config.cache_test_slow=快取測試成功,但回應速度慢: %s +config.cache_test_succeeded=快取測試成功,回應時間為 %s config.session_config=Session 組態 config.session_provider=Session 提供者 @@ -2758,6 +3314,7 @@ config.picture_config=圖片和大頭貼組態 config.picture_service=圖片服務 config.disable_gravatar=停用 Gravatar config.enable_federated_avatar=啟用 Federated Avatars +config.open_with_editor_app_help=「開啟方式」編輯器用於克隆選單。如果留空,將使用預設值。展開以查看預設值。 config.git_config=Git 組態 config.git_disable_diff_highlight=停用比較語法高亮 @@ -2772,12 +3329,15 @@ config.git_pull_timeout=Pull 作業逾時 config.git_gc_timeout=GC 作業逾時 config.log_config=日誌組態 +config.logger_name_fmt=記錄器: %s config.disabled_logger=已停用 config.access_log_mode=存取日誌模式 +config.access_log_template=存取日誌範本 config.xorm_log_sql=記錄 SQL config.set_setting_failed=寫入設定值 %s 失敗 +monitor.stats=統計 monitor.cron=Cron 任務 monitor.name=名稱 @@ -2786,11 +3346,16 @@ monitor.next=下次執行時間 monitor.previous=上次執行時間 monitor.execute_times=執行次數 monitor.process=執行中的處理程序 +monitor.stacktrace=堆疊追蹤 +monitor.processes_count=%d 個處理程序 +monitor.download_diagnosis_report=下載診斷報告 monitor.desc=描述 monitor.start=開始時間 monitor.execute_time=已執行時間 monitor.last_execution_result=結果 monitor.process.cancel=結束處理程序 +monitor.process.cancel_desc=結束處理程序可能造成資料遺失 +monitor.process.cancel_notices=結束: %s? monitor.process.children=子程序 monitor.queues=佇列 @@ -2799,14 +3364,19 @@ monitor.queue.name=名稱 monitor.queue.type=類型 monitor.queue.exemplar=型別 monitor.queue.numberworkers=工作者數量 +monitor.queue.activeworkers=活躍工作者 monitor.queue.maxnumberworkers=最大工作者數量 monitor.queue.numberinqueue=佇列中的數量 +monitor.queue.review_add=審查 / 增加工作者 monitor.queue.settings.title=集區設定 +monitor.queue.settings.desc=集區會根據工作者佇列的阻塞情況動態增長。 monitor.queue.settings.maxnumberworkers=最大工作者數量 monitor.queue.settings.maxnumberworkers.placeholder=目前 %[1]d monitor.queue.settings.maxnumberworkers.error=最大工作者數量必須是數字 monitor.queue.settings.submit=更新設定 monitor.queue.settings.changed=已更新設定 +monitor.queue.settings.remove_all_items=全部移除 +monitor.queue.settings.remove_all_items_done=佇列中的所有項目已被移除。 notices.system_notice_list=系統提示 notices.view_detail_header=查看提示細節 @@ -2823,6 +3393,14 @@ notices.desc=描述 notices.op=操作 notices.delete_success=已刪除系統提示。 +self_check.no_problem_found=尚未發現任何問題。 +self_check.startup_warnings=啟動警告: +self_check.database_collation_mismatch=預期資料庫使用排序規則:%s +self_check.database_collation_case_insensitive=資料庫正在使用排序規則 %s,這是一個不區分大小寫的排序規則。雖然 Gitea 可以正常運作,但在某些罕見情況下可能會出現預期外的問題。 +self_check.database_inconsistent_collation_columns=資料庫正在使用排序規則 %s,但這些欄位使用了不匹配的排序規則。這可能會導致一些預期外的問題。 +self_check.database_fix_mysql=對於 MySQL/MariaDB 使用者,您可以使用 "gitea doctor convert" 命令來修復排序規則問題,或者也可以手動使用 "ALTER ... COLLATE ..." SQL 語句來修復問題。 +self_check.database_fix_mssql=對於 MSSQL 使用者,目前您只能手動使用 "ALTER ... COLLATE ..." SQL 語句來修復問題。 +self_check.location_origin_mismatch=當前 URL (%[1]s) 與 Gitea 看到的 URL (%[2]s) 不匹配。如果您使用了反向代理,請確保 "Host" 和 "X-Forwarded-Proto" 標頭設置正確。 [action] create_repo=建立了儲存庫 %s @@ -2850,6 +3428,7 @@ mirror_sync_create=從鏡像同步了新參考 %[3]s%[3]s 刪除了參考 %[2]s approve_pull_request=`核可了 %[3]s#%[2]s` reject_pull_request=`提出了修改建議 %[3]s#%[2]s` +publish_release=`發布了 %[3]s "%[4]s" ` review_dismissed=`取消了 %[4]s%[3]s#%[2]s 的審核` review_dismissed_reason=原因: create_branch=在 %[4]s 中建立了分支 %[3]s @@ -2878,6 +3457,7 @@ raw_minutes=分鐘 [dropzone] default_message=拖放檔案或是點擊此處上傳。 +invalid_input_type=您無法上傳此類型的檔案 file_too_big=檔案大小({{filesize}} MB) 超過了最大允許大小({{maxFilesize}} MB) remove_file=移除文件 @@ -2915,8 +3495,10 @@ error.unit_not_allowed=您未被允許訪問此儲存庫區域 title=套件 desc=管理儲存庫套件。 empty=目前還沒有套件。 +no_metadata=沒有元數據。 empty.documentation=關於套件註冊中心的詳情請參閱說明文件。 empty.repo=已經上傳了一個套件,但是沒有顯示在這裡嗎?打開套件設定並將其連結到這個儲存庫。 +registry.documentation=有關 %s 註冊中心的更多資訊,請參閱說明文件。 filter.type=類型 filter.type.all=所有 filter.no_result=沒有篩選結果。 @@ -2948,6 +3530,8 @@ alpine.repository=儲存庫資訊 alpine.repository.branches=分支 alpine.repository.repositories=儲存庫 alpine.repository.architectures=架構 +arch.registry=在 /etc/pacman.conf 中新增伺服器及相關儲存庫和架構: +arch.install=使用 pacman 同步套件: arch.repository=儲存庫資訊 arch.repository.repositories=儲存庫 arch.repository.architectures=架構 @@ -2973,12 +3557,17 @@ container.layers=映像檔 Layers container.labels=標籤 container.labels.key=鍵 container.labels.value=值 +cran.registry=在您的 Rprofile.site 檔設定此註冊中心: cran.install=執行下列命令安裝此套件: debian.registry=透過下列命令設定此註冊中心: +debian.registry.info=從下列清單選擇$distribution和$component debian.install=執行下列命令安裝此套件: debian.repository=儲存庫資訊 +debian.repository.distributions=發行版 +debian.repository.components=元件 debian.repository.architectures=架構 generic.download=透過下列命令下載套件: +go.install=透過下列命令安裝套件: helm.registry=透過下列命令設定此註冊中心: helm.install=執行下列命令安裝此套件: maven.registry=在您專案的 pom.xml 檔設定此註冊中心: @@ -2993,6 +3582,7 @@ npm.install=執行下列命令以使用 npm 安裝此套件: npm.install2=或將它加到 package.json 檔: npm.dependencies=相依性 npm.dependencies.development=開發相依性 +npm.dependencies.bundle=捆綁相依性 npm.dependencies.peer=Peer 相依性 npm.dependencies.optional=選用相依性 npm.details.tag=標籤 @@ -3000,9 +3590,12 @@ pub.install=執行下列命令以使用 Dart 安裝此套件: pypi.requires=需要 Python pypi.install=執行下列命令以使用 pip 安裝此套件: rpm.registry=透過下列命令設定此註冊中心: +rpm.distros.redhat=在基於 RedHat 的發行版上 +rpm.distros.suse=在基於 SUSE 的發行版上 rpm.install=執行下列命令安裝此套件: rpm.repository=儲存庫資訊 rpm.repository.architectures=架構 +rpm.repository.multiple_groups=此套件在多個群組中可用。 rubygems.install=執行下列命令以使用 gem 安裝此套件: rubygems.install2=或將它加到 Gemfile: rubygems.dependencies.runtime=執行階段相依性 @@ -3026,14 +3619,17 @@ settings.delete.success=已刪除該套件。 settings.delete.error=刪除套件失敗。 owner.settings.cargo.title=Cargo Registry 索引 owner.settings.cargo.initialize=初始化索引 +owner.settings.cargo.initialize.description=使用 Cargo 註冊中心需要一個特殊的索引 Git 儲存庫。使用此選項將會 (重新) 建立儲存庫並自動配置它。 owner.settings.cargo.initialize.error=初始化 Cargo 索引失敗: %v owner.settings.cargo.initialize.success=成功建立了 Cargo 索引。 owner.settings.cargo.rebuild=重建索引 +owner.settings.cargo.rebuild.description=如果索引與儲存的 Cargo 套件不同步,重建索引可能會有幫助。 owner.settings.cargo.rebuild.error=重建 Cargo 索引失敗: %v owner.settings.cargo.rebuild.success=成功重建了 Cargo 索引。 owner.settings.cleanuprules.title=管理清理規則 owner.settings.cleanuprules.add=加入清理規則 owner.settings.cleanuprules.edit=編輯清理規則 +owner.settings.cleanuprules.none=沒有清理規則可用。請參閱說明文件。 owner.settings.cleanuprules.preview=清理規則預覽 owner.settings.cleanuprules.preview.overview=已排定要移除 %d 個套件。 owner.settings.cleanuprules.preview.none=清理規則不符合任何套件。 @@ -3052,6 +3648,7 @@ owner.settings.cleanuprules.success.update=已更新清理規則。 owner.settings.cleanuprules.success.delete=已刪除清理規則。 owner.settings.chef.title=Chef Registry owner.settings.chef.keypair=產生密鑰組 +owner.settings.chef.keypair.description=驗證 Chef 註冊中心需要一個密鑰組。如果您之前已生成過密鑰組,生成新密鑰組將會丟棄舊的密鑰組。 [secrets] secrets=Secret @@ -3066,6 +3663,7 @@ deletion=移除 Secret deletion.description=移除 Secret 是永久的且不可還原,是否繼續? deletion.success=已移除此 Secret。 deletion.failed=移除 Secret 失敗。 +management=Secret 管理 [actions] actions=Actions @@ -3077,6 +3675,7 @@ status.waiting=正在等候 status.running=正在執行 status.success=成功 status.failure=失敗 +status.cancelled=已取消 status.skipped=已略過 status.blocked=已阻塞 @@ -3093,6 +3692,7 @@ runners.labels=標籤 runners.last_online=最後上線時間 runners.runner_title=Runner runners.task_list=最近在此 Runner 上的任務 +runners.task_list.no_tasks=目前還沒有任務。 runners.task_list.run=執行 runners.task_list.status=狀態 runners.task_list.repository=儲存庫 @@ -3113,25 +3713,69 @@ runners.status.idle=閒置 runners.status.active=啟用 runners.status.offline=離線 runners.version=版本 +runners.reset_registration_token=重設註冊 Token runners.reset_registration_token_success=成功重設了 Runner 註冊 Token runs.all_workflows=所有工作流程 runs.commit=提交 +runs.scheduled=已排程 +runs.pushed_by=推送者 runs.invalid_workflow_helper=工作流程設定檔無效。請檢查您的設定檔: %s +runs.no_matching_online_runner_helper=沒有符合標籤的線上 Runner: %s +runs.no_job_without_needs=工作流程必須包含至少一個沒有依賴的工作。 +runs.no_job=工作流程必須包含至少一個工作 +runs.actor=執行者 runs.status=狀態 +runs.actors_no_select=所有執行者 +runs.status_no_select=所有狀態 +runs.no_results=沒有符合的結果。 +runs.no_workflows=目前還沒有工作流程。 +runs.no_workflows.quick_start=不知道如何開始使用 Gitea Actions?請參閱快速入門指南。 +runs.no_workflows.documentation=有關 Gitea Actions 的更多資訊,請參閱文件。 runs.no_runs=工作流程沒有執行過。 +runs.empty_commit_message=(空的提交訊息) +runs.expire_log_message=日誌已被清除,因為它們太舊了。 workflow.disable=停用工作流程 workflow.disable_success=已成功停用工作流程「%s」。 workflow.enable=啟用工作流程 workflow.enable_success=已成功啟用工作流程「%s」。 +workflow.disabled=工作流程已停用。 +workflow.run=執行工作流程 +workflow.not_found=找不到工作流程「%s」。 +workflow.run_success=工作流程「%s」執行成功。 +workflow.from_ref=使用工作流程來自 +workflow.has_workflow_dispatch=此工作流程有一個 workflow_dispatch 事件觸發器。 need_approval_desc=來自 Frok 儲存庫的合併請求需要核可才能執行工作流程。 +variables=變數 +variables.management=變數管理 +variables.creation=新增變數 +variables.none=還沒有任何變數。 +variables.deletion=移除變數 +variables.deletion.description=移除變數是永久的且不可還原,是否繼續? +variables.description=變數會被傳送到某些 Action 且無法以其他方式讀取。 +variables.id_not_exist=ID 為 %d 的變數不存在。 +variables.edit=編輯變數 +variables.deletion.failed=移除變數失敗。 +variables.deletion.success=已刪除變數。 +variables.creation.failed=新增變數失敗。 +variables.creation.success=已新增變數「%s」。 +variables.update.failed=編輯變數失敗。 +variables.update.success=已編輯變數。 [projects] +deleted.display_name=已刪除的專案 +type-1.display_name=個人專案 +type-2.display_name=儲存庫專案 +type-3.display_name=組織專案 [git.filemode] ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", … +directory=目錄 +normal_file=一般檔案 +executable_file=可執行檔 symbolic_link=符號連結 +submodule=子模組 From 195fccd6177d37fabe42d19b4764f5c0fd1b66c0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 18 Dec 2024 01:51:11 +0100 Subject: [PATCH 31/49] Move eslint config to javascript (#32869) In preparation for migrating to eslint flat config, move the config file to javascript. Additional changes: - `no-undef` is now disabled as it's no longer needed with typescript - `no-restricted-globals` config is simplified --- .eslintrc.cjs | 999 +++++++++++++++++++++++++++++++++++++++++++++++++ .eslintrc.yaml | 967 ----------------------------------------------- 2 files changed, 999 insertions(+), 967 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.yaml diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000000..af744db3e16 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,999 @@ +const restrictedSyntax = ['WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression']; + +module.exports = { + root: true, + reportUnusedDisableDirectives: true, + ignorePatterns: [ + '/web_src/js/vendor', + '/web_src/fomantic', + '/public/assets/js', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + project: true, + extraFileExtensions: ['.vue'], + parser: '@typescript-eslint/parser', // for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser + }, + settings: { + 'import-x/extensions': ['.js', '.ts'], + 'import-x/parsers': { + '@typescript-eslint/parser': ['.js', '.ts'], + }, + 'import-x/resolver': { + typescript: true, + }, + }, + plugins: [ + '@eslint-community/eslint-plugin-eslint-comments', + '@stylistic/eslint-plugin-js', + '@typescript-eslint/eslint-plugin', + 'eslint-plugin-array-func', + 'eslint-plugin-github', + 'eslint-plugin-import-x', + 'eslint-plugin-no-jquery', + 'eslint-plugin-no-use-extend-native', + 'eslint-plugin-regexp', + 'eslint-plugin-sonarjs', + 'eslint-plugin-unicorn', + 'eslint-plugin-vitest', + 'eslint-plugin-vitest-globals', + 'eslint-plugin-wc', + ], + env: { + es2024: true, + node: true, + }, + overrides: [ + { + files: ['web_src/**/*'], + globals: { + __webpack_public_path__: true, + process: false, // https://github.com/webpack/webpack/issues/15833 + }, + }, + { + files: ['web_src/**/*', 'docs/**/*'], + env: { + browser: true, + node: false, + }, + }, + { + files: ['*.config.*'], + rules: { + 'import-x/no-unused-modules': [0], + }, + }, + { + files: ['**/*.d.ts'], + rules: { + 'import-x/no-unused-modules': [0], + '@typescript-eslint/consistent-type-definitions': [0], + '@typescript-eslint/consistent-type-imports': [0], + }, + }, + { + files: ['web_src/js/types.ts'], + rules: { + 'import-x/no-unused-modules': [0], + }, + }, + { + files: ['**/*.test.*', 'web_src/js/test/setup.ts'], + env: { + 'vitest-globals/env': true, + }, + rules: { + 'vitest/consistent-test-filename': [0], + 'vitest/consistent-test-it': [0], + 'vitest/expect-expect': [0], + 'vitest/max-expects': [0], + 'vitest/max-nested-describe': [0], + 'vitest/no-alias-methods': [0], + 'vitest/no-commented-out-tests': [0], + 'vitest/no-conditional-expect': [0], + 'vitest/no-conditional-in-test': [0], + 'vitest/no-conditional-tests': [0], + 'vitest/no-disabled-tests': [0], + 'vitest/no-done-callback': [0], + 'vitest/no-duplicate-hooks': [0], + 'vitest/no-focused-tests': [0], + 'vitest/no-hooks': [0], + 'vitest/no-identical-title': [2], + 'vitest/no-interpolation-in-snapshots': [0], + 'vitest/no-large-snapshots': [0], + 'vitest/no-mocks-import': [0], + 'vitest/no-restricted-matchers': [0], + 'vitest/no-restricted-vi-methods': [0], + 'vitest/no-standalone-expect': [0], + 'vitest/no-test-prefixes': [0], + 'vitest/no-test-return-statement': [0], + 'vitest/prefer-called-with': [0], + 'vitest/prefer-comparison-matcher': [0], + 'vitest/prefer-each': [0], + 'vitest/prefer-equality-matcher': [0], + 'vitest/prefer-expect-resolves': [0], + 'vitest/prefer-hooks-in-order': [0], + 'vitest/prefer-hooks-on-top': [2], + 'vitest/prefer-lowercase-title': [0], + 'vitest/prefer-mock-promise-shorthand': [0], + 'vitest/prefer-snapshot-hint': [0], + 'vitest/prefer-spy-on': [0], + 'vitest/prefer-strict-equal': [0], + 'vitest/prefer-to-be': [0], + 'vitest/prefer-to-be-falsy': [0], + 'vitest/prefer-to-be-object': [0], + 'vitest/prefer-to-be-truthy': [0], + 'vitest/prefer-to-contain': [0], + 'vitest/prefer-to-have-length': [0], + 'vitest/prefer-todo': [0], + 'vitest/require-hook': [0], + 'vitest/require-to-throw-message': [0], + 'vitest/require-top-level-describe': [0], + 'vitest/valid-describe-callback': [2], + 'vitest/valid-expect': [2], + 'vitest/valid-title': [2], + }, + }, + { + files: ['web_src/js/modules/fetch.ts', 'web_src/js/standalone/**/*'], + rules: { + 'no-restricted-syntax': [2, ...restrictedSyntax], + }, + }, + { + files: ['**/*.vue'], + plugins: [ + 'eslint-plugin-vue', + 'eslint-plugin-vue-scoped-css', + ], + extends: [ + 'plugin:vue/vue3-recommended', + 'plugin:vue-scoped-css/vue3-recommended', + ], + rules: { + 'vue/attributes-order': [0], + 'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}], + 'vue/max-attributes-per-line': [0], + 'vue/singleline-html-element-content-newline': [0], + }, + }, + { + files: ['tests/e2e/**'], + plugins: [ + 'eslint-plugin-playwright' + ], + extends: [ + 'plugin:playwright/recommended', + ], + }, + ], + rules: { + '@eslint-community/eslint-comments/disable-enable-pair': [2], + '@eslint-community/eslint-comments/no-aggregating-enable': [2], + '@eslint-community/eslint-comments/no-duplicate-disable': [2], + '@eslint-community/eslint-comments/no-restricted-disable': [0], + '@eslint-community/eslint-comments/no-unlimited-disable': [2], + '@eslint-community/eslint-comments/no-unused-disable': [2], + '@eslint-community/eslint-comments/no-unused-enable': [2], + '@eslint-community/eslint-comments/no-use': [0], + '@eslint-community/eslint-comments/require-description': [0], + '@stylistic/js/array-bracket-newline': [0], + '@stylistic/js/array-bracket-spacing': [2, 'never'], + '@stylistic/js/array-element-newline': [0], + '@stylistic/js/arrow-parens': [2, 'always'], + '@stylistic/js/arrow-spacing': [2, {before: true, after: true}], + '@stylistic/js/block-spacing': [0], + '@stylistic/js/brace-style': [2, '1tbs', {allowSingleLine: true}], + '@stylistic/js/comma-dangle': [2, 'always-multiline'], + '@stylistic/js/comma-spacing': [2, {before: false, after: true}], + '@stylistic/js/comma-style': [2, 'last'], + '@stylistic/js/computed-property-spacing': [2, 'never'], + '@stylistic/js/dot-location': [2, 'property'], + '@stylistic/js/eol-last': [2], + '@stylistic/js/function-call-argument-newline': [0], + '@stylistic/js/function-call-spacing': [2, 'never'], + '@stylistic/js/function-paren-newline': [0], + '@stylistic/js/generator-star-spacing': [0], + '@stylistic/js/implicit-arrow-linebreak': [0], + '@stylistic/js/indent': [2, 2, {ignoreComments: true, SwitchCase: 1}], + '@stylistic/js/key-spacing': [2], + '@stylistic/js/keyword-spacing': [2], + '@stylistic/js/line-comment-position': [0], + '@stylistic/js/linebreak-style': [2, 'unix'], + '@stylistic/js/lines-around-comment': [0], + '@stylistic/js/lines-between-class-members': [0], + '@stylistic/js/max-len': [0], + '@stylistic/js/max-statements-per-line': [0], + '@stylistic/js/multiline-comment-style': [0], + '@stylistic/js/multiline-ternary': [0], + '@stylistic/js/new-parens': [2], + '@stylistic/js/newline-per-chained-call': [0], + '@stylistic/js/no-confusing-arrow': [0], + '@stylistic/js/no-extra-parens': [0], + '@stylistic/js/no-extra-semi': [2], + '@stylistic/js/no-floating-decimal': [0], + '@stylistic/js/no-mixed-operators': [0], + '@stylistic/js/no-mixed-spaces-and-tabs': [2], + '@stylistic/js/no-multi-spaces': [2, {ignoreEOLComments: true, exceptions: {Property: true}}], + '@stylistic/js/no-multiple-empty-lines': [2, {max: 1, maxEOF: 0, maxBOF: 0}], + '@stylistic/js/no-tabs': [2], + '@stylistic/js/no-trailing-spaces': [2], + '@stylistic/js/no-whitespace-before-property': [2], + '@stylistic/js/nonblock-statement-body-position': [2], + '@stylistic/js/object-curly-newline': [0], + '@stylistic/js/object-curly-spacing': [2, 'never'], + '@stylistic/js/object-property-newline': [0], + '@stylistic/js/one-var-declaration-per-line': [0], + '@stylistic/js/operator-linebreak': [2, 'after'], + '@stylistic/js/padded-blocks': [2, 'never'], + '@stylistic/js/padding-line-between-statements': [0], + '@stylistic/js/quote-props': [0], + '@stylistic/js/quotes': [2, 'single', {avoidEscape: true, allowTemplateLiterals: true}], + '@stylistic/js/rest-spread-spacing': [2, 'never'], + '@stylistic/js/semi': [2, 'always', {omitLastInOneLineBlock: true}], + '@stylistic/js/semi-spacing': [2, {before: false, after: true}], + '@stylistic/js/semi-style': [2, 'last'], + '@stylistic/js/space-before-blocks': [2, 'always'], + '@stylistic/js/space-before-function-paren': [2, {anonymous: 'ignore', named: 'never', asyncArrow: 'always'}], + '@stylistic/js/space-in-parens': [2, 'never'], + '@stylistic/js/space-infix-ops': [2], + '@stylistic/js/space-unary-ops': [2], + '@stylistic/js/spaced-comment': [2, 'always'], + '@stylistic/js/switch-colon-spacing': [2], + '@stylistic/js/template-curly-spacing': [2, 'never'], + '@stylistic/js/template-tag-spacing': [2, 'never'], + '@stylistic/js/wrap-iife': [2, 'inside'], + '@stylistic/js/wrap-regex': [0], + '@stylistic/js/yield-star-spacing': [2, 'after'], + '@typescript-eslint/adjacent-overload-signatures': [0], + '@typescript-eslint/array-type': [0], + '@typescript-eslint/await-thenable': [2], + '@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}], + '@typescript-eslint/ban-tslint-comment': [0], + '@typescript-eslint/class-literal-property-style': [0], + '@typescript-eslint/class-methods-use-this': [0], + '@typescript-eslint/consistent-generic-constructors': [0], + '@typescript-eslint/consistent-indexed-object-style': [0], + '@typescript-eslint/consistent-return': [0], + '@typescript-eslint/consistent-type-assertions': [2, {assertionStyle: 'as', objectLiteralTypeAssertions: 'allow'}], + '@typescript-eslint/consistent-type-definitions': [2, 'type'], + '@typescript-eslint/consistent-type-exports': [2, {fixMixedExportsWithInlineTypeSpecifier: false}], + '@typescript-eslint/consistent-type-imports': [2, {prefer: 'type-imports', fixStyle: 'separate-type-imports', disallowTypeAnnotations: true}], + '@typescript-eslint/default-param-last': [0], + '@typescript-eslint/dot-notation': [0], + '@typescript-eslint/explicit-function-return-type': [0], + '@typescript-eslint/explicit-member-accessibility': [0], + '@typescript-eslint/explicit-module-boundary-types': [0], + '@typescript-eslint/init-declarations': [0], + '@typescript-eslint/max-params': [0], + '@typescript-eslint/member-ordering': [0], + '@typescript-eslint/method-signature-style': [0], + '@typescript-eslint/naming-convention': [0], + '@typescript-eslint/no-array-constructor': [2], + '@typescript-eslint/no-array-delete': [2], + '@typescript-eslint/no-base-to-string': [0], + '@typescript-eslint/no-confusing-non-null-assertion': [2], + '@typescript-eslint/no-confusing-void-expression': [0], + '@typescript-eslint/no-deprecated': [2], + '@typescript-eslint/no-dupe-class-members': [0], + '@typescript-eslint/no-duplicate-enum-values': [2], + '@typescript-eslint/no-duplicate-type-constituents': [2, {ignoreUnions: true}], + '@typescript-eslint/no-dynamic-delete': [0], + '@typescript-eslint/no-empty-function': [0], + '@typescript-eslint/no-empty-interface': [0], + '@typescript-eslint/no-empty-object-type': [2], + '@typescript-eslint/no-explicit-any': [0], + '@typescript-eslint/no-extra-non-null-assertion': [2], + '@typescript-eslint/no-extraneous-class': [0], + '@typescript-eslint/no-floating-promises': [0], + '@typescript-eslint/no-for-in-array': [2], + '@typescript-eslint/no-implied-eval': [2], + '@typescript-eslint/no-import-type-side-effects': [0], // dupe with consistent-type-imports + '@typescript-eslint/no-inferrable-types': [0], + '@typescript-eslint/no-invalid-this': [0], + '@typescript-eslint/no-invalid-void-type': [0], + '@typescript-eslint/no-loop-func': [0], + '@typescript-eslint/no-loss-of-precision': [0], + '@typescript-eslint/no-magic-numbers': [0], + '@typescript-eslint/no-meaningless-void-operator': [0], + '@typescript-eslint/no-misused-new': [2], + '@typescript-eslint/no-misused-promises': [2, {checksVoidReturn: {attributes: false, arguments: false}}], + '@typescript-eslint/no-mixed-enums': [0], + '@typescript-eslint/no-namespace': [2], + '@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0], + '@typescript-eslint/no-non-null-asserted-optional-chain': [2], + '@typescript-eslint/no-non-null-assertion': [0], + '@typescript-eslint/no-redeclare': [0], + '@typescript-eslint/no-redundant-type-constituents': [2], + '@typescript-eslint/no-require-imports': [2], + '@typescript-eslint/no-restricted-imports': [0], + '@typescript-eslint/no-restricted-types': [0], + '@typescript-eslint/no-shadow': [0], + '@typescript-eslint/no-this-alias': [0], // handled by unicorn/no-this-assignment + '@typescript-eslint/no-unnecessary-boolean-literal-compare': [0], + '@typescript-eslint/no-unnecessary-condition': [0], + '@typescript-eslint/no-unnecessary-qualifier': [0], + '@typescript-eslint/no-unnecessary-template-expression': [0], + '@typescript-eslint/no-unnecessary-type-arguments': [0], + '@typescript-eslint/no-unnecessary-type-assertion': [2], + '@typescript-eslint/no-unnecessary-type-constraint': [2], + '@typescript-eslint/no-unsafe-argument': [0], + '@typescript-eslint/no-unsafe-assignment': [0], + '@typescript-eslint/no-unsafe-call': [0], + '@typescript-eslint/no-unsafe-declaration-merging': [2], + '@typescript-eslint/no-unsafe-enum-comparison': [2], + '@typescript-eslint/no-unsafe-function-type': [2], + '@typescript-eslint/no-unsafe-member-access': [0], + '@typescript-eslint/no-unsafe-return': [0], + '@typescript-eslint/no-unsafe-unary-minus': [2], + '@typescript-eslint/no-unused-expressions': [0], + '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}], + '@typescript-eslint/no-use-before-define': [0], + '@typescript-eslint/no-useless-constructor': [0], + '@typescript-eslint/no-useless-empty-export': [0], + '@typescript-eslint/no-wrapper-object-types': [2], + '@typescript-eslint/non-nullable-type-assertion-style': [0], + '@typescript-eslint/only-throw-error': [2], + '@typescript-eslint/parameter-properties': [0], + '@typescript-eslint/prefer-as-const': [2], + '@typescript-eslint/prefer-destructuring': [0], + '@typescript-eslint/prefer-enum-initializers': [0], + '@typescript-eslint/prefer-find': [2], + '@typescript-eslint/prefer-for-of': [2], + '@typescript-eslint/prefer-function-type': [2], + '@typescript-eslint/prefer-includes': [2], + '@typescript-eslint/prefer-literal-enum-member': [0], + '@typescript-eslint/prefer-namespace-keyword': [0], + '@typescript-eslint/prefer-nullish-coalescing': [0], + '@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}], + '@typescript-eslint/prefer-promise-reject-errors': [0], + '@typescript-eslint/prefer-readonly': [0], + '@typescript-eslint/prefer-readonly-parameter-types': [0], + '@typescript-eslint/prefer-reduce-type-parameter': [0], + '@typescript-eslint/prefer-regexp-exec': [0], + '@typescript-eslint/prefer-return-this-type': [0], + '@typescript-eslint/prefer-string-starts-ends-with': [2, {allowSingleElementEquality: 'always'}], + '@typescript-eslint/promise-function-async': [0], + '@typescript-eslint/require-array-sort-compare': [0], + '@typescript-eslint/require-await': [0], + '@typescript-eslint/restrict-plus-operands': [2], + '@typescript-eslint/restrict-template-expressions': [0], + '@typescript-eslint/return-await': [0], + '@typescript-eslint/strict-boolean-expressions': [0], + '@typescript-eslint/switch-exhaustiveness-check': [0], + '@typescript-eslint/triple-slash-reference': [2], + '@typescript-eslint/typedef': [0], + '@typescript-eslint/unbound-method': [0], // too many false-positives + '@typescript-eslint/unified-signatures': [2], + 'accessor-pairs': [2], + 'array-callback-return': [2, {checkForEach: true}], + 'array-func/avoid-reverse': [2], + 'array-func/from-map': [2], + 'array-func/no-unnecessary-this-arg': [2], + 'array-func/prefer-array-from': [2], + 'array-func/prefer-flat-map': [0], // handled by unicorn/prefer-array-flat-map + 'array-func/prefer-flat': [0], // handled by unicorn/prefer-array-flat + 'arrow-body-style': [0], + 'block-scoped-var': [2], + 'camelcase': [0], + 'capitalized-comments': [0], + 'class-methods-use-this': [0], + 'complexity': [0], + 'consistent-return': [0], + 'consistent-this': [0], + 'constructor-super': [2], + 'curly': [0], + 'default-case-last': [2], + 'default-case': [0], + 'default-param-last': [0], + 'dot-notation': [0], + 'eqeqeq': [2], + 'for-direction': [2], + 'func-name-matching': [2], + 'func-names': [0], + 'func-style': [0], + 'getter-return': [2], + 'github/a11y-aria-label-is-well-formatted': [0], + 'github/a11y-no-title-attribute': [0], + 'github/a11y-no-visually-hidden-interactive-element': [0], + 'github/a11y-role-supports-aria-props': [0], + 'github/a11y-svg-has-accessible-name': [0], + 'github/array-foreach': [0], + 'github/async-currenttarget': [2], + 'github/async-preventdefault': [2], + 'github/authenticity-token': [0], + 'github/get-attribute': [0], + 'github/js-class-name': [0], + 'github/no-blur': [0], + 'github/no-d-none': [0], + 'github/no-dataset': [2], + 'github/no-dynamic-script-tag': [2], + 'github/no-implicit-buggy-globals': [2], + 'github/no-inner-html': [0], + 'github/no-innerText': [2], + 'github/no-then': [2], + 'github/no-useless-passive': [2], + 'github/prefer-observers': [2], + 'github/require-passive-events': [2], + 'github/unescaped-html-literal': [0], + 'grouped-accessor-pairs': [2], + 'guard-for-in': [0], + 'id-blacklist': [0], + 'id-length': [0], + 'id-match': [0], + 'import-x/consistent-type-specifier-style': [0], + 'import-x/default': [0], + 'import-x/dynamic-import-chunkname': [0], + 'import-x/export': [2], + 'import-x/exports-last': [0], + 'import-x/extensions': [2, 'always', {ignorePackages: true}], + 'import-x/first': [2], + 'import-x/group-exports': [0], + 'import-x/max-dependencies': [0], + 'import-x/named': [2], + 'import-x/namespace': [0], + 'import-x/newline-after-import': [0], + 'import-x/no-absolute-path': [0], + 'import-x/no-amd': [2], + 'import-x/no-anonymous-default-export': [0], + 'import-x/no-commonjs': [2], + 'import-x/no-cycle': [2, {ignoreExternal: true, maxDepth: 1}], + 'import-x/no-default-export': [0], + 'import-x/no-deprecated': [0], + 'import-x/no-dynamic-require': [0], + 'import-x/no-empty-named-blocks': [2], + 'import-x/no-extraneous-dependencies': [2], + 'import-x/no-import-module-exports': [0], + 'import-x/no-internal-modules': [0], + 'import-x/no-mutable-exports': [0], + 'import-x/no-named-as-default-member': [0], + 'import-x/no-named-as-default': [0], + 'import-x/no-named-default': [0], + 'import-x/no-named-export': [0], + 'import-x/no-namespace': [0], + 'import-x/no-nodejs-modules': [0], + 'import-x/no-relative-packages': [0], + 'import-x/no-relative-parent-imports': [0], + 'import-x/no-restricted-paths': [0], + 'import-x/no-self-import': [2], + 'import-x/no-unassigned-import': [0], + 'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}], + 'import-x/no-unused-modules': [2, {unusedExports: true}], + 'import-x/no-useless-path-segments': [2, {commonjs: true}], + 'import-x/no-webpack-loader-syntax': [2], + 'import-x/order': [0], + 'import-x/prefer-default-export': [0], + 'import-x/unambiguous': [0], + 'init-declarations': [0], + 'line-comment-position': [0], + 'logical-assignment-operators': [0], + 'max-classes-per-file': [0], + 'max-depth': [0], + 'max-lines-per-function': [0], + 'max-lines': [0], + 'max-nested-callbacks': [0], + 'max-params': [0], + 'max-statements': [0], + 'multiline-comment-style': [2, 'separate-lines'], + 'new-cap': [0], + 'no-alert': [0], + 'no-array-constructor': [0], // handled by @typescript-eslint/no-array-constructor + 'no-async-promise-executor': [0], + 'no-await-in-loop': [0], + 'no-bitwise': [0], + 'no-buffer-constructor': [0], + 'no-caller': [2], + 'no-case-declarations': [2], + 'no-class-assign': [2], + 'no-compare-neg-zero': [2], + 'no-cond-assign': [2, 'except-parens'], + 'no-console': [1, {allow: ['debug', 'info', 'warn', 'error']}], + 'no-const-assign': [2], + 'no-constant-binary-expression': [2], + 'no-constant-condition': [0], + 'no-constructor-return': [2], + 'no-continue': [0], + 'no-control-regex': [0], + 'no-debugger': [1], + 'no-delete-var': [2], + 'no-div-regex': [0], + 'no-dupe-args': [2], + 'no-dupe-class-members': [2], + 'no-dupe-else-if': [2], + 'no-dupe-keys': [2], + 'no-duplicate-case': [2], + 'no-duplicate-imports': [0], + 'no-else-return': [2], + 'no-empty-character-class': [2], + 'no-empty-function': [0], + 'no-empty-pattern': [2], + 'no-empty-static-block': [2], + 'no-empty': [2, {allowEmptyCatch: true}], + 'no-eq-null': [2], + 'no-eval': [2], + 'no-ex-assign': [2], + 'no-extend-native': [2], + 'no-extra-bind': [2], + 'no-extra-boolean-cast': [2], + 'no-extra-label': [0], + 'no-fallthrough': [2], + 'no-func-assign': [2], + 'no-global-assign': [2], + 'no-implicit-coercion': [2], + 'no-implicit-globals': [0], + 'no-implied-eval': [0], // handled by @typescript-eslint/no-implied-eval + 'no-import-assign': [2], + 'no-inline-comments': [0], + 'no-inner-declarations': [2], + 'no-invalid-regexp': [2], + 'no-invalid-this': [0], + 'no-irregular-whitespace': [2], + 'no-iterator': [2], + 'no-jquery/no-ajax-events': [2], + 'no-jquery/no-ajax': [2], + 'no-jquery/no-and-self': [2], + 'no-jquery/no-animate-toggle': [2], + 'no-jquery/no-animate': [2], + 'no-jquery/no-append-html': [2], + 'no-jquery/no-attr': [2], + 'no-jquery/no-bind': [2], + 'no-jquery/no-box-model': [2], + 'no-jquery/no-browser': [2], + 'no-jquery/no-camel-case': [2], + 'no-jquery/no-class-state': [2], + 'no-jquery/no-class': [0], + 'no-jquery/no-clone': [2], + 'no-jquery/no-closest': [0], + 'no-jquery/no-constructor-attributes': [2], + 'no-jquery/no-contains': [2], + 'no-jquery/no-context-prop': [2], + 'no-jquery/no-css': [2], + 'no-jquery/no-data': [0], + 'no-jquery/no-deferred': [2], + 'no-jquery/no-delegate': [2], + 'no-jquery/no-done-fail': [2], + 'no-jquery/no-each-collection': [0], + 'no-jquery/no-each-util': [0], + 'no-jquery/no-each': [0], + 'no-jquery/no-error-shorthand': [2], + 'no-jquery/no-error': [2], + 'no-jquery/no-escape-selector': [2], + 'no-jquery/no-event-shorthand': [2], + 'no-jquery/no-extend': [2], + 'no-jquery/no-fade': [2], + 'no-jquery/no-filter': [0], + 'no-jquery/no-find-collection': [0], + 'no-jquery/no-find-util': [2], + 'no-jquery/no-find': [0], + 'no-jquery/no-fx-interval': [2], + 'no-jquery/no-fx': [2], + 'no-jquery/no-global-eval': [2], + 'no-jquery/no-global-selector': [0], + 'no-jquery/no-grep': [2], + 'no-jquery/no-has': [2], + 'no-jquery/no-hold-ready': [2], + 'no-jquery/no-html': [0], + 'no-jquery/no-in-array': [2], + 'no-jquery/no-is-array': [2], + 'no-jquery/no-is-empty-object': [2], + 'no-jquery/no-is-function': [2], + 'no-jquery/no-is-numeric': [2], + 'no-jquery/no-is-plain-object': [2], + 'no-jquery/no-is-window': [2], + 'no-jquery/no-is': [2], + 'no-jquery/no-jquery-constructor': [0], + 'no-jquery/no-live': [2], + 'no-jquery/no-load-shorthand': [2], + 'no-jquery/no-load': [2], + 'no-jquery/no-map-collection': [0], + 'no-jquery/no-map-util': [2], + 'no-jquery/no-map': [2], + 'no-jquery/no-merge': [2], + 'no-jquery/no-node-name': [2], + 'no-jquery/no-noop': [2], + 'no-jquery/no-now': [2], + 'no-jquery/no-on-ready': [2], + 'no-jquery/no-other-methods': [0], + 'no-jquery/no-other-utils': [2], + 'no-jquery/no-param': [2], + 'no-jquery/no-parent': [0], + 'no-jquery/no-parents': [2], + 'no-jquery/no-parse-html-literal': [2], + 'no-jquery/no-parse-html': [2], + 'no-jquery/no-parse-json': [2], + 'no-jquery/no-parse-xml': [2], + 'no-jquery/no-prop': [2], + 'no-jquery/no-proxy': [2], + 'no-jquery/no-ready-shorthand': [2], + 'no-jquery/no-ready': [2], + 'no-jquery/no-selector-prop': [2], + 'no-jquery/no-serialize': [2], + 'no-jquery/no-size': [2], + 'no-jquery/no-sizzle': [2], + 'no-jquery/no-slide': [2], + 'no-jquery/no-sub': [2], + 'no-jquery/no-support': [2], + 'no-jquery/no-text': [2], + 'no-jquery/no-trigger': [0], + 'no-jquery/no-trim': [2], + 'no-jquery/no-type': [2], + 'no-jquery/no-unique': [2], + 'no-jquery/no-unload-shorthand': [2], + 'no-jquery/no-val': [0], + 'no-jquery/no-visibility': [2], + 'no-jquery/no-when': [2], + 'no-jquery/no-wrap': [2], + 'no-jquery/variable-pattern': [2], + 'no-label-var': [2], + 'no-labels': [0], // handled by no-restricted-syntax + 'no-lone-blocks': [2], + 'no-lonely-if': [0], + 'no-loop-func': [0], + 'no-loss-of-precision': [2], + 'no-magic-numbers': [0], + 'no-misleading-character-class': [2], + 'no-multi-assign': [0], + 'no-multi-str': [2], + 'no-negated-condition': [0], + 'no-nested-ternary': [0], + 'no-new-func': [2], + 'no-new-native-nonconstructor': [2], + 'no-new-object': [2], + 'no-new-symbol': [2], + 'no-new-wrappers': [2], + 'no-new': [0], + 'no-nonoctal-decimal-escape': [2], + 'no-obj-calls': [2], + 'no-octal-escape': [2], + 'no-octal': [2], + 'no-param-reassign': [0], + 'no-plusplus': [0], + 'no-promise-executor-return': [0], + 'no-proto': [2], + 'no-prototype-builtins': [2], + 'no-redeclare': [0], // must be disabled for typescript overloads + 'no-regex-spaces': [2], + 'no-restricted-exports': [0], + 'no-restricted-globals': [2, 'addEventListener', 'blur', 'close', 'closed', 'confirm', 'defaultStatus', 'defaultstatus', 'error', 'event', 'external', 'find', 'focus', 'frameElement', 'frames', 'history', 'innerHeight', 'innerWidth', 'isFinite', 'isNaN', 'length', 'locationbar', 'menubar', 'moveBy', 'moveTo', 'name', 'onblur', 'onerror', 'onfocus', 'onload', 'onresize', 'onunload', 'open', 'opener', 'opera', 'outerHeight', 'outerWidth', 'pageXOffset', 'pageYOffset', 'parent', 'print', 'removeEventListener', 'resizeBy', 'resizeTo', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scroll', 'scrollbars', 'scrollBy', 'scrollTo', 'scrollX', 'scrollY', 'status', 'statusbar', 'stop', 'toolbar', 'top'], + 'no-restricted-imports': [0], + 'no-restricted-syntax': [2, ...restrictedSyntax, {selector: 'CallExpression[callee.name="fetch"]', message: 'use modules/fetch.ts instead'}], + 'no-return-assign': [0], + 'no-script-url': [2], + 'no-self-assign': [2, {props: true}], + 'no-self-compare': [2], + 'no-sequences': [2], + 'no-setter-return': [2], + 'no-shadow-restricted-names': [2], + 'no-shadow': [0], + 'no-sparse-arrays': [2], + 'no-template-curly-in-string': [2], + 'no-ternary': [0], + 'no-this-before-super': [2], + 'no-throw-literal': [2], + 'no-undef-init': [2], + 'no-undef': [0], + 'no-undefined': [0], + 'no-underscore-dangle': [0], + 'no-unexpected-multiline': [2], + 'no-unmodified-loop-condition': [2], + 'no-unneeded-ternary': [2], + 'no-unreachable-loop': [2], + 'no-unreachable': [2], + 'no-unsafe-finally': [2], + 'no-unsafe-negation': [2], + 'no-unused-expressions': [2], + 'no-unused-labels': [2], + 'no-unused-private-class-members': [2], + 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars + 'no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true}], + 'no-use-extend-native/no-use-extend-native': [2], + 'no-useless-backreference': [2], + 'no-useless-call': [2], + 'no-useless-catch': [2], + 'no-useless-computed-key': [2], + 'no-useless-concat': [2], + 'no-useless-constructor': [2], + 'no-useless-escape': [2], + 'no-useless-rename': [2], + 'no-useless-return': [2], + 'no-var': [2], + 'no-void': [2], + 'no-warning-comments': [0], + 'no-with': [0], // handled by no-restricted-syntax + 'object-shorthand': [2, 'always'], + 'one-var-declaration-per-line': [0], + 'one-var': [0], + 'operator-assignment': [2, 'always'], + 'operator-linebreak': [2, 'after'], + 'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}], + 'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}], + 'prefer-destructuring': [0], + 'prefer-exponentiation-operator': [2], + 'prefer-named-capture-group': [0], + 'prefer-numeric-literals': [2], + 'prefer-object-has-own': [2], + 'prefer-object-spread': [2], + 'prefer-promise-reject-errors': [2, {allowEmptyReject: false}], + 'prefer-regex-literals': [2], + 'prefer-rest-params': [2], + 'prefer-spread': [2], + 'prefer-template': [2], + 'radix': [2, 'as-needed'], + 'regexp/confusing-quantifier': [2], + 'regexp/control-character-escape': [2], + 'regexp/hexadecimal-escape': [0], + 'regexp/letter-case': [0], + 'regexp/match-any': [2], + 'regexp/negation': [2], + 'regexp/no-contradiction-with-assertion': [0], + 'regexp/no-control-character': [0], + 'regexp/no-dupe-characters-character-class': [2], + 'regexp/no-dupe-disjunctions': [2], + 'regexp/no-empty-alternative': [2], + 'regexp/no-empty-capturing-group': [2], + 'regexp/no-empty-character-class': [0], + 'regexp/no-empty-group': [2], + 'regexp/no-empty-lookarounds-assertion': [2], + 'regexp/no-empty-string-literal': [2], + 'regexp/no-escape-backspace': [2], + 'regexp/no-extra-lookaround-assertions': [0], + 'regexp/no-invalid-regexp': [2], + 'regexp/no-invisible-character': [2], + 'regexp/no-lazy-ends': [2], + 'regexp/no-legacy-features': [2], + 'regexp/no-misleading-capturing-group': [0], + 'regexp/no-misleading-unicode-character': [0], + 'regexp/no-missing-g-flag': [2], + 'regexp/no-non-standard-flag': [2], + 'regexp/no-obscure-range': [2], + 'regexp/no-octal': [2], + 'regexp/no-optional-assertion': [2], + 'regexp/no-potentially-useless-backreference': [2], + 'regexp/no-standalone-backslash': [2], + 'regexp/no-super-linear-backtracking': [0], + 'regexp/no-super-linear-move': [0], + 'regexp/no-trivially-nested-assertion': [2], + 'regexp/no-trivially-nested-quantifier': [2], + 'regexp/no-unused-capturing-group': [0], + 'regexp/no-useless-assertions': [2], + 'regexp/no-useless-backreference': [2], + 'regexp/no-useless-character-class': [2], + 'regexp/no-useless-dollar-replacements': [2], + 'regexp/no-useless-escape': [2], + 'regexp/no-useless-flag': [2], + 'regexp/no-useless-lazy': [2], + 'regexp/no-useless-non-capturing-group': [2], + 'regexp/no-useless-quantifier': [2], + 'regexp/no-useless-range': [2], + 'regexp/no-useless-set-operand': [2], + 'regexp/no-useless-string-literal': [2], + 'regexp/no-useless-two-nums-quantifier': [2], + 'regexp/no-zero-quantifier': [2], + 'regexp/optimal-lookaround-quantifier': [2], + 'regexp/optimal-quantifier-concatenation': [0], + 'regexp/prefer-character-class': [0], + 'regexp/prefer-d': [0], + 'regexp/prefer-escape-replacement-dollar-char': [0], + 'regexp/prefer-lookaround': [0], + 'regexp/prefer-named-backreference': [0], + 'regexp/prefer-named-capture-group': [0], + 'regexp/prefer-named-replacement': [0], + 'regexp/prefer-plus-quantifier': [2], + 'regexp/prefer-predefined-assertion': [2], + 'regexp/prefer-quantifier': [0], + 'regexp/prefer-question-quantifier': [2], + 'regexp/prefer-range': [2], + 'regexp/prefer-regexp-exec': [2], + 'regexp/prefer-regexp-test': [2], + 'regexp/prefer-result-array-groups': [0], + 'regexp/prefer-set-operation': [2], + 'regexp/prefer-star-quantifier': [2], + 'regexp/prefer-unicode-codepoint-escapes': [2], + 'regexp/prefer-w': [0], + 'regexp/require-unicode-regexp': [0], + 'regexp/simplify-set-operations': [2], + 'regexp/sort-alternatives': [0], + 'regexp/sort-character-class-elements': [0], + 'regexp/sort-flags': [0], + 'regexp/strict': [2], + 'regexp/unicode-escape': [0], + 'regexp/use-ignore-case': [0], + 'require-atomic-updates': [0], + 'require-await': [0], // handled by @typescript-eslint/require-await + 'require-unicode-regexp': [0], + 'require-yield': [2], + 'sonarjs/cognitive-complexity': [0], + 'sonarjs/elseif-without-else': [0], + 'sonarjs/max-switch-cases': [0], + 'sonarjs/no-all-duplicated-branches': [2], + 'sonarjs/no-collapsible-if': [0], + 'sonarjs/no-collection-size-mischeck': [2], + 'sonarjs/no-duplicate-string': [0], + 'sonarjs/no-duplicated-branches': [0], + 'sonarjs/no-element-overwrite': [2], + 'sonarjs/no-empty-collection': [2], + 'sonarjs/no-extra-arguments': [2], + 'sonarjs/no-gratuitous-expressions': [2], + 'sonarjs/no-identical-conditions': [2], + 'sonarjs/no-identical-expressions': [2], + 'sonarjs/no-identical-functions': [2, 5], + 'sonarjs/no-ignored-return': [2], + 'sonarjs/no-inverted-boolean-check': [2], + 'sonarjs/no-nested-switch': [0], + 'sonarjs/no-nested-template-literals': [0], + 'sonarjs/no-one-iteration-loop': [2], + 'sonarjs/no-redundant-boolean': [2], + 'sonarjs/no-redundant-jump': [2], + 'sonarjs/no-same-line-conditional': [2], + 'sonarjs/no-small-switch': [0], + 'sonarjs/no-unused-collection': [2], + 'sonarjs/no-use-of-empty-return-value': [2], + 'sonarjs/no-useless-catch': [2], + 'sonarjs/non-existent-operator': [2], + 'sonarjs/prefer-immediate-return': [0], + 'sonarjs/prefer-object-literal': [0], + 'sonarjs/prefer-single-boolean-return': [0], + 'sonarjs/prefer-while': [2], + 'sort-imports': [0], + 'sort-keys': [0], + 'sort-vars': [0], + 'strict': [0], + 'symbol-description': [2], + 'unicode-bom': [2, 'never'], + 'unicorn/better-regex': [0], + 'unicorn/catch-error-name': [0], + 'unicorn/consistent-destructuring': [2], + 'unicorn/consistent-empty-array-spread': [2], + 'unicorn/consistent-existence-index-check': [0], + 'unicorn/consistent-function-scoping': [0], + 'unicorn/custom-error-definition': [0], + 'unicorn/empty-brace-spaces': [2], + 'unicorn/error-message': [0], + 'unicorn/escape-case': [0], + 'unicorn/expiring-todo-comments': [0], + 'unicorn/explicit-length-check': [0], + 'unicorn/filename-case': [0], + 'unicorn/import-index': [0], + 'unicorn/import-style': [0], + 'unicorn/new-for-builtins': [2], + 'unicorn/no-abusive-eslint-disable': [0], + 'unicorn/no-anonymous-default-export': [0], + 'unicorn/no-array-callback-reference': [0], + 'unicorn/no-array-for-each': [2], + 'unicorn/no-array-method-this-argument': [2], + 'unicorn/no-array-push-push': [2], + 'unicorn/no-array-reduce': [2], + 'unicorn/no-await-expression-member': [0], + 'unicorn/no-await-in-promise-methods': [2], + 'unicorn/no-console-spaces': [0], + 'unicorn/no-document-cookie': [2], + 'unicorn/no-empty-file': [2], + 'unicorn/no-for-loop': [0], + 'unicorn/no-hex-escape': [0], + 'unicorn/no-instanceof-array': [0], + 'unicorn/no-invalid-fetch-options': [2], + 'unicorn/no-invalid-remove-event-listener': [2], + 'unicorn/no-keyword-prefix': [0], + 'unicorn/no-length-as-slice-end': [2], + 'unicorn/no-lonely-if': [2], + 'unicorn/no-magic-array-flat-depth': [0], + 'unicorn/no-negated-condition': [0], + 'unicorn/no-negation-in-equality-check': [2], + 'unicorn/no-nested-ternary': [0], + 'unicorn/no-new-array': [0], + 'unicorn/no-new-buffer': [0], + 'unicorn/no-null': [0], + 'unicorn/no-object-as-default-parameter': [0], + 'unicorn/no-process-exit': [0], + 'unicorn/no-single-promise-in-promise-methods': [2], + 'unicorn/no-static-only-class': [2], + 'unicorn/no-thenable': [2], + 'unicorn/no-this-assignment': [2], + 'unicorn/no-typeof-undefined': [2], + 'unicorn/no-unnecessary-await': [2], + 'unicorn/no-unnecessary-polyfills': [2], + 'unicorn/no-unreadable-array-destructuring': [0], + 'unicorn/no-unreadable-iife': [2], + 'unicorn/no-unused-properties': [2], + 'unicorn/no-useless-fallback-in-spread': [2], + 'unicorn/no-useless-length-check': [2], + 'unicorn/no-useless-promise-resolve-reject': [2], + 'unicorn/no-useless-spread': [2], + 'unicorn/no-useless-switch-case': [2], + 'unicorn/no-useless-undefined': [0], + 'unicorn/no-zero-fractions': [2], + 'unicorn/number-literal-case': [0], + 'unicorn/numeric-separators-style': [0], + 'unicorn/prefer-add-event-listener': [2], + 'unicorn/prefer-array-find': [2], + 'unicorn/prefer-array-flat-map': [2], + 'unicorn/prefer-array-flat': [2], + 'unicorn/prefer-array-index-of': [2], + 'unicorn/prefer-array-some': [2], + 'unicorn/prefer-at': [0], + 'unicorn/prefer-blob-reading-methods': [2], + 'unicorn/prefer-code-point': [0], + 'unicorn/prefer-date-now': [2], + 'unicorn/prefer-default-parameters': [0], + 'unicorn/prefer-dom-node-append': [2], + 'unicorn/prefer-dom-node-dataset': [0], + 'unicorn/prefer-dom-node-remove': [2], + 'unicorn/prefer-dom-node-text-content': [2], + 'unicorn/prefer-event-target': [2], + 'unicorn/prefer-export-from': [0], + 'unicorn/prefer-global-this': [0], + 'unicorn/prefer-includes': [2], + 'unicorn/prefer-json-parse-buffer': [0], + 'unicorn/prefer-keyboard-event-key': [2], + 'unicorn/prefer-logical-operator-over-ternary': [2], + 'unicorn/prefer-math-min-max': [2], + 'unicorn/prefer-math-trunc': [2], + 'unicorn/prefer-modern-dom-apis': [0], + 'unicorn/prefer-modern-math-apis': [2], + 'unicorn/prefer-module': [2], + 'unicorn/prefer-native-coercion-functions': [2], + 'unicorn/prefer-negative-index': [2], + 'unicorn/prefer-node-protocol': [2], + 'unicorn/prefer-number-properties': [0], + 'unicorn/prefer-object-from-entries': [2], + 'unicorn/prefer-object-has-own': [0], + 'unicorn/prefer-optional-catch-binding': [2], + 'unicorn/prefer-prototype-methods': [0], + 'unicorn/prefer-query-selector': [2], + 'unicorn/prefer-reflect-apply': [0], + 'unicorn/prefer-regexp-test': [2], + 'unicorn/prefer-set-has': [0], + 'unicorn/prefer-set-size': [2], + 'unicorn/prefer-spread': [0], + 'unicorn/prefer-string-raw': [0], + 'unicorn/prefer-string-replace-all': [0], + 'unicorn/prefer-string-slice': [0], + 'unicorn/prefer-string-starts-ends-with': [2], + 'unicorn/prefer-string-trim-start-end': [2], + 'unicorn/prefer-structured-clone': [2], + 'unicorn/prefer-switch': [0], + 'unicorn/prefer-ternary': [0], + 'unicorn/prefer-text-content': [2], + 'unicorn/prefer-top-level-await': [0], + 'unicorn/prefer-type-error': [0], + 'unicorn/prevent-abbreviations': [0], + 'unicorn/relative-url-style': [2], + 'unicorn/require-array-join-separator': [2], + 'unicorn/require-number-to-fixed-digits-argument': [2], + 'unicorn/require-post-message-target-origin': [0], + 'unicorn/string-content': [0], + 'unicorn/switch-case-braces': [0], + 'unicorn/template-indent': [2], + 'unicorn/text-encoding-identifier-case': [0], + 'unicorn/throw-new-error': [2], + 'use-isnan': [2], + 'valid-typeof': [2, {requireStringLiterals: true}], + 'vars-on-top': [0], + 'wc/attach-shadow-constructor': [2], + 'wc/define-tag-after-class-definition': [0], + 'wc/expose-class-on-global': [0], + 'wc/file-name-matches-element': [2], + 'wc/guard-define-call': [0], + 'wc/guard-super-call': [2], + 'wc/max-elements-per-file': [0], + 'wc/no-child-traversal-in-attributechangedcallback': [2], + 'wc/no-child-traversal-in-connectedcallback': [2], + 'wc/no-closed-shadow-root': [2], + 'wc/no-constructor-attributes': [2], + 'wc/no-constructor-params': [2], + 'wc/no-constructor': [2], + 'wc/no-customized-built-in-elements': [2], + 'wc/no-exports-with-element': [0], + 'wc/no-invalid-element-name': [2], + 'wc/no-invalid-extends': [2], + 'wc/no-method-prefixed-with-on': [2], + 'wc/no-self-class': [2], + 'wc/no-typos': [2], + 'wc/require-listener-teardown': [2], + 'wc/tag-name-matches-class': [2], + 'yoda': [2, 'never'], + }, +}; diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 0dd9a6687d8..00000000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,967 +0,0 @@ -root: true -reportUnusedDisableDirectives: true - -ignorePatterns: - - /web_src/js/vendor - - /web_src/fomantic - - /public/assets/js - -parser: "@typescript-eslint/parser" - -parserOptions: - sourceType: module - ecmaVersion: latest - project: true - extraFileExtensions: [".vue"] - parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser - -settings: - import-x/extensions: [".js", ".ts"] - import-x/parsers: - "@typescript-eslint/parser": [".js", ".ts"] - import-x/resolver: - typescript: true - -plugins: - - "@eslint-community/eslint-plugin-eslint-comments" - - "@stylistic/eslint-plugin-js" - - "@typescript-eslint/eslint-plugin" - - eslint-plugin-array-func - - eslint-plugin-github - - eslint-plugin-import-x - - eslint-plugin-no-jquery - - eslint-plugin-no-use-extend-native - - eslint-plugin-regexp - - eslint-plugin-sonarjs - - eslint-plugin-unicorn - - eslint-plugin-vitest - - eslint-plugin-vitest-globals - - eslint-plugin-wc - -env: - es2024: true - node: true - -overrides: - - files: ["web_src/**/*"] - globals: - __webpack_public_path__: true - process: false # https://github.com/webpack/webpack/issues/15833 - - files: ["web_src/**/*", "docs/**/*"] - env: - browser: true - node: false - - files: ["web_src/**/*worker.*"] - env: - worker: true - rules: - no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] - - files: ["*.config.*"] - rules: - import-x/no-unused-modules: [0] - - files: ["**/*.d.ts"] - rules: - import-x/no-unused-modules: [0] - "@typescript-eslint/consistent-type-definitions": [0] - "@typescript-eslint/consistent-type-imports": [0] - - files: ["web_src/js/types.ts"] - rules: - import-x/no-unused-modules: [0] - - files: ["**/*.test.*", "web_src/js/test/setup.ts"] - env: - vitest-globals/env: true - rules: - vitest/consistent-test-filename: [0] - vitest/consistent-test-it: [0] - vitest/expect-expect: [0] - vitest/max-expects: [0] - vitest/max-nested-describe: [0] - vitest/no-alias-methods: [0] - vitest/no-commented-out-tests: [0] - vitest/no-conditional-expect: [0] - vitest/no-conditional-in-test: [0] - vitest/no-conditional-tests: [0] - vitest/no-disabled-tests: [0] - vitest/no-done-callback: [0] - vitest/no-duplicate-hooks: [0] - vitest/no-focused-tests: [0] - vitest/no-hooks: [0] - vitest/no-identical-title: [2] - vitest/no-interpolation-in-snapshots: [0] - vitest/no-large-snapshots: [0] - vitest/no-mocks-import: [0] - vitest/no-restricted-matchers: [0] - vitest/no-restricted-vi-methods: [0] - vitest/no-standalone-expect: [0] - vitest/no-test-prefixes: [0] - vitest/no-test-return-statement: [0] - vitest/prefer-called-with: [0] - vitest/prefer-comparison-matcher: [0] - vitest/prefer-each: [0] - vitest/prefer-equality-matcher: [0] - vitest/prefer-expect-resolves: [0] - vitest/prefer-hooks-in-order: [0] - vitest/prefer-hooks-on-top: [2] - vitest/prefer-lowercase-title: [0] - vitest/prefer-mock-promise-shorthand: [0] - vitest/prefer-snapshot-hint: [0] - vitest/prefer-spy-on: [0] - vitest/prefer-strict-equal: [0] - vitest/prefer-to-be: [0] - vitest/prefer-to-be-falsy: [0] - vitest/prefer-to-be-object: [0] - vitest/prefer-to-be-truthy: [0] - vitest/prefer-to-contain: [0] - vitest/prefer-to-have-length: [0] - vitest/prefer-todo: [0] - vitest/require-hook: [0] - vitest/require-to-throw-message: [0] - vitest/require-top-level-describe: [0] - vitest/valid-describe-callback: [2] - vitest/valid-expect: [2] - vitest/valid-title: [2] - - files: ["web_src/js/modules/fetch.ts", "web_src/js/standalone/**/*"] - rules: - no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression] - - files: ["**/*.vue"] - plugins: - - eslint-plugin-vue - - eslint-plugin-vue-scoped-css - extends: - - plugin:vue/vue3-recommended - - plugin:vue-scoped-css/vue3-recommended - rules: - vue/attributes-order: [0] - vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] - vue/max-attributes-per-line: [0] - vue/singleline-html-element-content-newline: [0] - - files: ["tests/e2e/**"] - plugins: - - eslint-plugin-playwright - extends: plugin:playwright/recommended - -rules: - "@eslint-community/eslint-comments/disable-enable-pair": [2] - "@eslint-community/eslint-comments/no-aggregating-enable": [2] - "@eslint-community/eslint-comments/no-duplicate-disable": [2] - "@eslint-community/eslint-comments/no-restricted-disable": [0] - "@eslint-community/eslint-comments/no-unlimited-disable": [2] - "@eslint-community/eslint-comments/no-unused-disable": [2] - "@eslint-community/eslint-comments/no-unused-enable": [2] - "@eslint-community/eslint-comments/no-use": [0] - "@eslint-community/eslint-comments/require-description": [0] - "@stylistic/js/array-bracket-newline": [0] - "@stylistic/js/array-bracket-spacing": [2, never] - "@stylistic/js/array-element-newline": [0] - "@stylistic/js/arrow-parens": [2, always] - "@stylistic/js/arrow-spacing": [2, {before: true, after: true}] - "@stylistic/js/block-spacing": [0] - "@stylistic/js/brace-style": [2, 1tbs, {allowSingleLine: true}] - "@stylistic/js/comma-dangle": [2, always-multiline] - "@stylistic/js/comma-spacing": [2, {before: false, after: true}] - "@stylistic/js/comma-style": [2, last] - "@stylistic/js/computed-property-spacing": [2, never] - "@stylistic/js/dot-location": [2, property] - "@stylistic/js/eol-last": [2] - "@stylistic/js/function-call-argument-newline": [0] - "@stylistic/js/function-call-spacing": [2, never] - "@stylistic/js/function-paren-newline": [0] - "@stylistic/js/generator-star-spacing": [0] - "@stylistic/js/implicit-arrow-linebreak": [0] - "@stylistic/js/indent": [2, 2, {ignoreComments: true, SwitchCase: 1}] - "@stylistic/js/key-spacing": [2] - "@stylistic/js/keyword-spacing": [2] - "@stylistic/js/line-comment-position": [0] - "@stylistic/js/linebreak-style": [2, unix] - "@stylistic/js/lines-around-comment": [0] - "@stylistic/js/lines-between-class-members": [0] - "@stylistic/js/max-len": [0] - "@stylistic/js/max-statements-per-line": [0] - "@stylistic/js/multiline-comment-style": [0] - "@stylistic/js/multiline-ternary": [0] - "@stylistic/js/new-parens": [2] - "@stylistic/js/newline-per-chained-call": [0] - "@stylistic/js/no-confusing-arrow": [0] - "@stylistic/js/no-extra-parens": [0] - "@stylistic/js/no-extra-semi": [2] - "@stylistic/js/no-floating-decimal": [0] - "@stylistic/js/no-mixed-operators": [0] - "@stylistic/js/no-mixed-spaces-and-tabs": [2] - "@stylistic/js/no-multi-spaces": [2, {ignoreEOLComments: true, exceptions: {Property: true}}] - "@stylistic/js/no-multiple-empty-lines": [2, {max: 1, maxEOF: 0, maxBOF: 0}] - "@stylistic/js/no-tabs": [2] - "@stylistic/js/no-trailing-spaces": [2] - "@stylistic/js/no-whitespace-before-property": [2] - "@stylistic/js/nonblock-statement-body-position": [2] - "@stylistic/js/object-curly-newline": [0] - "@stylistic/js/object-curly-spacing": [2, never] - "@stylistic/js/object-property-newline": [0] - "@stylistic/js/one-var-declaration-per-line": [0] - "@stylistic/js/operator-linebreak": [2, after] - "@stylistic/js/padded-blocks": [2, never] - "@stylistic/js/padding-line-between-statements": [0] - "@stylistic/js/quote-props": [0] - "@stylistic/js/quotes": [2, single, {avoidEscape: true, allowTemplateLiterals: true}] - "@stylistic/js/rest-spread-spacing": [2, never] - "@stylistic/js/semi": [2, always, {omitLastInOneLineBlock: true}] - "@stylistic/js/semi-spacing": [2, {before: false, after: true}] - "@stylistic/js/semi-style": [2, last] - "@stylistic/js/space-before-blocks": [2, always] - "@stylistic/js/space-before-function-paren": [2, {anonymous: ignore, named: never, asyncArrow: always}] - "@stylistic/js/space-in-parens": [2, never] - "@stylistic/js/space-infix-ops": [2] - "@stylistic/js/space-unary-ops": [2] - "@stylistic/js/spaced-comment": [2, always] - "@stylistic/js/switch-colon-spacing": [2] - "@stylistic/js/template-curly-spacing": [2, never] - "@stylistic/js/template-tag-spacing": [2, never] - "@stylistic/js/wrap-iife": [2, inside] - "@stylistic/js/wrap-regex": [0] - "@stylistic/js/yield-star-spacing": [2, after] - "@typescript-eslint/adjacent-overload-signatures": [0] - "@typescript-eslint/array-type": [0] - "@typescript-eslint/await-thenable": [2] - "@typescript-eslint/ban-ts-comment": [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}] - "@typescript-eslint/ban-tslint-comment": [0] - "@typescript-eslint/class-literal-property-style": [0] - "@typescript-eslint/class-methods-use-this": [0] - "@typescript-eslint/consistent-generic-constructors": [0] - "@typescript-eslint/consistent-indexed-object-style": [0] - "@typescript-eslint/consistent-return": [0] - "@typescript-eslint/consistent-type-assertions": [2, {assertionStyle: as, objectLiteralTypeAssertions: allow}] - "@typescript-eslint/consistent-type-definitions": [2, type] - "@typescript-eslint/consistent-type-exports": [2, {fixMixedExportsWithInlineTypeSpecifier: false}] - "@typescript-eslint/consistent-type-imports": [2, {prefer: type-imports, fixStyle: separate-type-imports, disallowTypeAnnotations: true}] - "@typescript-eslint/default-param-last": [0] - "@typescript-eslint/dot-notation": [0] - "@typescript-eslint/explicit-function-return-type": [0] - "@typescript-eslint/explicit-member-accessibility": [0] - "@typescript-eslint/explicit-module-boundary-types": [0] - "@typescript-eslint/init-declarations": [0] - "@typescript-eslint/max-params": [0] - "@typescript-eslint/member-ordering": [0] - "@typescript-eslint/method-signature-style": [0] - "@typescript-eslint/naming-convention": [0] - "@typescript-eslint/no-array-constructor": [2] - "@typescript-eslint/no-array-delete": [2] - "@typescript-eslint/no-base-to-string": [0] - "@typescript-eslint/no-confusing-non-null-assertion": [2] - "@typescript-eslint/no-confusing-void-expression": [0] - "@typescript-eslint/no-deprecated": [2] - "@typescript-eslint/no-dupe-class-members": [0] - "@typescript-eslint/no-duplicate-enum-values": [2] - "@typescript-eslint/no-duplicate-type-constituents": [2, {ignoreUnions: true}] - "@typescript-eslint/no-dynamic-delete": [0] - "@typescript-eslint/no-empty-function": [0] - "@typescript-eslint/no-empty-interface": [0] - "@typescript-eslint/no-empty-object-type": [2] - "@typescript-eslint/no-explicit-any": [0] - "@typescript-eslint/no-extra-non-null-assertion": [2] - "@typescript-eslint/no-extraneous-class": [0] - "@typescript-eslint/no-floating-promises": [0] - "@typescript-eslint/no-for-in-array": [2] - "@typescript-eslint/no-implied-eval": [2] - "@typescript-eslint/no-import-type-side-effects": [0] # dupe with consistent-type-imports - "@typescript-eslint/no-inferrable-types": [0] - "@typescript-eslint/no-invalid-this": [0] - "@typescript-eslint/no-invalid-void-type": [0] - "@typescript-eslint/no-loop-func": [0] - "@typescript-eslint/no-loss-of-precision": [0] - "@typescript-eslint/no-magic-numbers": [0] - "@typescript-eslint/no-meaningless-void-operator": [0] - "@typescript-eslint/no-misused-new": [2] - "@typescript-eslint/no-misused-promises": [2, {checksVoidReturn: {attributes: false, arguments: false}}] - "@typescript-eslint/no-mixed-enums": [0] - "@typescript-eslint/no-namespace": [2] - "@typescript-eslint/no-non-null-asserted-nullish-coalescing": [0] - "@typescript-eslint/no-non-null-asserted-optional-chain": [2] - "@typescript-eslint/no-non-null-assertion": [0] - "@typescript-eslint/no-redeclare": [0] - "@typescript-eslint/no-redundant-type-constituents": [2] - "@typescript-eslint/no-require-imports": [2] - "@typescript-eslint/no-restricted-imports": [0] - "@typescript-eslint/no-restricted-types": [0] - "@typescript-eslint/no-shadow": [0] - "@typescript-eslint/no-this-alias": [0] # handled by unicorn/no-this-assignment - "@typescript-eslint/no-unnecessary-boolean-literal-compare": [0] - "@typescript-eslint/no-unnecessary-condition": [0] - "@typescript-eslint/no-unnecessary-qualifier": [0] - "@typescript-eslint/no-unnecessary-template-expression": [0] - "@typescript-eslint/no-unnecessary-type-arguments": [0] - "@typescript-eslint/no-unnecessary-type-assertion": [2] - "@typescript-eslint/no-unnecessary-type-constraint": [2] - "@typescript-eslint/no-unsafe-argument": [0] - "@typescript-eslint/no-unsafe-assignment": [0] - "@typescript-eslint/no-unsafe-call": [0] - "@typescript-eslint/no-unsafe-declaration-merging": [2] - "@typescript-eslint/no-unsafe-enum-comparison": [2] - "@typescript-eslint/no-unsafe-function-type": [2] - "@typescript-eslint/no-unsafe-member-access": [0] - "@typescript-eslint/no-unsafe-return": [0] - "@typescript-eslint/no-unsafe-unary-minus": [2] - "@typescript-eslint/no-unused-expressions": [0] - "@typescript-eslint/no-unused-vars": [2, {vars: all, args: all, caughtErrors: all, ignoreRestSiblings: false, argsIgnorePattern: ^_, varsIgnorePattern: ^_, caughtErrorsIgnorePattern: ^_, destructuredArrayIgnorePattern: ^_}] - "@typescript-eslint/no-use-before-define": [0] - "@typescript-eslint/no-useless-constructor": [0] - "@typescript-eslint/no-useless-empty-export": [0] - "@typescript-eslint/no-wrapper-object-types": [2] - "@typescript-eslint/non-nullable-type-assertion-style": [0] - "@typescript-eslint/only-throw-error": [2] - "@typescript-eslint/parameter-properties": [0] - "@typescript-eslint/prefer-as-const": [2] - "@typescript-eslint/prefer-destructuring": [0] - "@typescript-eslint/prefer-enum-initializers": [0] - "@typescript-eslint/prefer-find": [2] - "@typescript-eslint/prefer-for-of": [2] - "@typescript-eslint/prefer-function-type": [2] - "@typescript-eslint/prefer-includes": [2] - "@typescript-eslint/prefer-literal-enum-member": [0] - "@typescript-eslint/prefer-namespace-keyword": [0] - "@typescript-eslint/prefer-nullish-coalescing": [0] - "@typescript-eslint/prefer-optional-chain": [2, {requireNullish: true}] - "@typescript-eslint/prefer-promise-reject-errors": [0] - "@typescript-eslint/prefer-readonly": [0] - "@typescript-eslint/prefer-readonly-parameter-types": [0] - "@typescript-eslint/prefer-reduce-type-parameter": [0] - "@typescript-eslint/prefer-regexp-exec": [0] - "@typescript-eslint/prefer-return-this-type": [0] - "@typescript-eslint/prefer-string-starts-ends-with": [2, {allowSingleElementEquality: always}] - "@typescript-eslint/promise-function-async": [0] - "@typescript-eslint/require-array-sort-compare": [0] - "@typescript-eslint/require-await": [0] - "@typescript-eslint/restrict-plus-operands": [2] - "@typescript-eslint/restrict-template-expressions": [0] - "@typescript-eslint/return-await": [0] - "@typescript-eslint/strict-boolean-expressions": [0] - "@typescript-eslint/switch-exhaustiveness-check": [0] - "@typescript-eslint/triple-slash-reference": [2] - "@typescript-eslint/typedef": [0] - "@typescript-eslint/unbound-method": [0] # too many false-positives - "@typescript-eslint/unified-signatures": [2] - accessor-pairs: [2] - array-callback-return: [2, {checkForEach: true}] - array-func/avoid-reverse: [2] - array-func/from-map: [2] - array-func/no-unnecessary-this-arg: [2] - array-func/prefer-array-from: [2] - array-func/prefer-flat-map: [0] # handled by unicorn/prefer-array-flat-map - array-func/prefer-flat: [0] # handled by unicorn/prefer-array-flat - arrow-body-style: [0] - block-scoped-var: [2] - camelcase: [0] - capitalized-comments: [0] - class-methods-use-this: [0] - complexity: [0] - consistent-return: [0] - consistent-this: [0] - constructor-super: [2] - curly: [0] - default-case-last: [2] - default-case: [0] - default-param-last: [0] - dot-notation: [0] - eqeqeq: [2] - for-direction: [2] - func-name-matching: [2] - func-names: [0] - func-style: [0] - getter-return: [2] - github/a11y-aria-label-is-well-formatted: [0] - github/a11y-no-title-attribute: [0] - github/a11y-no-visually-hidden-interactive-element: [0] - github/a11y-role-supports-aria-props: [0] - github/a11y-svg-has-accessible-name: [0] - github/array-foreach: [0] - github/async-currenttarget: [2] - github/async-preventdefault: [2] - github/authenticity-token: [0] - github/get-attribute: [0] - github/js-class-name: [0] - github/no-blur: [0] - github/no-d-none: [0] - github/no-dataset: [2] - github/no-dynamic-script-tag: [2] - github/no-implicit-buggy-globals: [2] - github/no-inner-html: [0] - github/no-innerText: [2] - github/no-then: [2] - github/no-useless-passive: [2] - github/prefer-observers: [2] - github/require-passive-events: [2] - github/unescaped-html-literal: [0] - grouped-accessor-pairs: [2] - guard-for-in: [0] - id-blacklist: [0] - id-length: [0] - id-match: [0] - import-x/consistent-type-specifier-style: [0] - import-x/default: [0] - import-x/dynamic-import-chunkname: [0] - import-x/export: [2] - import-x/exports-last: [0] - import-x/extensions: [2, always, {ignorePackages: true}] - import-x/first: [2] - import-x/group-exports: [0] - import-x/max-dependencies: [0] - import-x/named: [2] - import-x/namespace: [0] - import-x/newline-after-import: [0] - import-x/no-absolute-path: [0] - import-x/no-amd: [2] - import-x/no-anonymous-default-export: [0] - import-x/no-commonjs: [2] - import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] - import-x/no-default-export: [0] - import-x/no-deprecated: [0] - import-x/no-dynamic-require: [0] - import-x/no-empty-named-blocks: [2] - import-x/no-extraneous-dependencies: [2] - import-x/no-import-module-exports: [0] - import-x/no-internal-modules: [0] - import-x/no-mutable-exports: [0] - import-x/no-named-as-default-member: [0] - import-x/no-named-as-default: [0] - import-x/no-named-default: [0] - import-x/no-named-export: [0] - import-x/no-namespace: [0] - import-x/no-nodejs-modules: [0] - import-x/no-relative-packages: [0] - import-x/no-relative-parent-imports: [0] - import-x/no-restricted-paths: [0] - import-x/no-self-import: [2] - import-x/no-unassigned-import: [0] - import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] - import-x/no-unused-modules: [2, {unusedExports: true}] - import-x/no-useless-path-segments: [2, {commonjs: true}] - import-x/no-webpack-loader-syntax: [2] - import-x/order: [0] - import-x/prefer-default-export: [0] - import-x/unambiguous: [0] - init-declarations: [0] - line-comment-position: [0] - logical-assignment-operators: [0] - max-classes-per-file: [0] - max-depth: [0] - max-lines-per-function: [0] - max-lines: [0] - max-nested-callbacks: [0] - max-params: [0] - max-statements: [0] - multiline-comment-style: [2, separate-lines] - new-cap: [0] - no-alert: [0] - no-array-constructor: [0] # handled by @typescript-eslint/no-array-constructor - no-async-promise-executor: [0] - no-await-in-loop: [0] - no-bitwise: [0] - no-buffer-constructor: [0] - no-caller: [2] - no-case-declarations: [2] - no-class-assign: [2] - no-compare-neg-zero: [2] - no-cond-assign: [2, except-parens] - no-console: [1, {allow: [debug, info, warn, error]}] - no-const-assign: [2] - no-constant-binary-expression: [2] - no-constant-condition: [0] - no-constructor-return: [2] - no-continue: [0] - no-control-regex: [0] - no-debugger: [1] - no-delete-var: [2] - no-div-regex: [0] - no-dupe-args: [2] - no-dupe-class-members: [2] - no-dupe-else-if: [2] - no-dupe-keys: [2] - no-duplicate-case: [2] - no-duplicate-imports: [0] - no-else-return: [2] - no-empty-character-class: [2] - no-empty-function: [0] - no-empty-pattern: [2] - no-empty-static-block: [2] - no-empty: [2, {allowEmptyCatch: true}] - no-eq-null: [2] - no-eval: [2] - no-ex-assign: [2] - no-extend-native: [2] - no-extra-bind: [2] - no-extra-boolean-cast: [2] - no-extra-label: [0] - no-fallthrough: [2] - no-func-assign: [2] - no-global-assign: [2] - no-implicit-coercion: [2] - no-implicit-globals: [0] - no-implied-eval: [0] # handled by @typescript-eslint/no-implied-eval - no-import-assign: [2] - no-inline-comments: [0] - no-inner-declarations: [2] - no-invalid-regexp: [2] - no-invalid-this: [0] - no-irregular-whitespace: [2] - no-iterator: [2] - no-jquery/no-ajax-events: [2] - no-jquery/no-ajax: [2] - no-jquery/no-and-self: [2] - no-jquery/no-animate-toggle: [2] - no-jquery/no-animate: [2] - no-jquery/no-append-html: [2] - no-jquery/no-attr: [2] - no-jquery/no-bind: [2] - no-jquery/no-box-model: [2] - no-jquery/no-browser: [2] - no-jquery/no-camel-case: [2] - no-jquery/no-class-state: [2] - no-jquery/no-class: [0] - no-jquery/no-clone: [2] - no-jquery/no-closest: [0] - no-jquery/no-constructor-attributes: [2] - no-jquery/no-contains: [2] - no-jquery/no-context-prop: [2] - no-jquery/no-css: [2] - no-jquery/no-data: [0] - no-jquery/no-deferred: [2] - no-jquery/no-delegate: [2] - no-jquery/no-done-fail: [2] - no-jquery/no-each-collection: [0] - no-jquery/no-each-util: [0] - no-jquery/no-each: [0] - no-jquery/no-error-shorthand: [2] - no-jquery/no-error: [2] - no-jquery/no-escape-selector: [2] - no-jquery/no-event-shorthand: [2] - no-jquery/no-extend: [2] - no-jquery/no-fade: [2] - no-jquery/no-filter: [0] - no-jquery/no-find-collection: [0] - no-jquery/no-find-util: [2] - no-jquery/no-find: [0] - no-jquery/no-fx-interval: [2] - no-jquery/no-fx: [2] - no-jquery/no-global-eval: [2] - no-jquery/no-global-selector: [0] - no-jquery/no-grep: [2] - no-jquery/no-has: [2] - no-jquery/no-hold-ready: [2] - no-jquery/no-html: [0] - no-jquery/no-in-array: [2] - no-jquery/no-is-array: [2] - no-jquery/no-is-empty-object: [2] - no-jquery/no-is-function: [2] - no-jquery/no-is-numeric: [2] - no-jquery/no-is-plain-object: [2] - no-jquery/no-is-window: [2] - no-jquery/no-is: [2] - no-jquery/no-jquery-constructor: [0] - no-jquery/no-live: [2] - no-jquery/no-load-shorthand: [2] - no-jquery/no-load: [2] - no-jquery/no-map-collection: [0] - no-jquery/no-map-util: [2] - no-jquery/no-map: [2] - no-jquery/no-merge: [2] - no-jquery/no-node-name: [2] - no-jquery/no-noop: [2] - no-jquery/no-now: [2] - no-jquery/no-on-ready: [2] - no-jquery/no-other-methods: [0] - no-jquery/no-other-utils: [2] - no-jquery/no-param: [2] - no-jquery/no-parent: [0] - no-jquery/no-parents: [2] - no-jquery/no-parse-html-literal: [2] - no-jquery/no-parse-html: [2] - no-jquery/no-parse-json: [2] - no-jquery/no-parse-xml: [2] - no-jquery/no-prop: [2] - no-jquery/no-proxy: [2] - no-jquery/no-ready-shorthand: [2] - no-jquery/no-ready: [2] - no-jquery/no-selector-prop: [2] - no-jquery/no-serialize: [2] - no-jquery/no-size: [2] - no-jquery/no-sizzle: [2] - no-jquery/no-slide: [2] - no-jquery/no-sub: [2] - no-jquery/no-support: [2] - no-jquery/no-text: [2] - no-jquery/no-trigger: [0] - no-jquery/no-trim: [2] - no-jquery/no-type: [2] - no-jquery/no-unique: [2] - no-jquery/no-unload-shorthand: [2] - no-jquery/no-val: [0] - no-jquery/no-visibility: [2] - no-jquery/no-when: [2] - no-jquery/no-wrap: [2] - no-jquery/variable-pattern: [2] - no-label-var: [2] - no-labels: [0] # handled by no-restricted-syntax - no-lone-blocks: [2] - no-lonely-if: [0] - no-loop-func: [0] - no-loss-of-precision: [2] - no-magic-numbers: [0] - no-misleading-character-class: [2] - no-multi-assign: [0] - no-multi-str: [2] - no-negated-condition: [0] - no-nested-ternary: [0] - no-new-func: [2] - no-new-native-nonconstructor: [2] - no-new-object: [2] - no-new-symbol: [2] - no-new-wrappers: [2] - no-new: [0] - no-nonoctal-decimal-escape: [2] - no-obj-calls: [2] - no-octal-escape: [2] - no-octal: [2] - no-param-reassign: [0] - no-plusplus: [0] - no-promise-executor-return: [0] - no-proto: [2] - no-prototype-builtins: [2] - no-redeclare: [0] # must be disabled for typescript overloads - no-regex-spaces: [2] - no-restricted-exports: [0] - no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename] - no-restricted-imports: [0] - no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.ts instead"}] - no-return-assign: [0] - no-script-url: [2] - no-self-assign: [2, {props: true}] - no-self-compare: [2] - no-sequences: [2] - no-setter-return: [2] - no-shadow-restricted-names: [2] - no-shadow: [0] - no-sparse-arrays: [2] - no-template-curly-in-string: [2] - no-ternary: [0] - no-this-before-super: [2] - no-throw-literal: [2] - no-undef-init: [2] - no-undef: [2, {typeof: true}] # TODO: disable this rule after tsc passes - no-undefined: [0] - no-underscore-dangle: [0] - no-unexpected-multiline: [2] - no-unmodified-loop-condition: [2] - no-unneeded-ternary: [2] - no-unreachable-loop: [2] - no-unreachable: [2] - no-unsafe-finally: [2] - no-unsafe-negation: [2] - no-unused-expressions: [2] - no-unused-labels: [2] - no-unused-private-class-members: [2] - no-unused-vars: [0] # handled by @typescript-eslint/no-unused-vars - no-use-before-define: [2, {functions: false, classes: true, variables: true, allowNamedExports: true}] - no-use-extend-native/no-use-extend-native: [2] - no-useless-backreference: [2] - no-useless-call: [2] - no-useless-catch: [2] - no-useless-computed-key: [2] - no-useless-concat: [2] - no-useless-constructor: [2] - no-useless-escape: [2] - no-useless-rename: [2] - no-useless-return: [2] - no-var: [2] - no-void: [2] - no-warning-comments: [0] - no-with: [0] # handled by no-restricted-syntax - object-shorthand: [2, always] - one-var-declaration-per-line: [0] - one-var: [0] - operator-assignment: [2, always] - operator-linebreak: [2, after] - prefer-arrow-callback: [2, {allowNamedFunctions: true, allowUnboundThis: true}] - prefer-const: [2, {destructuring: all, ignoreReadBeforeAssign: true}] - prefer-destructuring: [0] - prefer-exponentiation-operator: [2] - prefer-named-capture-group: [0] - prefer-numeric-literals: [2] - prefer-object-has-own: [2] - prefer-object-spread: [2] - prefer-promise-reject-errors: [2, {allowEmptyReject: false}] - prefer-regex-literals: [2] - prefer-rest-params: [2] - prefer-spread: [2] - prefer-template: [2] - radix: [2, as-needed] - regexp/confusing-quantifier: [2] - regexp/control-character-escape: [2] - regexp/hexadecimal-escape: [0] - regexp/letter-case: [0] - regexp/match-any: [2] - regexp/negation: [2] - regexp/no-contradiction-with-assertion: [0] - regexp/no-control-character: [0] - regexp/no-dupe-characters-character-class: [2] - regexp/no-dupe-disjunctions: [2] - regexp/no-empty-alternative: [2] - regexp/no-empty-capturing-group: [2] - regexp/no-empty-character-class: [0] - regexp/no-empty-group: [2] - regexp/no-empty-lookarounds-assertion: [2] - regexp/no-empty-string-literal: [2] - regexp/no-escape-backspace: [2] - regexp/no-extra-lookaround-assertions: [0] - regexp/no-invalid-regexp: [2] - regexp/no-invisible-character: [2] - regexp/no-lazy-ends: [2] - regexp/no-legacy-features: [2] - regexp/no-misleading-capturing-group: [0] - regexp/no-misleading-unicode-character: [0] - regexp/no-missing-g-flag: [2] - regexp/no-non-standard-flag: [2] - regexp/no-obscure-range: [2] - regexp/no-octal: [2] - regexp/no-optional-assertion: [2] - regexp/no-potentially-useless-backreference: [2] - regexp/no-standalone-backslash: [2] - regexp/no-super-linear-backtracking: [0] - regexp/no-super-linear-move: [0] - regexp/no-trivially-nested-assertion: [2] - regexp/no-trivially-nested-quantifier: [2] - regexp/no-unused-capturing-group: [0] - regexp/no-useless-assertions: [2] - regexp/no-useless-backreference: [2] - regexp/no-useless-character-class: [2] - regexp/no-useless-dollar-replacements: [2] - regexp/no-useless-escape: [2] - regexp/no-useless-flag: [2] - regexp/no-useless-lazy: [2] - regexp/no-useless-non-capturing-group: [2] - regexp/no-useless-quantifier: [2] - regexp/no-useless-range: [2] - regexp/no-useless-set-operand: [2] - regexp/no-useless-string-literal: [2] - regexp/no-useless-two-nums-quantifier: [2] - regexp/no-zero-quantifier: [2] - regexp/optimal-lookaround-quantifier: [2] - regexp/optimal-quantifier-concatenation: [0] - regexp/prefer-character-class: [0] - regexp/prefer-d: [0] - regexp/prefer-escape-replacement-dollar-char: [0] - regexp/prefer-lookaround: [0] - regexp/prefer-named-backreference: [0] - regexp/prefer-named-capture-group: [0] - regexp/prefer-named-replacement: [0] - regexp/prefer-plus-quantifier: [2] - regexp/prefer-predefined-assertion: [2] - regexp/prefer-quantifier: [0] - regexp/prefer-question-quantifier: [2] - regexp/prefer-range: [2] - regexp/prefer-regexp-exec: [2] - regexp/prefer-regexp-test: [2] - regexp/prefer-result-array-groups: [0] - regexp/prefer-set-operation: [2] - regexp/prefer-star-quantifier: [2] - regexp/prefer-unicode-codepoint-escapes: [2] - regexp/prefer-w: [0] - regexp/require-unicode-regexp: [0] - regexp/simplify-set-operations: [2] - regexp/sort-alternatives: [0] - regexp/sort-character-class-elements: [0] - regexp/sort-flags: [0] - regexp/strict: [2] - regexp/unicode-escape: [0] - regexp/use-ignore-case: [0] - require-atomic-updates: [0] - require-await: [0] # handled by @typescript-eslint/require-await - require-unicode-regexp: [0] - require-yield: [2] - sonarjs/cognitive-complexity: [0] - sonarjs/elseif-without-else: [0] - sonarjs/max-switch-cases: [0] - sonarjs/no-all-duplicated-branches: [2] - sonarjs/no-collapsible-if: [0] - sonarjs/no-collection-size-mischeck: [2] - sonarjs/no-duplicate-string: [0] - sonarjs/no-duplicated-branches: [0] - sonarjs/no-element-overwrite: [2] - sonarjs/no-empty-collection: [2] - sonarjs/no-extra-arguments: [2] - sonarjs/no-gratuitous-expressions: [2] - sonarjs/no-identical-conditions: [2] - sonarjs/no-identical-expressions: [2] - sonarjs/no-identical-functions: [2, 5] - sonarjs/no-ignored-return: [2] - sonarjs/no-inverted-boolean-check: [2] - sonarjs/no-nested-switch: [0] - sonarjs/no-nested-template-literals: [0] - sonarjs/no-one-iteration-loop: [2] - sonarjs/no-redundant-boolean: [2] - sonarjs/no-redundant-jump: [2] - sonarjs/no-same-line-conditional: [2] - sonarjs/no-small-switch: [0] - sonarjs/no-unused-collection: [2] - sonarjs/no-use-of-empty-return-value: [2] - sonarjs/no-useless-catch: [2] - sonarjs/non-existent-operator: [2] - sonarjs/prefer-immediate-return: [0] - sonarjs/prefer-object-literal: [0] - sonarjs/prefer-single-boolean-return: [0] - sonarjs/prefer-while: [2] - sort-imports: [0] - sort-keys: [0] - sort-vars: [0] - strict: [0] - symbol-description: [2] - unicode-bom: [2, never] - unicorn/better-regex: [0] - unicorn/catch-error-name: [0] - unicorn/consistent-destructuring: [2] - unicorn/consistent-empty-array-spread: [2] - unicorn/consistent-existence-index-check: [0] - unicorn/consistent-function-scoping: [0] - unicorn/custom-error-definition: [0] - unicorn/empty-brace-spaces: [2] - unicorn/error-message: [0] - unicorn/escape-case: [0] - unicorn/expiring-todo-comments: [0] - unicorn/explicit-length-check: [0] - unicorn/filename-case: [0] - unicorn/import-index: [0] - unicorn/import-style: [0] - unicorn/new-for-builtins: [2] - unicorn/no-abusive-eslint-disable: [0] - unicorn/no-anonymous-default-export: [0] - unicorn/no-array-callback-reference: [0] - unicorn/no-array-for-each: [2] - unicorn/no-array-method-this-argument: [2] - unicorn/no-array-push-push: [2] - unicorn/no-array-reduce: [2] - unicorn/no-await-expression-member: [0] - unicorn/no-await-in-promise-methods: [2] - unicorn/no-console-spaces: [0] - unicorn/no-document-cookie: [2] - unicorn/no-empty-file: [2] - unicorn/no-for-loop: [0] - unicorn/no-hex-escape: [0] - unicorn/no-instanceof-array: [0] - unicorn/no-invalid-fetch-options: [2] - unicorn/no-invalid-remove-event-listener: [2] - unicorn/no-keyword-prefix: [0] - unicorn/no-length-as-slice-end: [2] - unicorn/no-lonely-if: [2] - unicorn/no-magic-array-flat-depth: [0] - unicorn/no-negated-condition: [0] - unicorn/no-negation-in-equality-check: [2] - unicorn/no-nested-ternary: [0] - unicorn/no-new-array: [0] - unicorn/no-new-buffer: [0] - unicorn/no-null: [0] - unicorn/no-object-as-default-parameter: [0] - unicorn/no-process-exit: [0] - unicorn/no-single-promise-in-promise-methods: [2] - unicorn/no-static-only-class: [2] - unicorn/no-thenable: [2] - unicorn/no-this-assignment: [2] - unicorn/no-typeof-undefined: [2] - unicorn/no-unnecessary-await: [2] - unicorn/no-unnecessary-polyfills: [2] - unicorn/no-unreadable-array-destructuring: [0] - unicorn/no-unreadable-iife: [2] - unicorn/no-unused-properties: [2] - unicorn/no-useless-fallback-in-spread: [2] - unicorn/no-useless-length-check: [2] - unicorn/no-useless-promise-resolve-reject: [2] - unicorn/no-useless-spread: [2] - unicorn/no-useless-switch-case: [2] - unicorn/no-useless-undefined: [0] - unicorn/no-zero-fractions: [2] - unicorn/number-literal-case: [0] - unicorn/numeric-separators-style: [0] - unicorn/prefer-add-event-listener: [2] - unicorn/prefer-array-find: [2] - unicorn/prefer-array-flat-map: [2] - unicorn/prefer-array-flat: [2] - unicorn/prefer-array-index-of: [2] - unicorn/prefer-array-some: [2] - unicorn/prefer-at: [0] - unicorn/prefer-blob-reading-methods: [2] - unicorn/prefer-code-point: [0] - unicorn/prefer-date-now: [2] - unicorn/prefer-default-parameters: [0] - unicorn/prefer-dom-node-append: [2] - unicorn/prefer-dom-node-dataset: [0] - unicorn/prefer-dom-node-remove: [2] - unicorn/prefer-dom-node-text-content: [2] - unicorn/prefer-event-target: [2] - unicorn/prefer-export-from: [0] - unicorn/prefer-global-this: [0] - unicorn/prefer-includes: [2] - unicorn/prefer-json-parse-buffer: [0] - unicorn/prefer-keyboard-event-key: [2] - unicorn/prefer-logical-operator-over-ternary: [2] - unicorn/prefer-math-min-max: [2] - unicorn/prefer-math-trunc: [2] - unicorn/prefer-modern-dom-apis: [0] - unicorn/prefer-modern-math-apis: [2] - unicorn/prefer-module: [2] - unicorn/prefer-native-coercion-functions: [2] - unicorn/prefer-negative-index: [2] - unicorn/prefer-node-protocol: [2] - unicorn/prefer-number-properties: [0] - unicorn/prefer-object-from-entries: [2] - unicorn/prefer-object-has-own: [0] - unicorn/prefer-optional-catch-binding: [2] - unicorn/prefer-prototype-methods: [0] - unicorn/prefer-query-selector: [2] - unicorn/prefer-reflect-apply: [0] - unicorn/prefer-regexp-test: [2] - unicorn/prefer-set-has: [0] - unicorn/prefer-set-size: [2] - unicorn/prefer-spread: [0] - unicorn/prefer-string-raw: [0] - unicorn/prefer-string-replace-all: [0] - unicorn/prefer-string-slice: [0] - unicorn/prefer-string-starts-ends-with: [2] - unicorn/prefer-string-trim-start-end: [2] - unicorn/prefer-structured-clone: [2] - unicorn/prefer-switch: [0] - unicorn/prefer-ternary: [0] - unicorn/prefer-text-content: [2] - unicorn/prefer-top-level-await: [0] - unicorn/prefer-type-error: [0] - unicorn/prevent-abbreviations: [0] - unicorn/relative-url-style: [2] - unicorn/require-array-join-separator: [2] - unicorn/require-number-to-fixed-digits-argument: [2] - unicorn/require-post-message-target-origin: [0] - unicorn/string-content: [0] - unicorn/switch-case-braces: [0] - unicorn/template-indent: [2] - unicorn/text-encoding-identifier-case: [0] - unicorn/throw-new-error: [2] - use-isnan: [2] - valid-typeof: [2, {requireStringLiterals: true}] - vars-on-top: [0] - wc/attach-shadow-constructor: [2] - wc/define-tag-after-class-definition: [0] - wc/expose-class-on-global: [0] - wc/file-name-matches-element: [2] - wc/guard-define-call: [0] - wc/guard-super-call: [2] - wc/max-elements-per-file: [0] - wc/no-child-traversal-in-attributechangedcallback: [2] - wc/no-child-traversal-in-connectedcallback: [2] - wc/no-closed-shadow-root: [2] - wc/no-constructor-attributes: [2] - wc/no-constructor-params: [2] - wc/no-constructor: [2] - wc/no-customized-built-in-elements: [2] - wc/no-exports-with-element: [0] - wc/no-invalid-element-name: [2] - wc/no-invalid-extends: [2] - wc/no-method-prefixed-with-on: [2] - wc/no-self-class: [2] - wc/no-typos: [2] - wc/require-listener-teardown: [2] - wc/tag-name-matches-class: [2] - yoda: [2, never] From b9457422934f33a37e0285d100007aae12e87ff7 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Wed, 18 Dec 2024 04:08:04 +0100 Subject: [PATCH 32/49] Change pprof labels to be prometheus compatible (#32865) Enables scrapping pprof endpoint for continuous profiling Closes: https://github.com/go-gitea/gitea/issues/32854 --- modules/graceful/manager.go | 6 +++--- modules/graceful/manager_common.go | 14 ++++++++++---- modules/process/context.go | 2 +- modules/process/manager.go | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 991b2f2b7af..cac6ce62d63 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -136,7 +136,7 @@ func (g *Manager) doShutdown() { } g.lock.Lock() g.shutdownCtxCancel() - atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "post-shutdown")) + atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels(LifecyclePProfLabel, "post-shutdown")) pprof.SetGoroutineLabels(atShutdownCtx) for _, fn := range g.toRunAtShutdown { go fn() @@ -167,7 +167,7 @@ func (g *Manager) doHammerTime(d time.Duration) { default: log.Warn("Setting Hammer condition") g.hammerCtxCancel() - atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "post-hammer")) + atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels(LifecyclePProfLabel, "post-hammer")) pprof.SetGoroutineLabels(atHammerCtx) } g.lock.Unlock() @@ -183,7 +183,7 @@ func (g *Manager) doTerminate() { default: log.Warn("Terminating") g.terminateCtxCancel() - atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "post-terminate")) + atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels(LifecyclePProfLabel, "post-terminate")) pprof.SetGoroutineLabels(atTerminateCtx) for _, fn := range g.toRunAtTerminate { diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go index f6dbcc748dc..15dd4406d53 100644 --- a/modules/graceful/manager_common.go +++ b/modules/graceful/manager_common.go @@ -22,6 +22,12 @@ const ( watchdogMsg systemdNotifyMsg = "WATCHDOG=1" ) +// LifecyclePProfLabel is a label marking manager lifecycle phase +// Making it compliant with prometheus key regex https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels +// would enable someone interested to be able to to continuously gather profiles into pyroscope. +// Other labels for pprof (in "modules/process" package) should also follow this rule. +const LifecyclePProfLabel = "graceful_lifecycle" + func statusMsg(msg string) systemdNotifyMsg { return systemdNotifyMsg("STATUS=" + msg) } @@ -65,10 +71,10 @@ func (g *Manager) prepare(ctx context.Context) { g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx) g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx) - g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "with-terminate")) - g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("graceful-lifecycle", "with-shutdown")) - g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "with-hammer")) - g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "with-manager")) + g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels(LifecyclePProfLabel, "with-terminate")) + g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels(LifecyclePProfLabel, "with-shutdown")) + g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels(LifecyclePProfLabel, "with-hammer")) + g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels(LifecyclePProfLabel, "with-manager")) if !g.setStateTransition(stateInit, stateRunning) { panic("invalid graceful manager state: transition from init to running failed") diff --git a/modules/process/context.go b/modules/process/context.go index 26a80ebd620..1854988bce2 100644 --- a/modules/process/context.go +++ b/modules/process/context.go @@ -32,7 +32,7 @@ func (c *Context) Value(key any) any { } // ProcessContextKey is the key under which process contexts are stored -var ProcessContextKey any = "process-context" +var ProcessContextKey any = "process_context" // GetContext will return a process context if one exists func GetContext(ctx context.Context) *Context { diff --git a/modules/process/manager.go b/modules/process/manager.go index bdc4931810d..7b8ada786ea 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -26,7 +26,7 @@ var ( ) // DescriptionPProfLabel is a label set on goroutines that have a process attached -const DescriptionPProfLabel = "process-description" +const DescriptionPProfLabel = "process_description" // PIDPProfLabel is a label set on goroutines that have a process attached const PIDPProfLabel = "pid" @@ -35,7 +35,7 @@ const PIDPProfLabel = "pid" const PPIDPProfLabel = "ppid" // ProcessTypePProfLabel is a label set on goroutines that have a process attached -const ProcessTypePProfLabel = "process-type" +const ProcessTypePProfLabel = "process_type" // IDType is a pid type type IDType string From e4c4629465f4e45ffdb9ef5eff6ef17e15ee4c2d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 17 Dec 2024 19:44:16 -0800 Subject: [PATCH 33/49] Move RepoTransfer from models to models/repo sub package (#32506) `RepoTransfer` now is at models, but if we want to move it into `repo` model, it will depend on `Team`. So this PR also makes repo model depend on org model to make it possible. Just refactor, no code change. - [x] Move `DeleteOrganization` from `models/organization` to service layer - [x] Move `AccessibleTeamReposEnv` to `models/repo` - [x] Move `RepoTransfer` from `models` to `models/repo` - [x] Merge `getUserTeamIDs` and `GetUserTeamIDs`, Merge `GetUserTeams` and `getUserTeams`. - [x] Remove `Team`'s `Repos []*repo_model.Repository` to avoid dependency recycle. --- models/activities/action.go | 2 +- models/error.go | 42 ---- models/organization/org.go | 209 +----------------- models/organization/org_repo.go | 17 -- models/organization/org_test.go | 8 +- models/organization/team.go | 17 +- models/organization/team_list.go | 7 +- models/organization/team_repo.go | 26 --- models/organization/team_test.go | 10 +- models/repo/org_repo.go | 206 +++++++++++++++++ models/{repo_transfer.go => repo/transfer.go} | 66 +++++- routers/api/v1/org/team.go | 6 +- routers/api/v1/repo/teams.go | 2 +- routers/api/v1/repo/transfer.go | 7 +- routers/web/org/teams.go | 8 +- routers/web/repo/repo.go | 3 +- routers/web/repo/setting/collaboration.go | 2 +- routers/web/repo/setting/setting.go | 6 +- services/context/repo.go | 3 +- services/convert/repository.go | 7 +- services/org/org.go | 31 ++- services/org/team.go | 29 ++- services/org/team_test.go | 11 +- services/org/user.go | 2 +- services/repository/repo_team.go | 11 +- services/repository/transfer.go | 9 +- services/repository/transfer_test.go | 17 +- services/user/block.go | 3 +- templates/org/team/repositories.tmpl | 2 +- tests/integration/api_user_block_test.go | 3 +- 30 files changed, 383 insertions(+), 389 deletions(-) delete mode 100644 models/organization/org_repo.go create mode 100644 models/repo/org_repo.go rename models/{repo_transfer.go => repo/transfer.go} (73%) diff --git a/models/activities/action.go b/models/activities/action.go index 65d95fbe667..ff7fdb2f106 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -511,7 +511,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. } if opts.RequestedTeam != nil { - env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(ctx, opts.RequestedTeam) + env := repo_model.AccessibleTeamReposEnv(ctx, organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) if err != nil { return nil, fmt.Errorf("GetTeamRepositories: %w", err) diff --git a/models/error.go b/models/error.go index 75c53245de5..865d5a442f6 100644 --- a/models/error.go +++ b/models/error.go @@ -72,48 +72,6 @@ func (err ErrDeleteLastAdminUser) Error() string { return fmt.Sprintf("can not delete the last admin user [uid: %d]", err.UID) } -// ErrNoPendingRepoTransfer is an error type for repositories without a pending -// transfer request -type ErrNoPendingRepoTransfer struct { - RepoID int64 -} - -func (err ErrNoPendingRepoTransfer) Error() string { - return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID) -} - -// IsErrNoPendingTransfer is an error type when a repository has no pending -// transfers -func IsErrNoPendingTransfer(err error) bool { - _, ok := err.(ErrNoPendingRepoTransfer) - return ok -} - -func (err ErrNoPendingRepoTransfer) Unwrap() error { - return util.ErrNotExist -} - -// ErrRepoTransferInProgress represents the state of a repository that has an -// ongoing transfer -type ErrRepoTransferInProgress struct { - Uname string - Name string -} - -// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress. -func IsErrRepoTransferInProgress(err error) bool { - _, ok := err.(ErrRepoTransferInProgress) - return ok -} - -func (err ErrRepoTransferInProgress) Error() string { - return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name) -} - -func (err ErrRepoTransferInProgress) Unwrap() error { - return util.ErrAlreadyExist -} - // ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error. type ErrInvalidCloneAddr struct { Host string diff --git a/models/organization/org.go b/models/organization/org.go index 725a99356e9..3e55a367589 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -9,11 +9,8 @@ import ( "fmt" "strings" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -407,33 +404,6 @@ func GetOrgByName(ctx context.Context, name string) (*Organization, error) { return u, nil } -// DeleteOrganization deletes models associated to an organization. -func DeleteOrganization(ctx context.Context, org *Organization) error { - if org.Type != user_model.UserTypeOrganization { - return fmt.Errorf("%s is a user not an organization", org.Name) - } - - if err := db.DeleteBeans(ctx, - &Team{OrgID: org.ID}, - &OrgUser{OrgID: org.ID}, - &TeamUser{OrgID: org.ID}, - &TeamUnit{OrgID: org.ID}, - &TeamInvite{OrgID: org.ID}, - &secret_model.Secret{OwnerID: org.ID}, - &user_model.Blocking{BlockerID: org.ID}, - &actions_model.ActionRunner{OwnerID: org.ID}, - &actions_model.ActionRunnerToken{OwnerID: org.ID}, - ); err != nil { - return fmt.Errorf("DeleteBeans: %w", err) - } - - if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil { - return fmt.Errorf("Delete: %w", err) - } - - return nil -} - // GetOrgUserMaxAuthorizeLevel returns highest authorize level of user in an organization func (org *Organization) GetOrgUserMaxAuthorizeLevel(ctx context.Context, uid int64) (perm.AccessMode, error) { var authorize perm.AccessMode @@ -604,7 +574,9 @@ func RemoveOrgRepo(ctx context.Context, orgID, repoID int64) error { return err } -func (org *Organization) getUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) { +// GetUserTeams returns all teams that belong to user, +// and that the user has joined. +func (org *Organization) GetUserTeams(ctx context.Context, userID int64, cols ...string) ([]*Team, error) { teams := make([]*Team, 0, org.NumTeams) return teams, db.GetEngine(ctx). Where("`team_user`.org_id = ?", org.ID). @@ -616,7 +588,8 @@ func (org *Organization) getUserTeams(ctx context.Context, userID int64, cols .. Find(&teams) } -func (org *Organization) getUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) { +// GetUserTeamIDs returns of all team IDs of the organization that user is member of. +func (org *Organization) GetUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) { teamIDs := make([]int64, 0, org.NumTeams) return teamIDs, db.GetEngine(ctx). Table("team"). @@ -640,175 +613,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder { func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) { return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode) } - -// GetUserTeamIDs returns of all team IDs of the organization that user is member of. -func (org *Organization) GetUserTeamIDs(ctx context.Context, userID int64) ([]int64, error) { - return org.getUserTeamIDs(ctx, userID) -} - -// GetUserTeams returns all teams that belong to user, -// and that the user has joined. -func (org *Organization) GetUserTeams(ctx context.Context, userID int64) ([]*Team, error) { - return org.getUserTeams(ctx, userID) -} - -// AccessibleReposEnvironment operations involving the repositories that are -// accessible to a particular user -type AccessibleReposEnvironment interface { - CountRepos() (int64, error) - RepoIDs(page, pageSize int) ([]int64, error) - Repos(page, pageSize int) (repo_model.RepositoryList, error) - MirrorRepos() (repo_model.RepositoryList, error) - AddKeyword(keyword string) - SetSort(db.SearchOrderBy) -} - -type accessibleReposEnv struct { - org *Organization - user *user_model.User - team *Team - teamIDs []int64 - ctx context.Context - keyword string - orderBy db.SearchOrderBy -} - -// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org` -// that are accessible to the specified user. -func AccessibleReposEnv(ctx context.Context, org *Organization, userID int64) (AccessibleReposEnvironment, error) { - var user *user_model.User - - if userID > 0 { - u, err := user_model.GetUserByID(ctx, userID) - if err != nil { - return nil, err - } - user = u - } - - teamIDs, err := org.getUserTeamIDs(ctx, userID) - if err != nil { - return nil, err - } - return &accessibleReposEnv{ - org: org, - user: user, - teamIDs: teamIDs, - ctx: ctx, - orderBy: db.SearchOrderByRecentUpdated, - }, nil -} - -// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org` -// that are accessible to the specified team. -func (org *Organization) AccessibleTeamReposEnv(ctx context.Context, team *Team) AccessibleReposEnvironment { - return &accessibleReposEnv{ - org: org, - team: team, - ctx: ctx, - orderBy: db.SearchOrderByRecentUpdated, - } -} - -func (env *accessibleReposEnv) cond() builder.Cond { - cond := builder.NewCond() - if env.team != nil { - cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID}) - } else { - if env.user == nil || !env.user.IsRestricted { - cond = cond.Or(builder.Eq{ - "`repository`.owner_id": env.org.ID, - "`repository`.is_private": false, - }) - } - if len(env.teamIDs) > 0 { - cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) - } - } - if env.keyword != "" { - cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)}) - } - return cond -} - -func (env *accessibleReposEnv) CountRepos() (int64, error) { - repoCount, err := db.GetEngine(env.ctx). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). - Where(env.cond()). - Distinct("`repository`.id"). - Count(&repo_model.Repository{}) - if err != nil { - return 0, fmt.Errorf("count user repositories in organization: %w", err) - } - return repoCount, nil -} - -func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { - if page <= 0 { - page = 1 - } - - repoIDs := make([]int64, 0, pageSize) - return repoIDs, db.GetEngine(env.ctx). - Table("repository"). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). - Where(env.cond()). - GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]). - OrderBy(string(env.orderBy)). - Limit(pageSize, (page-1)*pageSize). - Cols("`repository`.id"). - Find(&repoIDs) -} - -func (env *accessibleReposEnv) Repos(page, pageSize int) (repo_model.RepositoryList, error) { - repoIDs, err := env.RepoIDs(page, pageSize) - if err != nil { - return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) - } - - repos := make([]*repo_model.Repository, 0, len(repoIDs)) - if len(repoIDs) == 0 { - return repos, nil - } - - return repos, db.GetEngine(env.ctx). - In("`repository`.id", repoIDs). - OrderBy(string(env.orderBy)). - Find(&repos) -} - -func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { - repoIDs := make([]int64, 0, 10) - return repoIDs, db.GetEngine(env.ctx). - Table("repository"). - Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true). - Where(env.cond()). - GroupBy("`repository`.id, `repository`.updated_unix"). - OrderBy(string(env.orderBy)). - Cols("`repository`.id"). - Find(&repoIDs) -} - -func (env *accessibleReposEnv) MirrorRepos() (repo_model.RepositoryList, error) { - repoIDs, err := env.MirrorRepoIDs() - if err != nil { - return nil, fmt.Errorf("MirrorRepoIDs: %w", err) - } - - repos := make([]*repo_model.Repository, 0, len(repoIDs)) - if len(repoIDs) == 0 { - return repos, nil - } - - return repos, db.GetEngine(env.ctx). - In("`repository`.id", repoIDs). - Find(&repos) -} - -func (env *accessibleReposEnv) AddKeyword(keyword string) { - env.keyword = keyword -} - -func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) { - env.orderBy = orderBy -} diff --git a/models/organization/org_repo.go b/models/organization/org_repo.go deleted file mode 100644 index f7e59928f42..00000000000 --- a/models/organization/org_repo.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package organization - -import ( - "context" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" -) - -// GetOrgRepositories get repos belonging to the given organization -func GetOrgRepositories(ctx context.Context, orgID int64) (repo_model.RepositoryList, error) { - var orgRepos []*repo_model.Repository - return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) -} diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 5e99e88689e..2c5b4090df3 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -318,7 +318,7 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID, expectedCount int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) count, err := env.CountRepos() assert.NoError(t, err) @@ -332,7 +332,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repoIDs, err := env.RepoIDs(1, 100) assert.NoError(t, err) @@ -346,7 +346,7 @@ func TestAccessibleReposEnv_Repos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repos, err := env.Repos(1, 100) assert.NoError(t, err) @@ -365,7 +365,7 @@ func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := organization.AccessibleReposEnv(db.DefaultContext, org, userID) + env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) repos, err := env.MirrorRepos() assert.NoError(t, err) diff --git a/models/organization/team.go b/models/organization/team.go index fb7f0c04939..96666da39a7 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -78,9 +77,8 @@ type Team struct { LowerName string Name string Description string - AccessMode perm.AccessMode `xorm:"'authorize'"` - Repos []*repo_model.Repository `xorm:"-"` - Members []*user_model.User `xorm:"-"` + AccessMode perm.AccessMode `xorm:"'authorize'"` + Members []*user_model.User `xorm:"-"` NumRepos int NumMembers int Units []*TeamUnit `xorm:"-"` @@ -155,17 +153,6 @@ func (t *Team) IsMember(ctx context.Context, userID int64) bool { return isMember } -// LoadRepositories returns paginated repositories in team of organization. -func (t *Team) LoadRepositories(ctx context.Context) (err error) { - if t.Repos != nil { - return nil - } - t.Repos, err = GetTeamRepositories(ctx, &SearchTeamRepoOptions{ - TeamID: t.ID, - }) - return err -} - // LoadMembers returns paginated members in team of organization. func (t *Team) LoadMembers(ctx context.Context) (err error) { t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{ diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 4ceb405e310..6f2a922e954 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "xorm.io/builder" @@ -98,11 +97,11 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64, } // GetRepoTeams gets the list of teams that has access to the repository -func GetRepoTeams(ctx context.Context, repo *repo_model.Repository) (teams TeamList, err error) { +func GetRepoTeams(ctx context.Context, orgID, repoID int64) (teams TeamList, err error) { return teams, db.GetEngine(ctx). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Where("team.org_id = ?", repo.OwnerID). - And("team_repo.repo_id=?", repo.ID). + Where("team.org_id = ?", orgID). + And("team_repo.repo_id=?", repoID). OrderBy("CASE WHEN name LIKE '" + OwnerTeamName + "' THEN '' ELSE name END"). Find(&teams) } diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index c90dfdeda04..53edd203a8a 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -8,10 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" - repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" - - "xorm.io/builder" ) // TeamRepo represents an team-repository relation. @@ -32,29 +29,6 @@ func HasTeamRepo(ctx context.Context, orgID, teamID, repoID int64) bool { return has } -type SearchTeamRepoOptions struct { - db.ListOptions - TeamID int64 -} - -// GetRepositories returns paginated repositories in team of organization. -func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo_model.RepositoryList, error) { - sess := db.GetEngine(ctx) - if opts.TeamID > 0 { - sess = sess.In("id", - builder.Select("repo_id"). - From("team_repo"). - Where(builder.Eq{"team_id": opts.TeamID}), - ) - } - if opts.PageSize > 0 { - sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) - } - var repos []*repo_model.Repository - return repos, sess.OrderBy("repository.name"). - Find(&repos) -} - // AddTeamRepo adds a repo for an organization's team func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error { _, err := db.GetEngine(ctx).Insert(&TeamRepo{ diff --git a/models/organization/team_test.go b/models/organization/team_test.go index 8c34e7a6124..deaabbfa2c0 100644 --- a/models/organization/team_test.go +++ b/models/organization/team_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "github.com/stretchr/testify/assert" @@ -42,9 +43,12 @@ func TestTeam_GetRepositories(t *testing.T) { test := func(teamID int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext)) - assert.Len(t, team.Repos, team.NumRepos) - for _, repo := range team.Repos { + repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{ + TeamID: team.ID, + }) + assert.NoError(t, err) + assert.Len(t, repos, team.NumRepos) + for _, repo := range repos { unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repo.ID}) } } diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go new file mode 100644 index 00000000000..5f0af2d475a --- /dev/null +++ b/models/repo/org_repo.go @@ -0,0 +1,206 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" + + "xorm.io/builder" +) + +// GetOrgRepositories get repos belonging to the given organization +func GetOrgRepositories(ctx context.Context, orgID int64) (RepositoryList, error) { + var orgRepos []*Repository + return orgRepos, db.GetEngine(ctx).Where("owner_id = ?", orgID).Find(&orgRepos) +} + +type SearchTeamRepoOptions struct { + db.ListOptions + TeamID int64 +} + +// GetRepositories returns paginated repositories in team of organization. +func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (RepositoryList, error) { + sess := db.GetEngine(ctx) + if opts.TeamID > 0 { + sess = sess.In("id", + builder.Select("repo_id"). + From("team_repo"). + Where(builder.Eq{"team_id": opts.TeamID}), + ) + } + if opts.PageSize > 0 { + sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + } + var repos []*Repository + return repos, sess.OrderBy("repository.name"). + Find(&repos) +} + +// AccessibleReposEnvironment operations involving the repositories that are +// accessible to a particular user +type AccessibleReposEnvironment interface { + CountRepos() (int64, error) + RepoIDs(page, pageSize int) ([]int64, error) + Repos(page, pageSize int) (RepositoryList, error) + MirrorRepos() (RepositoryList, error) + AddKeyword(keyword string) + SetSort(db.SearchOrderBy) +} + +type accessibleReposEnv struct { + org *org_model.Organization + user *user_model.User + team *org_model.Team + teamIDs []int64 + ctx context.Context + keyword string + orderBy db.SearchOrderBy +} + +// AccessibleReposEnv builds an AccessibleReposEnvironment for the repositories in `org` +// that are accessible to the specified user. +func AccessibleReposEnv(ctx context.Context, org *org_model.Organization, userID int64) (AccessibleReposEnvironment, error) { + var user *user_model.User + + if userID > 0 { + u, err := user_model.GetUserByID(ctx, userID) + if err != nil { + return nil, err + } + user = u + } + + teamIDs, err := org.GetUserTeamIDs(ctx, userID) + if err != nil { + return nil, err + } + return &accessibleReposEnv{ + org: org, + user: user, + teamIDs: teamIDs, + ctx: ctx, + orderBy: db.SearchOrderByRecentUpdated, + }, nil +} + +// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org` +// that are accessible to the specified team. +func AccessibleTeamReposEnv(ctx context.Context, org *org_model.Organization, team *org_model.Team) AccessibleReposEnvironment { + return &accessibleReposEnv{ + org: org, + team: team, + ctx: ctx, + orderBy: db.SearchOrderByRecentUpdated, + } +} + +func (env *accessibleReposEnv) cond() builder.Cond { + cond := builder.NewCond() + if env.team != nil { + cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID}) + } else { + if env.user == nil || !env.user.IsRestricted { + cond = cond.Or(builder.Eq{ + "`repository`.owner_id": env.org.ID, + "`repository`.is_private": false, + }) + } + if len(env.teamIDs) > 0 { + cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) + } + } + if env.keyword != "" { + cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)}) + } + return cond +} + +func (env *accessibleReposEnv) CountRepos() (int64, error) { + repoCount, err := db.GetEngine(env.ctx). + Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). + Where(env.cond()). + Distinct("`repository`.id"). + Count(&Repository{}) + if err != nil { + return 0, fmt.Errorf("count user repositories in organization: %w", err) + } + return repoCount, nil +} + +func (env *accessibleReposEnv) RepoIDs(page, pageSize int) ([]int64, error) { + if page <= 0 { + page = 1 + } + + repoIDs := make([]int64, 0, pageSize) + return repoIDs, db.GetEngine(env.ctx). + Table("repository"). + Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). + Where(env.cond()). + GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]). + OrderBy(string(env.orderBy)). + Limit(pageSize, (page-1)*pageSize). + Cols("`repository`.id"). + Find(&repoIDs) +} + +func (env *accessibleReposEnv) Repos(page, pageSize int) (RepositoryList, error) { + repoIDs, err := env.RepoIDs(page, pageSize) + if err != nil { + return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) + } + + repos := make([]*Repository, 0, len(repoIDs)) + if len(repoIDs) == 0 { + return repos, nil + } + + return repos, db.GetEngine(env.ctx). + In("`repository`.id", repoIDs). + OrderBy(string(env.orderBy)). + Find(&repos) +} + +func (env *accessibleReposEnv) MirrorRepoIDs() ([]int64, error) { + repoIDs := make([]int64, 0, 10) + return repoIDs, db.GetEngine(env.ctx). + Table("repository"). + Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id AND `repository`.is_mirror=?", true). + Where(env.cond()). + GroupBy("`repository`.id, `repository`.updated_unix"). + OrderBy(string(env.orderBy)). + Cols("`repository`.id"). + Find(&repoIDs) +} + +func (env *accessibleReposEnv) MirrorRepos() (RepositoryList, error) { + repoIDs, err := env.MirrorRepoIDs() + if err != nil { + return nil, fmt.Errorf("MirrorRepoIDs: %w", err) + } + + repos := make([]*Repository, 0, len(repoIDs)) + if len(repoIDs) == 0 { + return repos, nil + } + + return repos, db.GetEngine(env.ctx). + In("`repository`.id", repoIDs). + Find(&repos) +} + +func (env *accessibleReposEnv) AddKeyword(keyword string) { + env.keyword = keyword +} + +func (env *accessibleReposEnv) SetSort(orderBy db.SearchOrderBy) { + env.orderBy = orderBy +} diff --git a/models/repo_transfer.go b/models/repo/transfer.go similarity index 73% rename from models/repo_transfer.go rename to models/repo/transfer.go index 37f591f65dc..43e15b33bcf 100644 --- a/models/repo_transfer.go +++ b/models/repo/transfer.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package models +package repo import ( "context" @@ -10,16 +10,58 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" - repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) +// ErrNoPendingRepoTransfer is an error type for repositories without a pending +// transfer request +type ErrNoPendingRepoTransfer struct { + RepoID int64 +} + +func (err ErrNoPendingRepoTransfer) Error() string { + return fmt.Sprintf("repository doesn't have a pending transfer [repo_id: %d]", err.RepoID) +} + +// IsErrNoPendingTransfer is an error type when a repository has no pending +// transfers +func IsErrNoPendingTransfer(err error) bool { + _, ok := err.(ErrNoPendingRepoTransfer) + return ok +} + +func (err ErrNoPendingRepoTransfer) Unwrap() error { + return util.ErrNotExist +} + +// ErrRepoTransferInProgress represents the state of a repository that has an +// ongoing transfer +type ErrRepoTransferInProgress struct { + Uname string + Name string +} + +// IsErrRepoTransferInProgress checks if an error is a ErrRepoTransferInProgress. +func IsErrRepoTransferInProgress(err error) bool { + _, ok := err.(ErrRepoTransferInProgress) + return ok +} + +func (err ErrRepoTransferInProgress) Error() string { + return fmt.Sprintf("repository is already being transferred [uname: %s, name: %s]", err.Uname, err.Name) +} + +func (err ErrRepoTransferInProgress) Unwrap() error { + return util.ErrAlreadyExist +} + // RepoTransfer is used to manage repository transfers -type RepoTransfer struct { +type RepoTransfer struct { //nolint ID int64 `xorm:"pk autoincr"` DoerID int64 Doer *user_model.User `xorm:"-"` @@ -126,7 +168,7 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer // process for the repository -func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) (*RepoTransfer, error) { +func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { transfers, err := GetPendingRepositoryTransfers(ctx, &PendingRepositoryTransferOptions{RepoID: repo.ID}) if err != nil { return nil, err @@ -145,11 +187,11 @@ func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { } // TestRepositoryReadyForTransfer make sure repo is ready to transfer -func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { +func TestRepositoryReadyForTransfer(status RepositoryStatus) error { switch status { - case repo_model.RepositoryBeingMigrated: + case RepositoryBeingMigrated: return errors.New("repo is not ready, currently migrating") - case repo_model.RepositoryPendingTransfer: + case RepositoryPendingTransfer: return ErrRepoTransferInProgress{} } return nil @@ -159,7 +201,7 @@ func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { // it marks the repository transfer as "pending" func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repoID int64, teams []*organization.Team) error { return db.WithTx(ctx, func(ctx context.Context) error { - repo, err := repo_model.GetRepositoryByID(ctx, repoID) + repo, err := GetRepositoryByID(ctx, repoID) if err != nil { return err } @@ -169,16 +211,16 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m return err } - repo.Status = repo_model.RepositoryPendingTransfer - if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { + repo.Status = RepositoryPendingTransfer + if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { return err } // Check if new owner has repository with same name. - if has, err := repo_model.IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { + if has, err := IsRepositoryModelExist(ctx, newOwner, repo.Name); err != nil { return fmt.Errorf("IsRepositoryExist: %w", err) } else if has { - return repo_model.ErrRepoAlreadyExist{ + return ErrRepoAlreadyExist{ Uname: newOwner.LowerName, Name: repo.Name, } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index bc50960b61b..8164d2cfe99 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -573,19 +573,19 @@ func GetTeamRepos(ctx *context.APIContext) { // "$ref": "#/responses/notFound" team := ctx.Org.Team - teamRepos, err := organization.GetTeamRepositories(ctx, &organization.SearchTeamRepoOptions{ + teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ ListOptions: utils.GetListOptions(ctx), TeamID: team.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) + ctx.Error(http.StatusInternalServerError, "GetTeamRepositories", err) return } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) return } repos[i] = convert.ToRepo(ctx, repo, permission) diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 82ecaf30201..42fb0a1d753 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -42,7 +42,7 @@ func ListTeams(ctx *context.APIContext) { return } - teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) + teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) if err != nil { ctx.InternalServerError(err) return diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 776b336761f..787ec34404f 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -108,7 +107,7 @@ func Transfer(ctx *context.APIContext) { oldFullname := ctx.Repo.Repository.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { - if models.IsErrRepoTransferInProgress(err) { + if repo_model.IsErrRepoTransferInProgress(err) { ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err) return } @@ -213,9 +212,9 @@ func RejectTransfer(ctx *context.APIContext) { } func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { - repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { - if models.IsErrNoPendingTransfer(err) { + if repo_model.IsErrNoPendingTransfer(err) { ctx.NotFound() return nil } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index bd788321032..b03b18bd9c7 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -410,11 +410,15 @@ func TeamRepositories(ctx *context.Context) { return } - if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { - ctx.ServerError("GetRepositories", err) + repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + TeamID: ctx.Org.Team.ID, + }) + if err != nil { + ctx.ServerError("GetTeamRepositories", err) return } ctx.Data["Units"] = unit_model.Units + ctx.Data["TeamRepos"] = repos ctx.HTML(http.StatusOK, tplTeamRepositories) } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index f5e59b0357b..4b017f61c4b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -11,7 +11,6 @@ import ( "slices" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" @@ -378,7 +377,7 @@ func Action(ctx *context.Context) { } func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { - repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { return err } diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index cdf91edf4a2..df7cc5e39b6 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -32,7 +32,7 @@ func Collaboration(ctx *context.Context) { } ctx.Data["Collaborators"] = users - teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository) + teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("GetRepoTeams", err) return diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 717d7cbce1d..f2169d8a79f 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -788,7 +788,7 @@ func SettingsPost(ctx *context.Context) { if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if repo_model.IsErrRepoAlreadyExist(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) - } else if models.IsErrRepoTransferInProgress(err) { + } else if repo_model.IsErrRepoTransferInProgress(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) } else if errors.Is(err, user_model.ErrBlockedUser) { ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) @@ -814,9 +814,9 @@ func SettingsPost(ctx *context.Context) { return } - repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { - if models.IsErrNoPendingTransfer(err) { + if repo_model.IsErrNoPendingTransfer(err) { ctx.Flash.Error("repo.settings.transfer_abort_invalid") ctx.Redirect(repo.Link() + "/settings") } else { diff --git a/services/context/repo.go b/services/context/repo.go index 9b544391103..e96916ca421 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -14,7 +14,6 @@ import ( "path" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -709,7 +708,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer { - repoTransfer, err := models.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) + repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetPendingRepositoryTransfer", err) return cancel diff --git a/services/convert/repository.go b/services/convert/repository.go index e026d0f4405..88ccd88fcfe 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -7,7 +7,6 @@ import ( "context" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -158,8 +157,8 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR var transfer *api.RepoTransfer if repo.Status == repo_model.RepositoryPendingTransfer { - t, err := models.GetPendingRepositoryTransfer(ctx, repo) - if err != nil && !models.IsErrNoPendingTransfer(err) { + t, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) + if err != nil && !repo_model.IsErrNoPendingTransfer(err) { log.Warn("GetPendingRepositoryTransfer: %v", err) } else { if err := t.LoadAttributes(ctx); err != nil { @@ -248,7 +247,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR } // ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer -func ToRepoTransfer(ctx context.Context, t *models.RepoTransfer) *api.RepoTransfer { +func ToRepoTransfer(ctx context.Context, t *repo_model.RepoTransfer) *api.RepoTransfer { teams, _ := ToTeams(ctx, t.Teams, false) return &api.RepoTransfer{ diff --git a/services/org/org.go b/services/org/org.go index c19572a1230..471e6fcaf16 100644 --- a/services/org/org.go +++ b/services/org/org.go @@ -8,16 +8,45 @@ import ( "fmt" "code.gitea.io/gitea/models" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" repo_model "code.gitea.io/gitea/models/repo" + secret_model "code.gitea.io/gitea/models/secret" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" ) +// deleteOrganization deletes models associated to an organization. +func deleteOrganization(ctx context.Context, org *org_model.Organization) error { + if org.Type != user_model.UserTypeOrganization { + return fmt.Errorf("%s is a user not an organization", org.Name) + } + + if err := db.DeleteBeans(ctx, + &org_model.Team{OrgID: org.ID}, + &org_model.OrgUser{OrgID: org.ID}, + &org_model.TeamUser{OrgID: org.ID}, + &org_model.TeamUnit{OrgID: org.ID}, + &org_model.TeamInvite{OrgID: org.ID}, + &secret_model.Secret{OwnerID: org.ID}, + &user_model.Blocking{BlockerID: org.ID}, + &actions_model.ActionRunner{OwnerID: org.ID}, + &actions_model.ActionRunnerToken{OwnerID: org.ID}, + ); err != nil { + return fmt.Errorf("DeleteBeans: %w", err) + } + + if _, err := db.GetEngine(ctx).ID(org.ID).Delete(new(user_model.User)); err != nil { + return fmt.Errorf("Delete: %w", err) + } + + return nil +} + // DeleteOrganization completely and permanently deletes everything of organization. func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge bool) error { ctx, committer, err := db.TxContext(ctx) @@ -48,7 +77,7 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge return models.ErrUserOwnPackages{UID: org.ID} } - if err := org_model.DeleteOrganization(ctx, org); err != nil { + if err := deleteOrganization(ctx, org); err != nil { return fmt.Errorf("DeleteOrganization: %w", err) } diff --git a/services/org/team.go b/services/org/team.go index 3688e684339..ee3bd898ea7 100644 --- a/services/org/team.go +++ b/services/org/team.go @@ -141,11 +141,14 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA // Update access for team members if needed. if authChanged { - if err = t.LoadRepositories(ctx); err != nil { - return fmt.Errorf("LoadRepositories: %w", err) + repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + TeamID: t.ID, + }) + if err != nil { + return fmt.Errorf("GetTeamRepositories: %w", err) } - for _, repo := range t.Repos { + for _, repo := range repos { if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { return fmt.Errorf("recalculateTeamAccesses: %w", err) } @@ -172,10 +175,6 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { } defer committer.Close() - if err := t.LoadRepositories(ctx); err != nil { - return err - } - if err := t.LoadMembers(ctx); err != nil { return err } @@ -301,8 +300,11 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode // FIXME: Update watch repos batchly if setting.Service.AutoWatchNewRepos { // Get team and its repositories. - if err := team.LoadRepositories(ctx); err != nil { - log.Error("team.LoadRepositories failed: %v", err) + repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + TeamID: team.ID, + }) + if err != nil { + log.Error("GetTeamRepositories failed: %v", err) } // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment @@ -312,7 +314,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, user *user_mode log.Error("watch repo failed: %v", err) } } - }(team.Repos) + }(repos) } return nil @@ -332,7 +334,10 @@ func removeTeamMember(ctx context.Context, team *organization.Team, user *user_m team.NumMembers-- - if err := team.LoadRepositories(ctx); err != nil { + repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + TeamID: team.ID, + }) + if err != nil { return err } @@ -350,7 +355,7 @@ func removeTeamMember(ctx context.Context, team *organization.Team, user *user_m } // Delete access to team repositories. - for _, repo := range team.Repos { + for _, repo := range repos { if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil { return err } diff --git a/services/org/team_test.go b/services/org/team_test.go index 98addac8f8a..3791776e46e 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -189,9 +189,12 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { testTeamRepositories := func(teamID int64, repoIDs []int64) { team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) - assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) - assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name) + repos, err := repo_model.GetTeamRepositories(db.DefaultContext, &repo_model.SearchTeamRepoOptions{ + TeamID: team.ID, + }) + assert.NoError(t, err, "%s: GetTeamRepositories", team.Name) + assert.Len(t, repos, team.NumRepos, "%s: len repo", team.Name) + assert.Len(t, repos, len(repoIDs), "%s: repo count", team.Name) for i, rid := range repoIDs { if rid > 0 { assert.True(t, repo_service.HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i) @@ -310,5 +313,5 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i) } } - assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") + assert.NoError(t, DeleteOrganization(db.DefaultContext, org, false), "DeleteOrganization") } diff --git a/services/org/user.go b/services/org/user.go index 0627860fe7c..0e74d006bb7 100644 --- a/services/org/user.go +++ b/services/org/user.go @@ -60,7 +60,7 @@ func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *us } // Delete all repository accesses and unwatch them. - env, err := organization.AccessibleReposEnv(ctx, org, user.ID) + env, err := repo_model.AccessibleReposEnv(ctx, org, user.ID) if err != nil { return fmt.Errorf("AccessibleReposEnv: %w", err) } diff --git a/services/repository/repo_team.go b/services/repository/repo_team.go index 29c67893b23..672ee49fea9 100644 --- a/services/repository/repo_team.go +++ b/services/repository/repo_team.go @@ -63,7 +63,7 @@ func addRepositoryToTeam(ctx context.Context, t *organization.Team, repo *repo_m // If the team already has some repositories they will be left unchanged. func AddAllRepositoriesToTeam(ctx context.Context, t *organization.Team) error { return db.WithTx(ctx, func(ctx context.Context) error { - orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID) + orgRepos, err := repo_model.GetOrgRepositories(ctx, t.OrgID) if err != nil { return fmt.Errorf("get org repos: %w", err) } @@ -103,8 +103,15 @@ func RemoveAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (e // Note: Shall not be called if team includes all repositories func removeAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) { e := db.GetEngine(ctx) + repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{ + TeamID: t.ID, + }) + if err != nil { + return fmt.Errorf("GetTeamRepositories: %w", err) + } + // Delete all accesses. - for _, repo := range t.Repos { + for _, repo := range repos { if err := access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil { return err } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 9a643469d9d..9ef28ddeb9f 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -9,7 +9,6 @@ import ( "os" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -285,7 +284,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName wikiRenamed = true } - if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { return fmt.Errorf("deleteRepositoryTransfer: %w", err) } repo.Status = repo_model.RepositoryReady @@ -388,7 +387,7 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo // StartRepositoryTransfer transfer a repo from one owner to a new one. // it make repository into pending transfer state, if doer can not create repo for new owner. func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { - if err := models.TestRepositoryReadyForTransfer(repo.Status); err != nil { + if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { return err } @@ -425,7 +424,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use // Make repo as pending for transfer repo.Status = repo_model.RepositoryPendingTransfer - if err := models.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { + if err := repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { return err } @@ -449,7 +448,7 @@ func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) return err } - if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { return err } diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 0401701ba55..91722fb8ae1 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -7,7 +7,6 @@ import ( "sync" "testing" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -86,23 +85,23 @@ func TestRepositoryTransfer(t *testing.T) { doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - transfer, err := models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.NoError(t, err) assert.NotNil(t, transfer) // Cancel transfer assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) - transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.Error(t, err) assert.Nil(t, transfer) - assert.True(t, models.IsErrNoPendingTransfer(err)) + assert.True(t, repo_model.IsErrNoPendingTransfer(err)) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) + assert.NoError(t, repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) - transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) assert.NoError(t, err) assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) assert.Equal(t, "user2", transfer.Recipient.Name) @@ -110,12 +109,12 @@ func TestRepositoryTransfer(t *testing.T) { org6 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // Only transfer can be started at any given time - err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) + err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) assert.Error(t, err) - assert.True(t, models.IsErrRepoTransferInProgress(err)) + assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) // Unknown user - err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) + err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) assert.Error(t, err) // Cancel transfer diff --git a/services/user/block.go b/services/user/block.go index 0b3b618aae6..c24ce5273ca 100644 --- a/services/user/block.go +++ b/services/user/block.go @@ -6,7 +6,6 @@ package user import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" org_model "code.gitea.io/gitea/models/organization" @@ -194,7 +193,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro } func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { - transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{ + transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ SenderID: sender.ID, RecipientID: recipient.ID, }) diff --git a/templates/org/team/repositories.tmpl b/templates/org/team/repositories.tmpl index 502cf97992a..92c3d724ba6 100644 --- a/templates/org/team/repositories.tmpl +++ b/templates/org/team/repositories.tmpl @@ -27,7 +27,7 @@ {{end}}
- {{range .Team.Repos}} + {{range $.TeamRepos}}
{{template "repo/icon" .}} diff --git a/tests/integration/api_user_block_test.go b/tests/integration/api_user_block_test.go index 2cc3895a71d..ae6b9eb8496 100644 --- a/tests/integration/api_user_block_test.go +++ b/tests/integration/api_user_block_test.go @@ -8,7 +8,6 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" @@ -42,7 +41,7 @@ func TestBlockUser(t *testing.T) { } countRepositoryTransfers := func(t *testing.T, senderID, recipientID int64) int64 { - transfers, err := models.GetPendingRepositoryTransfers(db.DefaultContext, &models.PendingRepositoryTransferOptions{ + transfers, err := repo_model.GetPendingRepositoryTransfers(db.DefaultContext, &repo_model.PendingRepositoryTransferOptions{ SenderID: senderID, RecipientID: recipientID, }) From f9f62b4c4cb356c7a06b4ce6b0be2e672aa47725 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 17 Dec 2024 20:10:38 -0800 Subject: [PATCH 34/49] Move delete deploy keys into service layer (#32201) --- models/asymkey/error.go | 5 ++- models/repo.go | 48 ---------------------- routers/api/v1/repo/key.go | 2 +- routers/web/repo/setting/deploy_key.go | 2 +- services/asymkey/deploy_key.go | 56 ++++++++++++++++++++++++-- services/asymkey/main_test.go | 1 + services/repository/delete.go | 13 ++---- 7 files changed, 61 insertions(+), 66 deletions(-) diff --git a/models/asymkey/error.go b/models/asymkey/error.go index 03bc82302f1..2e65d766129 100644 --- a/models/asymkey/error.go +++ b/models/asymkey/error.go @@ -217,6 +217,7 @@ func (err ErrGPGKeyAccessDenied) Unwrap() error { // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 + RepoID int64 KeyID int64 Note string } @@ -228,8 +229,8 @@ func IsErrKeyAccessDenied(err error) bool { } func (err ErrKeyAccessDenied) Error() string { - return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]", - err.UserID, err.KeyID, err.Note) + return fmt.Sprintf("user does not have access to the key [user_id: %d, repo_id: %d, key_id: %d, note: %s]", + err.UserID, err.RepoID, err.KeyID, err.Note) } func (err ErrKeyAccessDenied) Unwrap() error { diff --git a/models/repo.go b/models/repo.go index 0dc8ee5df33..3e9c52fdd9c 100644 --- a/models/repo.go +++ b/models/repo.go @@ -6,15 +6,12 @@ package models import ( "context" - "fmt" "strconv" _ "image/jpeg" // Needed for jpeg support - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -315,48 +312,3 @@ func DoctorUserStarNum(ctx context.Context) (err error) { return err } - -// DeleteDeployKey delete deploy keys -func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { - key, err := asymkey_model.GetDeployKeyByID(ctx, id) - if err != nil { - if asymkey_model.IsErrDeployKeyNotExist(err) { - return nil - } - return fmt.Errorf("GetDeployKeyByID: %w", err) - } - - // Check if user has access to delete this key. - if !doer.IsAdmin { - repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) - if err != nil { - return fmt.Errorf("GetRepositoryByID: %w", err) - } - has, err := access_model.IsUserRepoAdmin(ctx, repo, doer) - if err != nil { - return fmt.Errorf("GetUserRepoPermission: %w", err) - } else if !has { - return asymkey_model.ErrKeyAccessDenied{ - UserID: doer.ID, - KeyID: key.ID, - Note: "deploy", - } - } - } - - if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil { - return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err) - } - - // Check if this is the last reference to same key content. - has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) - if err != nil { - return err - } else if !has { - if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil { - return err - } - } - - return nil -} diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index e5115980eb0..060694d085f 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -279,7 +279,7 @@ func DeleteDeploykey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Doer, ctx.PathParamInt64(":id")); err != nil { + if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64(":id")); err != nil { if asymkey_model.IsErrKeyAccessDenied(err) { ctx.Error(http.StatusForbidden, "", "You do not have access to this key") } else { diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go index abc3eb4af18..193562528bf 100644 --- a/routers/web/repo/setting/deploy_key.go +++ b/routers/web/repo/setting/deploy_key.go @@ -99,7 +99,7 @@ func DeployKeysPost(ctx *context.Context) { // DeleteDeployKey response for deleting a deploy key func DeleteDeployKey(ctx *context.Context) { - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil { + if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteDeployKey: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success")) diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go index 324688c5340..9e5a6d6292b 100644 --- a/services/asymkey/deploy_key.go +++ b/services/asymkey/deploy_key.go @@ -5,21 +5,69 @@ package asymkey import ( "context" + "fmt" - "code.gitea.io/gitea/models" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" + repo_model "code.gitea.io/gitea/models/repo" ) +// DeleteRepoDeployKeys deletes all deploy keys of a repository. permissions check should be done outside +func DeleteRepoDeployKeys(ctx context.Context, repoID int64) (int, error) { + deployKeys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: repoID}) + if err != nil { + return 0, fmt.Errorf("listDeployKeys: %w", err) + } + + for _, dKey := range deployKeys { + if err := deleteDeployKeyFromDB(ctx, dKey); err != nil { + return 0, fmt.Errorf("deleteDeployKeys: %w", err) + } + } + return len(deployKeys), nil +} + +// deleteDeployKeyFromDB delete deploy keys from database +func deleteDeployKeyFromDB(ctx context.Context, key *asymkey_model.DeployKey) error { + if _, err := db.DeleteByID[asymkey_model.DeployKey](ctx, key.ID); err != nil { + return fmt.Errorf("delete deploy key [%d]: %w", key.ID, err) + } + + // Check if this is the last reference to same key content. + has, err := asymkey_model.IsDeployKeyExistByKeyID(ctx, key.KeyID) + if err != nil { + return err + } else if !has { + if _, err = db.DeleteByID[asymkey_model.PublicKey](ctx, key.KeyID); err != nil { + return err + } + } + + return nil +} + // DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed. -func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { +// Permissions check should be done outside. +func DeleteDeployKey(ctx context.Context, repo *repo_model.Repository, id int64) error { dbCtx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() - if err := models.DeleteDeployKey(dbCtx, doer, id); err != nil { + key, err := asymkey_model.GetDeployKeyByID(ctx, id) + if err != nil { + if asymkey_model.IsErrDeployKeyNotExist(err) { + return nil + } + return fmt.Errorf("GetDeployKeyByID: %w", err) + } + + if key.RepoID != repo.ID { + return fmt.Errorf("deploy key %d does not belong to repository %d", id, repo.ID) + } + + if err := deleteDeployKeyFromDB(dbCtx, key); err != nil { return err } if err := committer.Commit(); err != nil { diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go index 3505b26f699..1cdc39933d4 100644 --- a/services/asymkey/main_test.go +++ b/services/asymkey/main_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" + _ "code.gitea.io/gitea/models" _ "code.gitea.io/gitea/models/actions" _ "code.gitea.io/gitea/models/activities" ) diff --git a/services/repository/delete.go b/services/repository/delete.go index f33bae77901..61e39fe1056 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -7,11 +7,9 @@ import ( "context" "fmt" - "code.gitea.io/gitea/models" actions_model "code.gitea.io/gitea/models/actions" activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -76,16 +74,11 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID } // Delete Deploy Keys - deployKeys, err := db.Find[asymkey_model.DeployKey](ctx, asymkey_model.ListDeployKeysOptions{RepoID: repoID}) + deleted, err := asymkey_service.DeleteRepoDeployKeys(ctx, repoID) if err != nil { - return fmt.Errorf("listDeployKeys: %w", err) - } - needRewriteKeysFile := len(deployKeys) > 0 - for _, dKey := range deployKeys { - if err := models.DeleteDeployKey(ctx, doer, dKey.ID); err != nil { - return fmt.Errorf("deleteDeployKeys: %w", err) - } + return err } + needRewriteKeysFile := deleted > 0 if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil { return err From 2beaedc4179ba6a3917ca99e2fd95fd2f476925e Mon Sep 17 00:00:00 2001 From: Exploding Dragon Date: Wed, 18 Dec 2024 15:25:05 +0800 Subject: [PATCH 35/49] Fix Arch package metadata introduced incorrect field (#32881) Incorrect content was introduced while generating the index, which has now been removed, and the missing fields have been added. ![](https://github.com/user-attachments/assets/4fbb8884-337e-43b1-939f-a5ba687f7ffd) --- modules/packages/arch/metadata.go | 6 ++++++ modules/packages/arch/metadata_test.go | 4 ++++ services/packages/arch/repository.go | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index e1e79c60e0d..06a2206a362 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -69,10 +69,12 @@ type FileMetadata struct { Packager string `json:"packager,omitempty"` Groups []string `json:"groups,omitempty"` Provides []string `json:"provides,omitempty"` + Replaces []string `json:"replaces,omitempty"` Depends []string `json:"depends,omitempty"` OptDepends []string `json:"opt_depends,omitempty"` MakeDepends []string `json:"make_depends,omitempty"` CheckDepends []string `json:"check_depends,omitempty"` + Conflicts []string `json:"conflicts,omitempty"` XData []string `json:"xdata,omitempty"` Backup []string `json:"backup,omitempty"` Files []string `json:"files,omitempty"` @@ -201,12 +203,16 @@ func ParsePackageInfo(r io.Reader) (*Package, error) { p.FileMetadata.Provides = append(p.FileMetadata.Provides, value) case "depend": p.FileMetadata.Depends = append(p.FileMetadata.Depends, value) + case "replaces": + p.FileMetadata.Replaces = append(p.FileMetadata.Replaces, value) case "optdepend": p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value) case "makedepend": p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value) case "checkdepend": p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value) + case "conflict": + p.FileMetadata.Conflicts = append(p.FileMetadata.Conflicts, value) case "backup": p.FileMetadata.Backup = append(p.FileMetadata.Backup, value) case "group": diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go index f611ef5e845..37c0a553b84 100644 --- a/modules/packages/arch/metadata_test.go +++ b/modules/packages/arch/metadata_test.go @@ -42,8 +42,10 @@ depend = gitea provides = common provides = gitea optdepend = hex +replaces = gogs checkdepend = common makedepend = cmake +conflict = ninja backup = usr/bin/paket1`) } @@ -149,8 +151,10 @@ func TestParsePackageInfo(t *testing.T) { assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups) assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides) assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends) + assert.ElementsMatch(t, []string{"gogs"}, p.FileMetadata.Replaces) assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends) assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends) + assert.ElementsMatch(t, []string{"ninja"}, p.FileMetadata.Conflicts) assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends) assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup) }) diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go index ab1b85ae958..6731d9a1ac7 100644 --- a/services/packages/arch/repository.go +++ b/services/packages/arch/repository.go @@ -371,11 +371,12 @@ func writeDescription(tw *tar.Writer, opts *entryOptions) error { {"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)}, {"PACKAGER", opts.FileMetadata.Packager}, {"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")}, + {"REPLACES", strings.Join(opts.FileMetadata.Replaces, "\n")}, + {"CONFLICTS", strings.Join(opts.FileMetadata.Conflicts, "\n")}, {"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")}, {"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")}, {"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")}, {"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")}, - {"XDATA", strings.Join(opts.FileMetadata.XData, "\n")}, }) } From 857abed3a92eb6b02a9fd14d2eff20b7a94ed8b4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 18 Dec 2024 21:26:17 +0100 Subject: [PATCH 36/49] Move RepoBranchTagSelector init outside the SFC (#32890) SFCs shouldn't export anything besides their component, and this eliminates one issue with tsc, while apparently also solving a hack. It seems to work as before, also when multiples are on the same page. --- .../js/components/RepoBranchTagSelector.vue | 75 ++++++++----------- web_src/js/features/repo-legacy.ts | 9 ++- web_src/js/globals.d.ts | 1 - 3 files changed, 38 insertions(+), 47 deletions(-) diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 4b7ca1429dc..a5ed8b6dad6 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -1,5 +1,5 @@