From d727757cfb3eb84e58f613fb0898047d87ff1d8f Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 26 Sep 2024 08:03:00 +0000 Subject: [PATCH 01/89] Update dependency monaco-editor to v0.51.0 --- 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 7dd1cba03a..7a537c3fc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "mermaid": "11.2.1", "mini-css-extract-plugin": "2.9.1", "minimatch": "10.0.1", - "monaco-editor": "0.50.0", + "monaco-editor": "0.51.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "postcss": "8.4.47", @@ -12114,9 +12114,9 @@ } }, "node_modules/monaco-editor": { - "version": "0.50.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz", - "integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==", + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.51.0.tgz", + "integrity": "sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==", "license": "MIT" }, "node_modules/monaco-editor-webpack-plugin": { diff --git a/package.json b/package.json index 12c6a05c12..085d10dfca 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "mermaid": "11.2.1", "mini-css-extract-plugin": "2.9.1", "minimatch": "10.0.1", - "monaco-editor": "0.50.0", + "monaco-editor": "0.51.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "postcss": "8.4.47", From 300e01f7337e7999870444b682f8036b1b6cd71e Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Sun, 15 Sep 2024 00:40:36 +0800 Subject: [PATCH 02/89] Check if the `due_date` is nil when editing issues (#32035) (cherry picked from commit 3a51c37672d2fbad1f222922e75ce704d5a1ac71) (cherry picked from commit 961766744bb5b7bf138cb508c8f9afe69622d514) --- routers/api/v1/repo/issue.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index afcfbc00e3..22779e38d2 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -839,10 +839,16 @@ func EditIssue(ctx *context.APIContext) { if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite { var deadlineUnix timeutil.TimeStamp - if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() { - deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), - 23, 59, 59, 0, form.Deadline.Location()) - deadlineUnix = timeutil.TimeStamp(deadline.Unix()) + if form.RemoveDeadline == nil || !*form.RemoveDeadline { + if form.Deadline == nil { + ctx.Error(http.StatusBadRequest, "", "The due_date cannot be empty") + return + } + if !form.Deadline.IsZero() { + deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), + 23, 59, 59, 0, form.Deadline.Location()) + deadlineUnix = timeutil.TimeStamp(deadline.Unix()) + } } if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { From 232179aa3d25a554c8acdac29d13c3d82d14da92 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 16 Sep 2024 23:10:33 +0200 Subject: [PATCH 03/89] Do not escape relative path in RPM primary index (#32038) Fixes #32021 Do not escape the relative path. (cherry picked from commit f528df944bb9436afcb9272add2ee0cccefbdb55) (cherry picked from commit 0cafec4c7a2faf810953e9d522faf5dc019e1522) --- services/packages/rpm/repository.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index 8a2db8670f..2cea04212a 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -13,7 +13,6 @@ import ( "errors" "fmt" "io" - "net/url" "strings" "time" @@ -440,7 +439,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] Archive: pd.FileMetadata.ArchiveSize, }, Location: Location{ - Href: fmt.Sprintf("package/%s/%s/%s/%s", url.PathEscape(pd.Package.Name), url.PathEscape(packageVersion), url.PathEscape(pd.FileMetadata.Architecture), url.PathEscape(fmt.Sprintf("%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture))), + Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture), }, Format: Format{ License: pd.VersionMetadata.License, From 84718e7b17a2b16e382d69528441a6ce1d4959a9 Mon Sep 17 00:00:00 2001 From: hiifong Date: Wed, 18 Sep 2024 03:02:48 +0800 Subject: [PATCH 04/89] Lazy load avatar images (#32051) (cherry picked from commit f38e1014483b84f4541ffb354cd5dfdd7e000e2c) (cherry picked from commit 9d5f409a5a0ac784523cc94a6011f8c8a41d5a95) --- modules/templates/util_avatar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go index 85832cf264..afc1091516 100644 --- a/modules/templates/util_avatar.go +++ b/modules/templates/util_avatar.go @@ -34,7 +34,7 @@ func AvatarHTML(src string, size int, class, name string) template.HTML { name = "avatar" } - return template.HTML(``) + return template.HTML(``) } // Avatar renders user avatars. args: user, size (int), class (string) From 1a8f1482af1a2ce6db834914edfcf00f8dfb0683 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 22 Sep 2024 09:28:20 +0200 Subject: [PATCH 05/89] feat: add IfZero utility function (cherry picked from commit 43de021ac1ca017212ec75fd88a8a80a9db27c4c) (cherry picked from commit 1bdf334844f398d0a2adafc4499cef15f95df6d8) --- modules/util/util.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/util/util.go b/modules/util/util.go index 0444680228..dcd7cf4f29 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -225,6 +225,15 @@ func Iif[T any](condition bool, trueVal, falseVal T) T { return falseVal } +// IfZero returns "def" if "v" is a zero value, otherwise "v" +func IfZero[T comparable](v, def T) T { + var zero T + if v == zero { + return def + } + return v +} + func ReserveLineBreakForTextarea(input string) string { // Since the content is from a form which is a textarea, the line endings are \r\n. // It's a standard behavior of HTML. From d26b7902ec03b3ae9454163a01b184d01d8217d0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 18 Sep 2024 15:17:25 +0800 Subject: [PATCH 06/89] Refactor CSRF protector (#32057) Remove unused CSRF options, decouple "new csrf protector" and "prepare" logic, do not redirect to home page if CSRF validation falis (it shouldn't happen in daily usage, if it happens, redirecting to home doesn't help either but just makes the problem more complex for "fetch") (cherry picked from commit 1fede04b83288d8a91304a83b7601699bb5cba04) Conflicts: options/locale/locale_en-US.ini tests/integration/repo_branch_test.go trivial context conflicts (cherry picked from commit 1ae3b127fc74906f0722caf6486890cf32d23715) --- options/locale/locale_en-US.ini | 1 - routers/web/web.go | 2 + services/context/context.go | 6 +- services/context/csrf.go | 192 ++++++++------------------ tests/integration/attachment_test.go | 3 +- tests/integration/csrf_test.go | 26 +--- tests/integration/repo_branch_test.go | 12 +- 7 files changed, 71 insertions(+), 171 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6b732fb121..61a820774d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -231,7 +231,6 @@ string.desc = Z - A [error] occurred = An error occurred report_message = If you believe that this is a Forgejo bug, please search for issues on Codeberg or open a new issue if necessary. -invalid_csrf = Bad Request: invalid CSRF token not_found = The target couldn't be found. network_error = Network error server_internal = Internal server error diff --git a/routers/web/web.go b/routers/web/web.go index d174b4e251..39116b882d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -132,6 +132,8 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) { // ensure the session uid is deleted _ = ctx.Session.Delete("uid") } + + ctx.Csrf.PrepareForSessionUser(ctx) } } diff --git a/services/context/context.go b/services/context/context.go index c0819ab11e..91e7b1849d 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -127,10 +127,8 @@ func Contexter() func(next http.Handler) http.Handler { csrfOpts := CsrfOptions{ Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), Cookie: setting.CSRFCookieName, - SetCookie: true, Secure: setting.SessionConfig.Secure, CookieHTTPOnly: setting.CSRFCookieHTTPOnly, - Header: "X-Csrf-Token", CookieDomain: setting.SessionConfig.Domain, CookiePath: setting.SessionConfig.CookiePath, SameSite: setting.SessionConfig.SameSite, @@ -156,7 +154,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Base.AppendContextValue(WebContextKey, ctx) ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) - ctx.Csrf = PrepareCSRFProtector(csrfOpts, ctx) + ctx.Csrf = NewCSRFProtector(csrfOpts) // Get the last flash message from cookie lastFlashCookie := middleware.GetSiteCookie(ctx.Req, CookieNameFlash) @@ -193,8 +191,6 @@ func Contexter() func(next http.Handler) http.Handler { ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["SystemConfig"] = setting.Config() - ctx.Data["CsrfToken"] = ctx.Csrf.GetToken() - ctx.Data["CsrfTokenHtml"] = template.HTML(``) // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations diff --git a/services/context/csrf.go b/services/context/csrf.go index 57c55e6550..5890d53f42 100644 --- a/services/context/csrf.go +++ b/services/context/csrf.go @@ -20,64 +20,42 @@ package context import ( - "encoding/base32" - "fmt" + "html/template" "net/http" "strconv" "time" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web/middleware" +) + +const ( + CsrfHeaderName = "X-Csrf-Token" + CsrfFormName = "_csrf" ) // CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. type CSRFProtector interface { - // GetHeaderName returns HTTP header to search for token. - GetHeaderName() string - // GetFormName returns form value to search for token. - GetFormName() string - // GetToken returns the token. - GetToken() string - // Validate validates the token in http context. + // PrepareForSessionUser prepares the csrf protector for the current session user. + PrepareForSessionUser(ctx *Context) + // Validate validates the csrf token in http context. Validate(ctx *Context) - // DeleteCookie deletes the cookie + // DeleteCookie deletes the csrf cookie DeleteCookie(ctx *Context) } type csrfProtector struct { opt CsrfOptions - // Token generated to pass via header, cookie, or hidden form value. - Token string - // This value must be unique per user. - ID string -} - -// GetHeaderName returns the name of the HTTP header for csrf token. -func (c *csrfProtector) GetHeaderName() string { - return c.opt.Header -} - -// GetFormName returns the name of the form value for csrf token. -func (c *csrfProtector) GetFormName() string { - return c.opt.Form -} - -// GetToken returns the current token. This is typically used -// to populate a hidden form in an HTML template. -func (c *csrfProtector) GetToken() string { - return c.Token + // id must be unique per user. + id string + // token is the valid one which wil be used by end user and passed via header, cookie, or hidden form value. + token string } // CsrfOptions maintains options to manage behavior of Generate. type CsrfOptions struct { // The global secret value used to generate Tokens. Secret string - // HTTP header used to set and get token. - Header string - // Form value used to set and get token. - Form string // Cookie value used to set and get token. Cookie string // Cookie domain. @@ -87,103 +65,64 @@ type CsrfOptions struct { CookieHTTPOnly bool // SameSite set the cookie SameSite type SameSite http.SameSite - // Key used for getting the unique ID per user. - SessionKey string - // oldSessionKey saves old value corresponding to SessionKey. - oldSessionKey string - // If true, send token via X-Csrf-Token header. - SetHeader bool - // If true, send token via _csrf cookie. - SetCookie bool // Set the Secure flag to true on the cookie. Secure bool - // Disallow Origin appear in request header. - Origin bool - // Cookie lifetime. Default is 0 - CookieLifeTime int + // sessionKey is the key used for getting the unique ID per user. + sessionKey string + // oldSessionKey saves old value corresponding to sessionKey. + oldSessionKey string } -func prepareDefaultCsrfOptions(opt CsrfOptions) CsrfOptions { - if opt.Secret == "" { - randBytes, err := util.CryptoRandomBytes(8) - if err != nil { - // this panic can be handled by the recover() in http handlers - panic(fmt.Errorf("failed to generate random bytes: %w", err)) - } - opt.Secret = base32.StdEncoding.EncodeToString(randBytes) - } - if opt.Header == "" { - opt.Header = "X-Csrf-Token" - } - if opt.Form == "" { - opt.Form = "_csrf" - } - if opt.Cookie == "" { - opt.Cookie = "_csrf" - } - if opt.CookiePath == "" { - opt.CookiePath = "/" - } - if opt.SessionKey == "" { - opt.SessionKey = "uid" - } - if opt.CookieLifeTime == 0 { - opt.CookieLifeTime = int(CsrfTokenTimeout.Seconds()) - } - - opt.oldSessionKey = "_old_" + opt.SessionKey - return opt -} - -func newCsrfCookie(c *csrfProtector, value string) *http.Cookie { +func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie { return &http.Cookie{ - Name: c.opt.Cookie, + Name: opt.Cookie, Value: value, - Path: c.opt.CookiePath, - Domain: c.opt.CookieDomain, - MaxAge: c.opt.CookieLifeTime, - Secure: c.opt.Secure, - HttpOnly: c.opt.CookieHTTPOnly, - SameSite: c.opt.SameSite, + Path: opt.CookiePath, + Domain: opt.CookieDomain, + MaxAge: int(CsrfTokenTimeout.Seconds()), + Secure: opt.Secure, + HttpOnly: opt.CookieHTTPOnly, + SameSite: opt.SameSite, } } -// PrepareCSRFProtector returns a CSRFProtector to be used for every request. -// Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie. -func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector { - opt = prepareDefaultCsrfOptions(opt) - x := &csrfProtector{opt: opt} - - if opt.Origin && len(ctx.Req.Header.Get("Origin")) > 0 { - return x +func NewCSRFProtector(opt CsrfOptions) CSRFProtector { + if opt.Secret == "" { + panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code } + opt.Cookie = util.IfZero(opt.Cookie, "_csrf") + opt.CookiePath = util.IfZero(opt.CookiePath, "/") + opt.sessionKey = "uid" + opt.oldSessionKey = "_old_" + opt.sessionKey + return &csrfProtector{opt: opt} +} - x.ID = "0" - uidAny := ctx.Session.Get(opt.SessionKey) - if uidAny != nil { +func (c *csrfProtector) PrepareForSessionUser(ctx *Context) { + c.id = "0" + if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil { switch uidVal := uidAny.(type) { case string: - x.ID = uidVal + c.id = uidVal case int64: - x.ID = strconv.FormatInt(uidVal, 10) + c.id = strconv.FormatInt(uidVal, 10) default: log.Error("invalid uid type in session: %T", uidAny) } } - oldUID := ctx.Session.Get(opt.oldSessionKey) - uidChanged := oldUID == nil || oldUID.(string) != x.ID - cookieToken := ctx.GetSiteCookie(opt.Cookie) + oldUID := ctx.Session.Get(c.opt.oldSessionKey) + uidChanged := oldUID == nil || oldUID.(string) != c.id + cookieToken := ctx.GetSiteCookie(c.opt.Cookie) needsNew := true if uidChanged { - _ = ctx.Session.Set(opt.oldSessionKey, x.ID) + _ = ctx.Session.Set(c.opt.oldSessionKey, c.id) } else if cookieToken != "" { // If cookie token presents, reuse existing unexpired token, else generate a new one. if issueTime, ok := ParseCsrfToken(cookieToken); ok { dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time. if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval { - x.Token = cookieToken + c.token = cookieToken needsNew = false } } @@ -191,42 +130,33 @@ func PrepareCSRFProtector(opt CsrfOptions, ctx *Context) CSRFProtector { if needsNew { // FIXME: actionId. - x.Token = GenerateCsrfToken(x.opt.Secret, x.ID, "POST", time.Now()) - if opt.SetCookie { - cookie := newCsrfCookie(x, x.Token) - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) - } + c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now()) + cookie := newCsrfCookie(&c.opt, c.token) + ctx.Resp.Header().Add("Set-Cookie", cookie.String()) } - if opt.SetHeader { - ctx.Resp.Header().Add(opt.Header, x.Token) - } - return x + ctx.Data["CsrfToken"] = c.token + ctx.Data["CsrfTokenHtml"] = template.HTML(``) } func (c *csrfProtector) validateToken(ctx *Context, token string) { - if !ValidCsrfToken(token, c.opt.Secret, c.ID, "POST", time.Now()) { + if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) { c.DeleteCookie(ctx) - if middleware.IsAPIPath(ctx.Req) { - // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. - http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) - } else { - ctx.Flash.Error(ctx.Tr("error.invalid_csrf")) - ctx.Redirect(setting.AppSubURL + "/") - } + // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. + // FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch) + http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) } } // Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token" // HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated. -// If this validation fails, custom Error is sent in the reply. -// If neither a header nor form value is found, http.StatusBadRequest is sent. +// If this validation fails, http.StatusBadRequest is sent. func (c *csrfProtector) Validate(ctx *Context) { - if token := ctx.Req.Header.Get(c.GetHeaderName()); token != "" { + if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" { c.validateToken(ctx, token) return } - if token := ctx.Req.FormValue(c.GetFormName()); token != "" { + if token := ctx.Req.FormValue(CsrfFormName); token != "" { c.validateToken(ctx, token) return } @@ -234,9 +164,7 @@ func (c *csrfProtector) Validate(ctx *Context) { } func (c *csrfProtector) DeleteCookie(ctx *Context) { - if c.opt.SetCookie { - cookie := newCsrfCookie(c, "") - cookie.MaxAge = -1 - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) - } + cookie := newCsrfCookie(&c.opt, "") + cookie.MaxAge = -1 + ctx.Resp.Header().Add("Set-Cookie", cookie.String()) } diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go index 95c9c9f753..7cbc2545d5 100644 --- a/tests/integration/attachment_test.go +++ b/tests/integration/attachment_test.go @@ -60,7 +60,8 @@ func createAttachment(t *testing.T, session *TestSession, repoURL, filename stri func TestCreateAnonymousAttachment(t *testing.T) { defer tests.PrepareTestEnv(t)() session := emptyTestSession(t) - createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusSeeOther) + // this test is not right because it just doesn't pass the CSRF validation + createAttachment(t, session, "user2/repo1", "image.png", generateImg(), http.StatusBadRequest) } func TestCreateIssueAttachment(t *testing.T) { diff --git a/tests/integration/csrf_test.go b/tests/integration/csrf_test.go index a789859889..fcb9661b8a 100644 --- a/tests/integration/csrf_test.go +++ b/tests/integration/csrf_test.go @@ -5,12 +5,10 @@ package integration import ( "net/http" - "strings" "testing" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -25,28 +23,12 @@ func TestCsrfProtection(t *testing.T) { req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ "_csrf": "fake_csrf", }) - session.MakeRequest(t, req, http.StatusSeeOther) - - resp := session.MakeRequest(t, req, http.StatusSeeOther) - loc := resp.Header().Get("Location") - assert.Equal(t, setting.AppSubURL+"/", loc) - resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - assert.Equal(t, "Bad Request: invalid CSRF token", - strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), - ) + resp := session.MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "Invalid CSRF token") // test web form csrf via header. TODO: should use an UI api to test req = NewRequest(t, "POST", "/user/settings") req.Header.Add("X-Csrf-Token", "fake_csrf") - session.MakeRequest(t, req, http.StatusSeeOther) - - resp = session.MakeRequest(t, req, http.StatusSeeOther) - loc = resp.Header().Get("Location") - assert.Equal(t, setting.AppSubURL+"/", loc) - resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) - htmlDoc = NewHTMLParser(t, resp.Body) - assert.Equal(t, "Bad Request: invalid CSRF token", - strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), - ) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "Invalid CSRF token") } diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 2aa299479a..df9ea9a97c 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/graceful" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" repo_service "code.gitea.io/gitea/services/repository" @@ -157,15 +156,8 @@ func TestCreateBranchInvalidCSRF(t *testing.T) { "_csrf": "fake_csrf", "new_branch_name": "test", }) - resp := session.MakeRequest(t, req, http.StatusSeeOther) - loc := resp.Header().Get("Location") - assert.Equal(t, setting.AppSubURL+"/", loc) - resp = session.MakeRequest(t, NewRequest(t, "GET", loc), http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - assert.Equal(t, - "Bad Request: invalid CSRF token", - strings.TrimSpace(htmlDoc.doc.Find(".ui.message").Text()), - ) + resp := session.MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), "Invalid CSRF token") } func TestDatabaseMissingABranch(t *testing.T) { From 5b6d8a303d3a6738657238983b75edf547ace0a3 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 22 Sep 2024 12:57:03 +0200 Subject: [PATCH 07/89] Refactor CSRF protector (#32057) (fix forgejo tests) Fix the tests unique to Forgejo that are impacted by the refactor. (cherry picked from commit 6275d1bc5000a09793574696cb18cc5b5c4b07d6) --- services/context/csrf.go | 7 ++-- tests/integration/links_test.go | 24 +++++++++--- tests/integration/oauth_test.go | 66 ++++++++++++++++++--------------- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/services/context/csrf.go b/services/context/csrf.go index 5890d53f42..e0518a499b 100644 --- a/services/context/csrf.go +++ b/services/context/csrf.go @@ -30,8 +30,9 @@ import ( ) const ( - CsrfHeaderName = "X-Csrf-Token" - CsrfFormName = "_csrf" + CsrfHeaderName = "X-Csrf-Token" + CsrfFormName = "_csrf" + CsrfErrorString = "Invalid CSRF token." ) // CSRFProtector represents a CSRF protector and is used to get the current token and validate the token. @@ -144,7 +145,7 @@ func (c *csrfProtector) validateToken(ctx *Context, token string) { c.DeleteCookie(ctx) // currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints. // FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch) - http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest) + http.Error(ctx.Resp, CsrfErrorString, http.StatusBadRequest) } } diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go index 68d7008e02..e9ad933b24 100644 --- a/tests/integration/links_test.go +++ b/tests/integration/links_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + forgejo_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -190,11 +191,6 @@ func TestRedirectsWebhooks(t *testing.T) { {from: "/user/settings/hooks/" + kind + "/new", to: "/user/login", verb: "GET"}, {from: "/admin/system-hooks/" + kind + "/new", to: "/user/login", verb: "GET"}, {from: "/admin/default-hooks/" + kind + "/new", to: "/user/login", verb: "GET"}, - {from: "/user2/repo1/settings/hooks/" + kind + "/new", to: "/", verb: "POST"}, - {from: "/admin/system-hooks/" + kind + "/new", to: "/", verb: "POST"}, - {from: "/admin/default-hooks/" + kind + "/new", to: "/", verb: "POST"}, - {from: "/user2/repo1/settings/hooks/1", to: "/", verb: "POST"}, - {from: "/admin/hooks/1", to: "/", verb: "POST"}, } for _, info := range redirects { req := NewRequest(t, info.verb, info.from) @@ -202,6 +198,24 @@ func TestRedirectsWebhooks(t *testing.T) { assert.EqualValues(t, path.Join(setting.AppSubURL, info.to), test.RedirectURL(resp), info.from) } } + + for _, kind := range []string{"forgejo", "gitea"} { + csrf := []struct { + from string + verb string + }{ + {from: "/user2/repo1/settings/hooks/" + kind + "/new", verb: "POST"}, + {from: "/admin/hooks/1", verb: "POST"}, + {from: "/admin/system-hooks/" + kind + "/new", verb: "POST"}, + {from: "/admin/default-hooks/" + kind + "/new", verb: "POST"}, + {from: "/user2/repo1/settings/hooks/1", verb: "POST"}, + } + for _, info := range csrf { + req := NewRequest(t, info.verb, info.from) + resp := MakeRequest(t, req, http.StatusBadRequest) + assert.Contains(t, resp.Body.String(), forgejo_context.CsrfErrorString) + } + } } func TestRepoLinks(t *testing.T) { diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 0d5e9a0472..f385b99e46 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "net/url" "strings" "testing" @@ -24,6 +25,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/routers/web/auth" + forgejo_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" "github.com/markbates/goth" @@ -803,6 +805,16 @@ func TestOAuthIntrospection(t *testing.T) { }) } +func requireCookieCSRF(t *testing.T, resp http.ResponseWriter) string { + for _, c := range resp.(*httptest.ResponseRecorder).Result().Cookies() { + if c.Name == "_csrf" { + return c.Value + } + } + require.True(t, false, "_csrf not found in cookies") + return "" +} + func TestOAuth_GrantScopesReadUser(t *testing.T) { defer tests.PrepareTestEnv(t)() @@ -840,19 +852,18 @@ func TestOAuth_GrantScopesReadUser(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, @@ -921,19 +932,18 @@ func TestOAuth_GrantScopesFailReadRepository(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, @@ -1000,19 +1010,18 @@ func TestOAuth_GrantScopesReadRepository(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, @@ -1082,19 +1091,18 @@ func TestOAuth_GrantScopesReadPrivateGroups(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, @@ -1164,19 +1172,18 @@ func TestOAuth_GrantScopesReadOnlyPublicGroups(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, @@ -1260,19 +1267,18 @@ func TestOAuth_GrantScopesReadPublicGroupsWithTheReadScope(t *testing.T) { authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther) authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&")[0] - htmlDoc := NewHTMLParser(t, authorizeResp.Body) grantReq := NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "client_id": app.ClientID, "redirect_uri": "a", "state": "thestate", "granted": "true", }) - grantResp := ctx.MakeRequest(t, grantReq, http.StatusSeeOther) - htmlDocGrant := NewHTMLParser(t, grantResp.Body) + grantResp := ctx.MakeRequest(t, grantReq, http.StatusBadRequest) + assert.NotContains(t, grantResp.Body.String(), forgejo_context.CsrfErrorString) accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{ - "_csrf": htmlDocGrant.GetCSRF(), + "_csrf": requireCookieCSRF(t, authorizeResp), "grant_type": "authorization_code", "client_id": app.ClientID, "client_secret": app.ClientSecret, From 2f1a737769b2ecd64c7c62e94cabf4bd72e89b69 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 20 Sep 2024 21:00:39 +0200 Subject: [PATCH 08/89] Fix incorrect `/tokens` api (#32085) Fixes #32078 - Add missing scopes output. - Disallow empty scope. --------- Co-authored-by: Lunny Xiao (cherry picked from commit 08adbc468f8875fd4763c3656b334203c11adc0a) (cherry picked from commit 526054332acb221e061d3900bba2dc6e012da52d) --- routers/api/v1/user/app.go | 5 +++++ tests/integration/api_token_test.go | 31 ++++++++++------------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index b61ebac7d0..d5b20f7703 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -118,6 +118,10 @@ func CreateAccessToken(ctx *context.APIContext) { ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err)) return } + if scope == "" { + ctx.Error(http.StatusBadRequest, "AccessTokenScope", "access token must have a scope") + return + } t.Scope = scope if err := auth_model.NewAccessToken(ctx, t); err != nil { @@ -129,6 +133,7 @@ func CreateAccessToken(ctx *context.APIContext) { Token: t.Token, ID: t.ID, TokenLastEight: t.TokenLastEight, + Scopes: t.Scope.StringSlice(), }) } diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go index 9c7bf37330..01d18ef6f1 100644 --- a/tests/integration/api_token_test.go +++ b/tests/integration/api_token_test.go @@ -23,10 +23,10 @@ func TestAPICreateAndDeleteToken(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, nil) + newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) deleteAPIAccessToken(t, newAccessToken, user) - newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, nil) + newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) deleteAPIAccessToken(t, newAccessToken, user) } @@ -72,19 +72,19 @@ func TestAPIDeleteTokensPermission(t *testing.T) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // admin can delete tokens for other users - createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, nil) + createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) req := NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-1"). AddBasicAuth(admin.Name) MakeRequest(t, req, http.StatusNoContent) // non-admin can delete tokens for himself - createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, nil) + createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-2"). AddBasicAuth(user2.Name) MakeRequest(t, req, http.StatusNoContent) // non-admin can't delete tokens for other users - createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, nil) + createAPIAccessTokenWithoutCleanUp(t, "test-key-3", user2, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) req = NewRequest(t, "DELETE", "/api/v1/users/"+user2.LoginName+"/tokens/test-key-3"). AddBasicAuth(user4.Name) MakeRequest(t, req, http.StatusForbidden) @@ -520,7 +520,7 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...) } - accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, &unauthorizedScopes) + accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes) defer deleteAPIAccessToken(t, accessToken, user) // Request the endpoint. Verify that permission is denied. @@ -532,20 +532,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model // createAPIAccessTokenWithoutCleanUp Create an API access token and assert that // creation succeeded. The caller is responsible for deleting the token. -func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes *[]auth_model.AccessTokenScope) api.AccessToken { +func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes []auth_model.AccessTokenScope) api.AccessToken { payload := map[string]any{ - "name": tokenName, - } - if scopes != nil { - for _, scope := range *scopes { - scopes, scopesExists := payload["scopes"].([]string) - if !scopesExists { - scopes = make([]string, 0) - } - scopes = append(scopes, string(scope)) - payload["scopes"] = scopes - } + "name": tokenName, + "scopes": scopes, } + log.Debug("Requesting creation of token with scopes: %v", scopes) req := NewRequestWithJSON(t, "POST", "/api/v1/users/"+user.LoginName+"/tokens", payload). AddBasicAuth(user.Name) @@ -563,8 +555,7 @@ func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *us return newAccessToken } -// createAPIAccessTokenWithoutCleanUp Delete an API access token and assert that -// deletion succeeded. +// deleteAPIAccessToken deletes an API access token and assert that deletion succeeded. func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) { req := NewRequestf(t, "DELETE", "/api/v1/users/"+user.LoginName+"/tokens/%d", accessToken.ID). AddBasicAuth(user.Name) From 81308159fd72fc901cdd4afa0b8b1193bc054532 Mon Sep 17 00:00:00 2001 From: Timon van der Berg Date: Sat, 21 Sep 2024 20:57:01 +0200 Subject: [PATCH 09/89] Repo Activity: count new issues that were closed (#31776) I'm new to go and contributing to gitea, your guidance is much appreciated. This is meant to solve https://github.com/go-gitea/gitea/issues/13309 Previously, closed issues would not be shown under new issues in the activity tab, even if they were newly created. changes: * Split out newlyCreatedIssues from issuesForActivityStatement to count both currently open and closed issues. * Use a seperate function to count active issues to prevent double-counting issues after the above change. Result is that new issues that have been closed are shown both under "new" and "closed". Signed-off-by: Timon van der Berg (cherry picked from commit ebfde845294cc681de6b1fe1adcf27e35f61b89b) (cherry picked from commit 2675a24649af2fff34f5c7e416d6ff78591d8d9c) --- models/activities/repo_activity.go | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index ba5e4959f0..3ffad035b7 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -34,6 +34,7 @@ type ActivityStats struct { OpenedPRAuthorCount int64 MergedPRs issues_model.PullRequestList MergedPRAuthorCount int64 + ActiveIssues issues_model.IssueList OpenedIssues issues_model.IssueList OpenedIssueAuthorCount int64 ClosedIssues issues_model.IssueList @@ -172,7 +173,7 @@ func (stats *ActivityStats) MergedPRPerc() int { // ActiveIssueCount returns total active issue count func (stats *ActivityStats) ActiveIssueCount() int { - return stats.OpenedIssueCount() + stats.ClosedIssueCount() + return len(stats.ActiveIssues) } // OpenedIssueCount returns open issue count @@ -285,13 +286,21 @@ func (stats *ActivityStats) FillIssues(ctx context.Context, repoID int64, fromTi stats.ClosedIssueAuthorCount = count // New issues - sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false) + sess = newlyCreatedIssues(ctx, repoID, fromTime) sess.OrderBy("issue.created_unix ASC") stats.OpenedIssues = make(issues_model.IssueList, 0) if err = sess.Find(&stats.OpenedIssues); err != nil { return err } + // Active issues + sess = activeIssues(ctx, repoID, fromTime) + sess.OrderBy("issue.created_unix ASC") + stats.ActiveIssues = make(issues_model.IssueList, 0) + if err = sess.Find(&stats.ActiveIssues); err != nil { + return err + } + // Opened issue authors sess = issuesForActivityStatement(ctx, repoID, fromTime, false, false) if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil { @@ -317,6 +326,23 @@ func (stats *ActivityStats) FillUnresolvedIssues(ctx context.Context, repoID int return sess.Find(&stats.UnresolvedIssues) } +func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { + sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). + And("issue.is_pull = ?", false). // Retain the is_pull check to exclude pull requests + And("issue.created_unix >= ?", fromTime.Unix()) // Include all issues created after fromTime + + return sess +} + +func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session { + sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). + And("issue.is_pull = ?", false). + And("issue.created_unix >= ?", fromTime.Unix()). + Or("issue.closed_unix >= ?", fromTime.Unix()) + + return sess +} + func issuesForActivityStatement(ctx context.Context, repoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session { sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID). And("issue.is_closed = ?", closed) From 6c16834d281fb66caf3e3f21817ef6464a781117 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 22 Sep 2024 05:56:25 +0800 Subject: [PATCH 10/89] Fix wrong last modify time (#32102) (cherry picked from commit a802508f88e546bf18990559e44bf27a09c869ee) (cherry picked from commit f709de24039ab7e605d3e09e3b61240836381603) --- modules/httpcache/httpcache.go | 3 ++- modules/httplib/serve.go | 1 + routers/api/packages/maven/maven.go | 4 +++- routers/web/repo/githttp.go | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index b4af371541..30ce0a4a03 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -76,7 +76,8 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s w.Header().Set("Etag", etag) } if lastModified != nil && !lastModified.IsZero() { - w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) + // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat + w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat)) } if len(etag) > 0 { diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index 6e147d76f5..2e3e6a7c42 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -79,6 +79,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { httpcache.SetCacheControlInHeader(header, duration) if !opts.LastModified.IsZero() { + // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) } } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 58271e1d43..4181577454 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -117,7 +117,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) latest := pds[len(pds)-1] - ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat)) + // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat + lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModifed) ext := strings.ToLower(filepath.Ext(params.Filename)) if isChecksumExtension(ext) { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 9f3b63698a..a082498dfd 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -395,7 +395,8 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string ctx.Resp.Header().Set("Content-Type", contentType) ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size())) - ctx.Resp.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat)) + // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat + ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat)) http.ServeFile(ctx.Resp, ctx.Req, reqFile) } From 74712e340026f127bcc627cc2342fe5b629c5f00 Mon Sep 17 00:00:00 2001 From: Jamie Schouten Date: Sun, 22 Sep 2024 00:42:17 +0200 Subject: [PATCH 11/89] Add bin to Composer Metadata (#32099) This PR addresses the missing `bin` field in Composer metadata, which currently causes vendor-provided binaries to not be symlinked to `vendor/bin` during installation. In the current implementation, running `composer install` does not publish the binaries, leading to issues where expected binaries are not available. By properly declaring the `bin` field, this PR ensures that binaries are correctly symlinked upon installation, as described in the [Composer documentation](https://getcomposer.org/doc/articles/vendor-binaries.md). (cherry picked from commit d351a42494e71b5e2da63302c2f9b46c78e6dbde) (cherry picked from commit 9d3473119893ffde0ab36d98e7a0e41c5d0ba9a3) --- modules/packages/composer/metadata.go | 1 + tests/integration/api_packages_composer_test.go | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/packages/composer/metadata.go b/modules/packages/composer/metadata.go index 2c2e9ebf27..6035eae8ca 100644 --- a/modules/packages/composer/metadata.go +++ b/modules/packages/composer/metadata.go @@ -48,6 +48,7 @@ type Metadata struct { Homepage string `json:"homepage,omitempty"` License Licenses `json:"license,omitempty"` Authors []Author `json:"authors,omitempty"` + Bin []string `json:"bin,omitempty"` Autoload map[string]any `json:"autoload,omitempty"` AutoloadDev map[string]any `json:"autoload-dev,omitempty"` Extra map[string]any `json:"extra,omitempty"` diff --git a/tests/integration/api_packages_composer_test.go b/tests/integration/api_packages_composer_test.go index 9cdcd07e37..9d25cc4d64 100644 --- a/tests/integration/api_packages_composer_test.go +++ b/tests/integration/api_packages_composer_test.go @@ -37,6 +37,7 @@ func TestPackageComposer(t *testing.T) { packageType := "composer-plugin" packageAuthor := "Gitea Authors" packageLicense := "MIT" + packageBin := "./bin/script" var buf bytes.Buffer archive := zip.NewWriter(&buf) @@ -50,6 +51,9 @@ func TestPackageComposer(t *testing.T) { { "name": "` + packageAuthor + `" } + ], + "bin": [ + "` + packageBin + `" ] }`)) archive.Close() @@ -211,6 +215,8 @@ func TestPackageComposer(t *testing.T) { assert.Len(t, pkgs[0].Authors, 1) assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name) assert.Equal(t, "zip", pkgs[0].Dist.Type) - assert.Equal(t, "7b40bfd6da811b2b78deec1e944f156dbb2c747b", pkgs[0].Dist.Checksum) + assert.Equal(t, "4f5fa464c3cb808a1df191dbf6cb75363f8b7072", pkgs[0].Dist.Checksum) + assert.Len(t, pkgs[0].Bin, 1) + assert.Equal(t, packageBin, pkgs[0].Bin[0]) }) } From ba7da0af3184e7f732ea5eec4a01c6fdd1b899ab Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 21 Sep 2024 17:50:54 +0800 Subject: [PATCH 12/89] Use camo.Always instead of camo.Allways (#32097) Fix #31575 https://gitea.com/gitea/docs/pulls/73 (cherry picked from commit 8e2dd5d3ddfb442937c79f05df88d18b856952cb) (cherry picked from commit 2ffb08bb888bb950b6ed4ff12a4cf4bacc337e18) --- custom/conf/app.example.ini | 3 ++- modules/markup/camo.go | 2 +- modules/markup/camo_test.go | 2 +- modules/setting/camo.go | 14 ++++++++++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9cb5a67172..2eff51fe98 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -529,7 +529,8 @@ INTERNAL_TOKEN = ;; HMAC to encode urls with, it **is required** if camo is enabled. ;HMAC_KEY = ;; Set to true to use camo for https too lese only non https urls are proxyed -;ALLWAYS = false +;; ALLWAYS is deprecated and will be removed in the future +;ALWAYS = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/markup/camo.go b/modules/markup/camo.go index e93797de2b..7e2583469d 100644 --- a/modules/markup/camo.go +++ b/modules/markup/camo.go @@ -38,7 +38,7 @@ func camoHandleLink(link string) string { if setting.Camo.Enabled { lnkURL, err := url.Parse(link) if err == nil && lnkURL.IsAbs() && !strings.HasPrefix(link, setting.AppURL) && - (setting.Camo.Allways || lnkURL.Scheme != "https") { + (setting.Camo.Always || lnkURL.Scheme != "https") { return CamoEncode(link) } } diff --git a/modules/markup/camo_test.go b/modules/markup/camo_test.go index ba58835221..3c5d40afa0 100644 --- a/modules/markup/camo_test.go +++ b/modules/markup/camo_test.go @@ -28,7 +28,7 @@ func TestCamoHandleLink(t *testing.T) { "https://image.proxy/eivin43gJwGVIjR9MiYYtFIk0mw/aHR0cDovL3Rlc3RpbWFnZXMub3JnL2ltZy5qcGc", camoHandleLink("http://testimages.org/img.jpg")) - setting.Camo.Allways = true + setting.Camo.Always = true assert.Equal(t, "https://gitea.com/img.jpg", camoHandleLink("https://gitea.com/img.jpg")) diff --git a/modules/setting/camo.go b/modules/setting/camo.go index 366e9a116c..608ecf8363 100644 --- a/modules/setting/camo.go +++ b/modules/setting/camo.go @@ -3,18 +3,28 @@ package setting -import "code.gitea.io/gitea/modules/log" +import ( + "strconv" + + "code.gitea.io/gitea/modules/log" +) var Camo = struct { Enabled bool ServerURL string `ini:"SERVER_URL"` HMACKey string `ini:"HMAC_KEY"` - Allways bool + Always bool }{} func loadCamoFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "camo", &Camo) if Camo.Enabled { + oldValue := rootCfg.Section("camo").Key("ALLWAYS").MustString("") + if oldValue != "" { + log.Warn("camo.ALLWAYS is deprecated, use camo.ALWAYS instead") + Camo.Always, _ = strconv.ParseBool(oldValue) + } + if Camo.ServerURL == "" || Camo.HMACKey == "" { log.Fatal(`Camo settings require "SERVER_URL" and HMAC_KEY`) } From eb4f1de8ec64a078971de8be6070b81ce74b6c1d Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 22 Sep 2024 10:01:43 +0200 Subject: [PATCH 13/89] chore(release-notes): weekly cherry-pick week 2024-39 (cherry picked from commit e3deb88a8d2494e386f221b9dc743a6a1710837d) --- release-notes/5372.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 release-notes/5372.md diff --git a/release-notes/5372.md b/release-notes/5372.md new file mode 100644 index 0000000000..fccb305f34 --- /dev/null +++ b/release-notes/5372.md @@ -0,0 +1,5 @@ +feat: [commit](https://codeberg.org/forgejo/forgejo/commit/9d3473119893ffde0ab36d98e7a0e41c5d0ba9a3) Add bin to Composer Metadata. +fix: [commit](https://codeberg.org/forgejo/forgejo/commit/f709de24039ab7e605d3e09e3b61240836381603) Fix wrong last modify time. +fix: [commit](https://codeberg.org/forgejo/forgejo/commit/2675a24649af2fff34f5c7e416d6ff78591d8d9c) Repo Activity: count new issues that were closed. +fix: [commit](https://codeberg.org/forgejo/forgejo/commit/526054332acb221e061d3900bba2dc6e012da52d) Fix incorrect /tokens api. +fix: [commit](https://codeberg.org/forgejo/forgejo/commit/0cafec4c7a2faf810953e9d522faf5dc019e1522) Do not escape relative path in RPM primary index. From 658ed564cbfd4b95e2985dc2e15bedad33e8a6ee Mon Sep 17 00:00:00 2001 From: Exploding Dragon Date: Fri, 27 Sep 2024 08:21:22 +0000 Subject: [PATCH 14/89] feat: add architecture-specific removal support for arch package (#5351) - [x] add architecture-specific removal support - [x] Fix upload competition - [x] Fix not checking input when downloading docs: https://codeberg.org/forgejo/docs/pulls/874 ### Release notes - [ ] I do not want this change to show in the release notes. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5351 Reviewed-by: Earl Warren Co-authored-by: Exploding Dragon Co-committed-by: Exploding Dragon (cherry picked from commit 89742c49135e47372e272596bd536d5d1244721f) --- modules/packages/arch/metadata.go | 69 ++++++++++----------- routers/api/packages/api.go | 12 ++-- routers/api/packages/arch/arch.go | 47 +++++++++----- services/packages/arch/repository.go | 65 ++++++++++--------- templates/package/content/arch.tmpl | 4 +- templates/package/metadata/arch.tmpl | 4 +- tests/integration/api_packages_arch_test.go | 50 ++++++++++++--- 7 files changed, 151 insertions(+), 100 deletions(-) diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index 0e08670311..6cdde75cdc 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -39,8 +39,8 @@ const ( var ( reName = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+$`) reVer = regexp.MustCompile(`^[a-zA-Z0-9:_.+]+-+[0-9]+$`) - reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+([<>]?=?[a-zA-Z0-9@._+-]+)?(:.*)?$`) - rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+([<>]?=?[a-zA-Z0-9@._+-]+)?$`) + reOptDep = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+([<>]?=?([0-9]+:)?[a-zA-Z0-9@._+-]+)?(:.*)?$`) + rePkgVer = regexp.MustCompile(`^[a-zA-Z0-9@._+-]+([<>]?=?([0-9]+:)?[a-zA-Z0-9@._+-]+)?$`) magicZSTD = []byte{0x28, 0xB5, 0x2F, 0xFD} magicXZ = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A} @@ -71,7 +71,7 @@ type VersionMetadata struct { Conflicts []string `json:"conflicts,omitempty"` Replaces []string `json:"replaces,omitempty"` Backup []string `json:"backup,omitempty"` - Xdata []string `json:"xdata,omitempty"` + XData []string `json:"xdata,omitempty"` } // FileMetadata Metadata related to specific package file. @@ -125,7 +125,7 @@ func ParsePackage(r *packages.HashedBuffer) (*Package, error) { defer tarball.Close() var pkg *Package - var mtree bool + var mTree bool for { f, err := tarball.Read() @@ -135,24 +135,24 @@ func ParsePackage(r *packages.HashedBuffer) (*Package, error) { if err != nil { return nil, err } - defer f.Close() - switch f.Name() { case ".PKGINFO": pkg, err = ParsePackageInfo(tarballType, f) if err != nil { + _ = f.Close() return nil, err } case ".MTREE": - mtree = true + mTree = true } + _ = f.Close() } if pkg == nil { return nil, util.NewInvalidArgumentErrorf(".PKGINFO file not found") } - if !mtree { + if !mTree { return nil, util.NewInvalidArgumentErrorf(".MTREE file not found") } @@ -220,7 +220,7 @@ func ParsePackageInfo(compressType string, r io.Reader) (*Package, error) { case "replaces": p.VersionMetadata.Replaces = append(p.VersionMetadata.Replaces, value) case "xdata": - p.VersionMetadata.Xdata = append(p.VersionMetadata.Xdata, value) + p.VersionMetadata.XData = append(p.VersionMetadata.XData, value) case "builddate": bd, err := strconv.ParseInt(value, 10, 64) if err != nil { @@ -260,48 +260,43 @@ func ValidatePackageSpec(p *Package) error { return util.NewInvalidArgumentErrorf("invalid project URL") } } - for _, cd := range p.VersionMetadata.CheckDepends { - if !rePkgVer.MatchString(cd) { - return util.NewInvalidArgumentErrorf("invalid check dependency: %s", cd) + for _, checkDepend := range p.VersionMetadata.CheckDepends { + if !rePkgVer.MatchString(checkDepend) { + return util.NewInvalidArgumentErrorf("invalid check dependency: %s", checkDepend) } } - for _, d := range p.VersionMetadata.Depends { - if !rePkgVer.MatchString(d) { - return util.NewInvalidArgumentErrorf("invalid dependency: %s", d) + for _, depend := range p.VersionMetadata.Depends { + if !rePkgVer.MatchString(depend) { + return util.NewInvalidArgumentErrorf("invalid dependency: %s", depend) } } - for _, md := range p.VersionMetadata.MakeDepends { - if !rePkgVer.MatchString(md) { - return util.NewInvalidArgumentErrorf("invalid make dependency: %s", md) + for _, makeDepend := range p.VersionMetadata.MakeDepends { + if !rePkgVer.MatchString(makeDepend) { + return util.NewInvalidArgumentErrorf("invalid make dependency: %s", makeDepend) } } - for _, p := range p.VersionMetadata.Provides { - if !rePkgVer.MatchString(p) { - return util.NewInvalidArgumentErrorf("invalid provides: %s", p) + for _, provide := range p.VersionMetadata.Provides { + if !rePkgVer.MatchString(provide) { + return util.NewInvalidArgumentErrorf("invalid provides: %s", provide) } } - for _, p := range p.VersionMetadata.Conflicts { - if !rePkgVer.MatchString(p) { - return util.NewInvalidArgumentErrorf("invalid conflicts: %s", p) + for _, conflict := range p.VersionMetadata.Conflicts { + if !rePkgVer.MatchString(conflict) { + return util.NewInvalidArgumentErrorf("invalid conflicts: %s", conflict) } } - for _, p := range p.VersionMetadata.Replaces { - if !rePkgVer.MatchString(p) { - return util.NewInvalidArgumentErrorf("invalid replaces: %s", p) + for _, replace := range p.VersionMetadata.Replaces { + if !rePkgVer.MatchString(replace) { + return util.NewInvalidArgumentErrorf("invalid replaces: %s", replace) } } - for _, p := range p.VersionMetadata.Replaces { - if !rePkgVer.MatchString(p) { - return util.NewInvalidArgumentErrorf("invalid xdata: %s", p) + for _, optDepend := range p.VersionMetadata.OptDepends { + if !reOptDep.MatchString(optDepend) { + return util.NewInvalidArgumentErrorf("invalid optional dependency: %s", optDepend) } } - for _, od := range p.VersionMetadata.OptDepends { - if !reOptDep.MatchString(od) { - return util.NewInvalidArgumentErrorf("invalid optional dependency: %s", od) - } - } - for _, bf := range p.VersionMetadata.Backup { - if strings.HasPrefix(bf, "/") { + for _, b := range p.VersionMetadata.Backup { + if strings.HasPrefix(b, "/") { return util.NewInvalidArgumentErrorf("backup file contains leading forward slash") } } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 76a8fd4714..c72f812704 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -175,18 +175,20 @@ func CommonRoutes() *web.Route { arch.PushPackage(ctx) return } else if isDelete { - if groupLen < 2 { + if groupLen < 3 { ctx.Status(http.StatusBadRequest) return } - if groupLen == 2 { + if groupLen == 3 { ctx.SetParams("group", "") ctx.SetParams("package", pathGroups[0]) ctx.SetParams("version", pathGroups[1]) + ctx.SetParams("arch", pathGroups[2]) } else { - ctx.SetParams("group", strings.Join(pathGroups[:groupLen-2], "/")) - ctx.SetParams("package", pathGroups[groupLen-2]) - ctx.SetParams("version", pathGroups[groupLen-1]) + ctx.SetParams("group", strings.Join(pathGroups[:groupLen-3], "/")) + ctx.SetParams("package", pathGroups[groupLen-3]) + ctx.SetParams("version", pathGroups[groupLen-2]) + ctx.SetParams("arch", pathGroups[groupLen-1]) } reqPackageAccess(perm.AccessModeWrite)(ctx) if ctx.Written() { diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 2d3481a33f..15fcc37c70 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -9,12 +9,14 @@ import ( "fmt" "io" "net/http" + "path/filepath" "regexp" "strings" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" arch_module "code.gitea.io/gitea/modules/packages/arch" + "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" @@ -25,6 +27,8 @@ import ( var ( archPkgOrSig = regexp.MustCompile(`^.*\.pkg\.tar\.\w+(\.sig)*$`) archDBOrSig = regexp.MustCompile(`^.*.db(\.tar\.gz)*(\.sig)*$`) + + locker = sync.NewExclusivePool() ) func apiError(ctx *context.Context, status int, obj any) { @@ -33,6 +37,14 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func refreshLocker(ctx *context.Context, group string) func() { + key := fmt.Sprintf("pkg_%d_arch_pkg_%s", ctx.Package.Owner.ID, group) + locker.CheckIn(key) + return func() { + locker.CheckOut(key) + } +} + func GetRepositoryKey(ctx *context.Context) { _, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID) if err != nil { @@ -48,7 +60,8 @@ func GetRepositoryKey(ctx *context.Context) { func PushPackage(ctx *context.Context) { group := ctx.Params("group") - + releaser := refreshLocker(ctx, group) + defer releaser() upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -154,6 +167,7 @@ func PushPackage(ctx *context.Context) { }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) + return } if err = arch_service.BuildPacmanDB(ctx, ctx.Package.Owner.ID, group, p.FileMetadata.Arch); err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -169,7 +183,7 @@ func GetPackageOrDB(ctx *context.Context) { arch = ctx.Params("arch") ) if archPkgOrSig.MatchString(file) { - pkg, err := arch_service.GetPackageFile(ctx, group, file, ctx.Package.Owner.ID) + pkg, u, pf, err := arch_service.GetPackageFile(ctx, group, file, ctx.Package.Owner.ID) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) @@ -178,15 +192,12 @@ func GetPackageOrDB(ctx *context.Context) { } return } - - ctx.ServeContent(pkg, &context.ServeHeaderOptions{ - Filename: file, - }) + helper.ServePackageFile(ctx, pkg, u, pf) return } if archDBOrSig.MatchString(file) { - pkg, err := arch_service.GetPackageDBFile(ctx, group, arch, ctx.Package.Owner.ID, + pkg, u, pf, err := arch_service.GetPackageDBFile(ctx, group, arch, ctx.Package.Owner.ID, strings.HasSuffix(file, ".sig")) if err != nil { if errors.Is(err, util.ErrNotExist) { @@ -196,9 +207,7 @@ func GetPackageOrDB(ctx *context.Context) { } return } - ctx.ServeContent(pkg, &context.ServeHeaderOptions{ - Filename: file, - }) + helper.ServePackageFile(ctx, pkg, u, pf) return } @@ -207,10 +216,13 @@ func GetPackageOrDB(ctx *context.Context) { func RemovePackage(ctx *context.Context) { var ( - group = ctx.Params("group") - pkg = ctx.Params("package") - ver = ctx.Params("version") + group = ctx.Params("group") + pkg = ctx.Params("package") + ver = ctx.Params("version") + pkgArch = ctx.Params("arch") ) + releaser := refreshLocker(ctx, group) + defer releaser() pv, err := packages_model.GetVersionByNameAndVersion( ctx, ctx.Package.Owner.ID, packages_model.TypeArch, pkg, ver, ) @@ -229,7 +241,13 @@ func RemovePackage(ctx *context.Context) { } deleted := false for _, file := range files { - if file.CompositeKey == group { + extName := fmt.Sprintf("-%s.pkg.tar%s", pkgArch, filepath.Ext(file.LowerName)) + if strings.HasSuffix(file.LowerName, ".sig") { + extName = fmt.Sprintf("-%s.pkg.tar%s.sig", pkgArch, + filepath.Ext(strings.TrimSuffix(file.LowerName, filepath.Ext(file.LowerName)))) + } + if file.CompositeKey == group && + strings.HasSuffix(file.LowerName, extName) { deleted = true err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.ContextUser, file) if err != nil { @@ -242,6 +260,7 @@ func RemovePackage(ctx *context.Context) { err = arch_service.BuildCustomRepositoryFiles(ctx, ctx.Package.Owner.ID, group) if err != nil { apiError(ctx, http.StatusInternalServerError, err) + return } ctx.Status(http.StatusNoContent) } else { diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go index de72467421..58433ab5c2 100644 --- a/services/packages/arch/repository.go +++ b/services/packages/arch/repository.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "net/url" "os" "path/filepath" "sort" @@ -20,6 +21,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" arch_module "code.gitea.io/gitea/modules/packages/arch" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" @@ -28,6 +30,8 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/packet" ) +var locker = sync.NewExclusivePool() + func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) { return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion) } @@ -101,6 +105,9 @@ func NewFileSign(ctx context.Context, ownerID int64, input io.Reader) (*packages // BuildPacmanDB Create db signature cache func BuildPacmanDB(ctx context.Context, ownerID int64, group, arch string) error { + key := fmt.Sprintf("pkg_%d_arch_db_%s", ownerID, group) + locker.CheckIn(key) + defer locker.CheckOut(key) pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { return err @@ -173,15 +180,18 @@ func createDB(ctx context.Context, ownerID int64, group, arch string) (*packages if err != nil { return nil, err } + defer db.Close() gw := gzip.NewWriter(db) + defer gw.Close() tw := tar.NewWriter(gw) + defer tw.Close() count := 0 for _, pkg := range pkgs { versions, err := packages_model.GetVersionsByPackageName( ctx, ownerID, packages_model.TypeArch, pkg.Name, ) if err != nil { - return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err) + return nil, err } sort.Slice(versions, func(i, j int) bool { return versions[i].CreatedUnix > versions[j].CreatedUnix @@ -190,7 +200,7 @@ func createDB(ctx context.Context, ownerID int64, group, arch string) (*packages for _, ver := range versions { files, err := packages_model.GetFilesByVersionID(ctx, ver.ID) if err != nil { - return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err) + return nil, err } var pf *packages_model.PackageFile for _, file := range files { @@ -213,7 +223,7 @@ func createDB(ctx context.Context, ownerID int64, group, arch string) (*packages ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyDescription, ) if err != nil { - return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err) + return nil, err } if len(pps) >= 1 { meta := []byte(pps[0].Value) @@ -223,60 +233,50 @@ func createDB(ctx context.Context, ownerID int64, group, arch string) (*packages Mode: int64(os.ModePerm), } if err = tw.WriteHeader(header); err != nil { - return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err) + return nil, err } if _, err := tw.Write(meta); err != nil { - return nil, errors.Join(tw.Close(), gw.Close(), db.Close(), err) + return nil, err } count++ break } } } - defer gw.Close() - defer tw.Close() if count == 0 { - return nil, errors.Join(db.Close(), io.EOF) + return nil, io.EOF } return db, nil } // GetPackageFile Get data related to provided filename and distribution, for package files // update download counter. -func GetPackageFile(ctx context.Context, group, file string, ownerID int64) (io.ReadSeekCloser, error) { - pf, err := getPackageFile(ctx, group, file, ownerID) - if err != nil { - return nil, err +func GetPackageFile(ctx context.Context, group, file string, ownerID int64) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { + fileSplit := strings.Split(file, "-") + if len(fileSplit) <= 3 { + return nil, nil, nil, errors.New("invalid file format, need ---.pkg.") } - - filestream, _, _, err := packages_service.GetPackageFileStream(ctx, pf) - return filestream, err -} - -// Ejects parameters required to get package file property from file name. -func getPackageFile(ctx context.Context, group, file string, ownerID int64) (*packages_model.PackageFile, error) { var ( - splt = strings.Split(file, "-") - pkgname = strings.Join(splt[0:len(splt)-3], "-") - vername = splt[len(splt)-3] + "-" + splt[len(splt)-2] + pkgName = strings.Join(fileSplit[0:len(fileSplit)-3], "-") + pkgVer = fileSplit[len(fileSplit)-3] + "-" + fileSplit[len(fileSplit)-2] ) - - version, err := packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeArch, pkgname, vername) + version, err := packages_model.GetVersionByNameAndVersion(ctx, ownerID, packages_model.TypeArch, pkgName, pkgVer) if err != nil { - return nil, err + return nil, nil, nil, err } - pkgfile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, group) + pkgFile, err := packages_model.GetFileForVersionByName(ctx, version.ID, file, group) if err != nil { - return nil, err + return nil, nil, nil, err } - return pkgfile, nil + + return packages_service.GetPackageFileStream(ctx, pkgFile) } -func GetPackageDBFile(ctx context.Context, group, arch string, ownerID int64, signFile bool) (io.ReadSeekCloser, error) { +func GetPackageDBFile(ctx context.Context, group, arch string, ownerID int64, signFile bool) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { pv, err := GetOrCreateRepositoryVersion(ctx, ownerID) if err != nil { - return nil, err + return nil, nil, nil, err } fileName := fmt.Sprintf("%s.db", arch) if signFile { @@ -284,10 +284,9 @@ func GetPackageDBFile(ctx context.Context, group, arch string, ownerID int64, si } file, err := packages_model.GetFileForVersionByName(ctx, pv.ID, fileName, group) if err != nil { - return nil, err + return nil, nil, nil, err } - filestream, _, _, err := packages_service.GetPackageFileStream(ctx, file) - return filestream, err + return packages_service.GetPackageFileStream(ctx, file) } // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl index bcc24b585b..6138b1d698 100644 --- a/templates/package/content/arch.tmpl +++ b/templates/package/content/arch.tmpl @@ -16,11 +16,11 @@ pacman-key --lsign-key '{{$.SignMail}}'

 {{- if gt (len $.Groups) 1 -}}
-# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi"  $.PackageDescriptor.Package.LowerName}}
+# {{ctx.Locale.Tr "packages.arch.pacman.repo.multi" $.PackageDescriptor.Package.LowerName}}
 
 {{end -}}
 {{- $GroupSize := (len .Groups) -}}
-{{-  range $i,$v :=  .Groups -}}
+{{-  range $i,$v := .Groups -}}
 {{- if gt $i 0}}
 {{end -}}{{- if gt $GroupSize 1 -}}
 # {{ctx.Locale.Tr "packages.arch.pacman.repo.multi.item" .}}
diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl
index 822973eb7d..89001b979c 100644
--- a/templates/package/metadata/arch.tmpl
+++ b/templates/package/metadata/arch.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "arch"}}
-	{{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "gt-mr-3"}} {{.}}
{{end}} - {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "mr-3"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} + {{range .PackageDescriptor.Metadata.License}}
{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}
{{end}} {{end}} diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go index af275de442..2cf0186416 100644 --- a/tests/integration/api_packages_arch_test.go +++ b/tests/integration/api_packages_arch_test.go @@ -14,6 +14,7 @@ import ( "io" "net/http" "strings" + "sync" "testing" "testing/fstest" @@ -258,11 +259,15 @@ HMhNSS1IzUsBcpJAPFAwwUXSM0u4BjoaR8EoGAWjgGQAAILFeyQADAAA AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusCreated) - req = NewRequestWithBody(t, "DELETE", rootURL+"/base/notfound/1.0.0-1", nil). + req = NewRequestWithBody(t, "DELETE", rootURL+"/base/notfound/1.0.0-1/any", nil). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNotFound) - req = NewRequestWithBody(t, "DELETE", groupURL+"/test/1.0.0-1", nil). + req = NewRequestWithBody(t, "DELETE", groupURL+"/test/1.0.0-1/x86_64", nil). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequestWithBody(t, "DELETE", groupURL+"/test/1.0.0-1/any", nil). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) @@ -270,12 +275,22 @@ HMhNSS1IzUsBcpJAPFAwwUXSM0u4BjoaR8EoGAWjgGQAAILFeyQADAAA respPkg := MakeRequest(t, req, http.StatusOK) files, err := listTarGzFiles(respPkg.Body.Bytes()) require.NoError(t, err) - require.Len(t, files, 1) // other pkg in L225 + require.Len(t, files, 1) - req = NewRequestWithBody(t, "DELETE", groupURL+"/test2/1.0.0-1", nil). + req = NewRequestWithBody(t, "DELETE", groupURL+"/test2/1.0.0-1/any", nil). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) - req = NewRequest(t, "GET", groupURL+"/x86_64/base.db") + + req = NewRequest(t, "GET", groupURL+"/x86_64/base.db"). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequestWithBody(t, "DELETE", groupURL+"/test/1.0.0-1/aarch64", nil). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", groupURL+"/aarch64/base.db"). + AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNotFound) }) @@ -294,12 +309,33 @@ HMhNSS1IzUsBcpJAPFAwwUXSM0u4BjoaR8EoGAWjgGQAAILFeyQADAAA resp := MakeRequest(t, req, http.StatusOK) require.Equal(t, pkgs[key], resp.Body.Bytes()) - req = NewRequestWithBody(t, "DELETE", groupURL+"/test2/1.0.0-1", nil). + req = NewRequestWithBody(t, "DELETE", groupURL+"/test2/1.0.0-1/any", nil). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusNoContent) }) } } + t.Run("Concurrent Upload", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + var wg sync.WaitGroup + + targets := []string{"any", "aarch64", "x86_64"} + for _, tag := range targets { + wg.Add(1) + go func(i string) { + defer wg.Done() + req := NewRequestWithBody(t, "PUT", rootURL, bytes.NewReader(pkgs[i])). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusCreated) + }(tag) + } + wg.Wait() + for _, target := range targets { + req := NewRequestWithBody(t, "DELETE", rootURL+"/test/1.0.0-1/"+target, nil). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNoContent) + } + }) } func getProperty(data, key string) string { @@ -318,10 +354,10 @@ func getProperty(data, key string) string { func listTarGzFiles(data []byte) (fstest.MapFS, error) { reader, err := gzip.NewReader(bytes.NewBuffer(data)) - defer reader.Close() if err != nil { return nil, err } + defer reader.Close() tarRead := tar.NewReader(reader) files := make(fstest.MapFS) for { From 7d45c1c6c779ac73825507dcea27689a08925ba9 Mon Sep 17 00:00:00 2001 From: Codeberg Translate Date: Sat, 28 Sep 2024 09:40:29 +0000 Subject: [PATCH 15/89] i18n: update of translations from Codeberg Translate (#5355) Co-authored-by: earl-warren Co-authored-by: Vaclovas Intas Co-authored-by: Zughy Co-authored-by: aleksi Co-authored-by: Application-Maker Co-authored-by: Salif Mehmed Co-authored-by: 0ko <0ko@users.noreply.translate.codeberg.org> Co-authored-by: Fjuro Co-authored-by: Panagiotis \"Ivory\" Vasilopoulos Co-authored-by: claudep Co-authored-by: vri Co-authored-by: nicokaiser Co-authored-by: Outbreak2096 Co-authored-by: robines Co-authored-by: nazrin Co-authored-by: Kaede Fujisaki Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5355 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Co-authored-by: Codeberg Translate Co-committed-by: Codeberg Translate (cherry picked from commit e40554f89baa79d12a1ff89b434041b297afff02) --- options/locale/locale_bg.ini | 25 +++- options/locale/locale_cs-CZ.ini | 38 +++-- options/locale/locale_de-DE.ini | 32 ++++- options/locale/locale_el-GR.ini | 242 ++++++++++++++++---------------- options/locale/locale_fi-FI.ini | 5 + options/locale/locale_fr-FR.ini | 58 +++++--- options/locale/locale_is-IS.ini | 8 +- options/locale/locale_it-IT.ini | 23 +++ options/locale/locale_ja-JP.ini | 7 + options/locale/locale_lt.ini | 77 +++++++++- options/locale/locale_nb_NO.ini | 113 ++++++++++++++- options/locale/locale_ru-RU.ini | 16 ++- options/locale/locale_zh-CN.ini | 34 ++++- 13 files changed, 505 insertions(+), 173 deletions(-) diff --git a/options/locale/locale_bg.ini b/options/locale/locale_bg.ini index 01fb84c3c6..2207129cfa 100644 --- a/options/locale/locale_bg.ini +++ b/options/locale/locale_bg.ini @@ -168,6 +168,7 @@ versions.view_all = Вижте всички dependencies = Зависимости published_by_in = Публикуван %[1]s от %[3]s в %[5]s published_by = Публикуван %[1]s от %[3]s +generic.download = Изтеглете пакета от командния ред: [tool] hours = %d часа @@ -1020,7 +1021,7 @@ pulls.title_desc_one = иска да слее %[1]d подаване от pulls.showing_specified_commit_range = Показани са само промените между %[1]s..%[2]s pulls.merged_title_desc_one = сля %[1]d подаване от %[2]s в %[3]s %[4]s pulls.no_merge_access = Не сте упълномощени за сливане на тази заявка за сливане. -activity.navbar.code_frequency = Честота на кода +activity.navbar.code_frequency = Честота на промените activity.git_stats_pushed_1 = е изтласкал activity.git_stats_push_to_branch = към %s и contributors.contribution_type.commits = Подавания @@ -1183,6 +1184,15 @@ diff.hide_file_tree = Скриване на файловото дърво tag.ahead.target = в %s след този маркер diff.file_image_width = Широчина activity.unresolved_conv_label = Отворено +invisible_runes_line = `Този ред съдържа невидими Уникод знаци` +code.desc = Достъп до програмния код, файловете, подаванията и клоновете. +settings.branches.update_default_branch = Обновяване на стандартния клон +settings.default_branch_desc = Изберете стандартен клон за хранилището, за заявки за сливане и подавания на код: +settings.transfer.button = Прехвърляне на притежанието +settings.transfer.modal.title = Прехвърляне на притежанието +ambiguous_runes_line = `Този ред съдържа двусмислени Уникод знаци` +ambiguous_character = `%[1]c [U+%04[1]X] може да бъде объркан с %[2]c [U+%04[2]X]` +invisible_runes_header = `Този файл съдържа невидими Уникод знаци` [modal] confirm = Потвърждаване @@ -1279,6 +1289,7 @@ members.member = Участник members.private_helper = Да е видим teams.no_desc = Този екип няма описание settings.delete_org_desc = Тази организация ще бъде изтрита перманентно. Продължаване? +open_dashboard = Отваряне на таблото [install] admin_password = Парола @@ -1378,6 +1389,7 @@ followers.title.few = Последователи followers.title.one = Последовател following.title.one = Следван following.title.few = Следвани +public_activity.visibility_hint.self_public = Вашата дейност е видима за всички, с изключение на взаимодействията в частни пространства. Конфигуриране. [home] filter = Други филтри @@ -1544,6 +1556,8 @@ push_tag = изтласка маркер %[3]s към %[3]s#%[2]s` reject_pull_request = `предложи промени за %[3]s#%[2]s` compare_branch = Сравняване +compare_commits_general = Сравняване на подавания +compare_commits = Сравнете %d подавания [auth] tab_openid = OpenID @@ -1572,6 +1586,12 @@ tab_signin = Влизане tab_signup = Регистриране password_pwned = Паролата, която сте избрали, е в списък с откраднати пароли, разкрити преди това при публични пробиви на данни. Моля, опитайте отново с различна парола. confirmation_mail_sent_prompt = Ново ел. писмо за потвърждение е изпратено до %s. За да завършите процеса на регистрация, моля, проверете входящата си кутия и последвайте предоставената връзка в рамките на следващите %s. Ако адресът за ел. поща е неправилен, можете да влезете и да поискате друго ел. писмо за потвърждение да бъде изпратено на различен адрес. +hint_login = Вече имате акаунт? Влезте! +hint_register = Нуждаете се от акаунт? Регистрирайте се. +sign_up_button = Регистрирайте се. +back_to_sign_in = Назад към Вход +sign_in_openid = Продължаване с OpenID +send_reset_mail = Изпращане на ел. писмо за възстановяване [aria] footer.software = Относно този софтуер @@ -1582,7 +1602,7 @@ footer = Долен колонтитул install = Лесен за инсталиране lightweight = Лек license = Отворен код -install_desc = Просто стартирайте двоичния файл за вашата платформа, използвайте Docker, или го получете пакетирано. +install_desc = Просто стартирайте двоичния файл за вашата платформа, използвайте Docker, или го получете пакетиран. app_desc = Безпроблемна Git услуга със самостоятелен хостинг platform = Междуплатформен lightweight_desc = Forgejo има ниски минимални изисквания и може да работи на икономичен Raspberry Pi. Спестете енергията на вашата машина! @@ -1670,6 +1690,7 @@ contributors.what = приноси recent_commits.what = скорошни подавания component_loading = Зареждане на %s... component_loading_info = Това може да отнеме известно време… +code_frequency.what = честота на промените [projects] type-1.display_name = Индивидуален проект diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 9ca8c6b387..4fa19b80f2 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -842,8 +842,8 @@ add_key=Přidat klíč ssh_desc=Tyto veřejné klíče SSH jsou propojeny s vaším účtem. Odpovídající soukromé klíče umožní plný přístup k vašim repozitářům. Klíče SSH, které byly ověřeny, mohou být použity pro ověření Git commitů podepsaných přes SSH. principal_desc=Tyto SSH Principal certifikáty jsou přidruženy k vašemu účtu a umožňují plný přístup do vašich repozitářů. gpg_desc=Tyto veřejné klíče GPG jsou propojeny s vaším účtem a používají se k ověření vašich commitů. Uložte je na bezpečné místo, jelikož umožňují podepsat commity vaší identitou. -ssh_helper=Potřebujete pomoct? Podívejte se do příručky GitHubu na to vytvoření vlastních klíčů SSH nebo vyřešte běžné problémy, se kterými se můžete potkat při použití SSH. -gpg_helper=Potřebujete pomoct? Podívejte se do příručky GitHubu o GPG. +ssh_helper=Potřebujete pomoct? Podívejte se do příručky, jak vytvořit vlastní klíče SSH nebo vyřešte běžné problémy, se kterými se můžete potkat při použití SSH. +gpg_helper=Potřebujete pomoct? Podívejte se do příručky o GPG. add_new_key=Přidat klíč SSH add_new_gpg_key=Přidat klíč GPG key_content_ssh_placeholder=Začíná s „ssh-ed25519“, „ssh-rsa“, „ecdsa-sha2-nistp256“, „ecdsa-sha2-nistp384“, „ecdsa-sha2-nistp521“, „sk-ecdsa-sha2-nistp256@openssh.com“ nebo „sk-ssh-ed25519@openssh.com“ @@ -1427,7 +1427,7 @@ commitstatus.failure=Chyba commitstatus.pending=Čekající commitstatus.success=Úspěch -ext_issues=Přístup k externím problémům +ext_issues=Externí problémy ext_issues.desc=Odkaz na externí systém problémů. projects=Projekty @@ -1608,9 +1608,9 @@ issues.no_content=K dispozici není žádný popis. issues.close=Zavřít problém issues.comment_pull_merged_at=sloučený commit %[1]s do %[2]s %[3]s issues.comment_manually_pull_merged_at=ručně sloučený commit %[1]s do %[2]s %[3]s -issues.close_comment_issue=Okomentovat a zavřít +issues.close_comment_issue=Zavřít s komentářem issues.reopen_issue=Znovu otevřít -issues.reopen_comment_issue=Okomentovat a znovu otevřít +issues.reopen_comment_issue=Znovu otevřít s komentářem issues.create_comment=Okomentovat issues.closed_at=`uzavřel/a tento problém %[2]s` issues.reopened_at=`znovu otevřel/a tento problém %[2]s` @@ -1995,7 +1995,7 @@ signing.wont_sign.commitssigned=Sloučení nebude podepsáno, protože všechny signing.wont_sign.approved=Sloučení nebude podepsáno, protože požadavek na natažení není schválen. signing.wont_sign.not_signed_in=Nejste přihlášeni. -ext_wiki=Přístup k externí Wiki +ext_wiki=Externí wiki ext_wiki.desc=Odkaz do externí Wiki. wiki=Wiki @@ -2436,7 +2436,7 @@ settings.protect_branch_name_pattern=Vzor jména chráněné větve settings.protect_branch_name_pattern_desc=Vzory názvů chráněných větví. Pro vzorovou syntaxi viz dokumentace. Příklady: main, release/** settings.protect_patterns=Vzory settings.protect_protected_file_patterns=Vzory chráněných souborů (oddělené středníkem „;“) -settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na github.com/gobwas/glob dokumentaci pro syntaxi vzoru. Příklady: .drone.yml, /docs/**/*.txt. +settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na dokumentaci %[2]s pro syntaxi vzoru. Příklady: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Vzory nechráněných souborů (oddělené středníkem „;“) settings.protect_unprotected_file_patterns_desc=Nechráněné soubory, které je možné měnit přímo, pokud má uživatel právo zápisu, čímž se obejde omezení push. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na %[2]s dokumentaci pro syntaxi vzoru. Příklady: .drone.yml, /docs/**/*.txt. settings.add_protected_branch=Zapnout ochranu @@ -3822,7 +3822,7 @@ management=Správa tajných klíčů [actions] actions=Akce -unit.desc=Spravovat integrované pipeliny CI/CD pomocí funkce Forgejo Actions +unit.desc=Spravovat integrované pipeliny CI/CD pomocí funkce Forgejo Actions. status.unknown=Neznámý status.waiting=Čekání @@ -3987,4 +3987,24 @@ eib = EiB [translation_meta] -test = diky vsem za pomoc :) \ No newline at end of file +test = diky vsem za pomoc :) + +[repo.permissions] +pulls.write = Zapisovat: Zavírat žádosti o sloučení a spravovat metadata jako štítky, milníky, zpracovatele, data dokončení a závislosti. +packages.write = Zapisovat: Zveřejňovat a mazat balíčky připojené k repozitáři. +projects.read = Číst: Přístup k projektovým nástěnkám repozitáře. +code.write = Zapisovat: Odesílat změny do repozitáře, vytvářet větve a značky. +issues.write = Zapisovat: Zavírat problémy a spravovat metadata jako štítky, milníky, zpracovatele, data dokončení a závislosti. +pulls.read = Číst: Číst a vytvářet žádosti o sloučení. +releases.read = Číst: Zobrazovat a stahovat vydání. +releases.write = Zapisovat: Zveřejňovat, upravovat a mazat vydání a jejich soubory. +wiki.read = Číst: Číst integrovanou wiki a její historii. +wiki.write = Zapisovat: Vytvářet, aktualizovat a mazat stránky v integrované wiki. +projects.write = Zapisovat: Vytvářet projekty a sloupce a upravovat je. +packages.read = Číst: Zobrazovat a stahovat balíčky připojené k repozitáři. +actions.read = Číst: Zobrazovat integrované pipeliny CI/CD a jejich protokoly. +actions.write = Zapisovat: Ručně spouštět, restartovat, rušit nebo schvalovat čekající pipeliny CI/CD. +ext_wiki = Přístup k odkazu v externí wiki. Oprávnění jsou spravována externě. +code.read = Číst: Přístup a klonování kódu v repozitáři. +issues.read = Číst: Číst a vytvářet problémy a komentáře. +ext_issues = Přístup k odkazu v externím sledovacím systému problémů. Oprávnění jsou spravována externě. \ No newline at end of file diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 39b3596aa7..b112ec82c3 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1420,7 +1420,7 @@ commitstatus.failure=Fehler commitstatus.pending=Ausstehend commitstatus.success=Erfolg -ext_issues=Zugriff auf Externe Issues +ext_issues=Externe Issues ext_issues.desc=Link zu externem Issuetracker. projects=Projekte @@ -1601,9 +1601,9 @@ issues.no_content=Keine Beschreibung angegeben. issues.close=Issue schließen issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s zusammengeführt issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell zusammengeführt -issues.close_comment_issue=Kommentieren und schließen +issues.close_comment_issue=Mit Kommentar schließen issues.reopen_issue=Wieder öffnen -issues.reopen_comment_issue=Kommentieren und wieder öffnen +issues.reopen_comment_issue=Mit Kommentar wieder öffnen issues.create_comment=Kommentieren issues.closed_at=`hat diesen Issue %[2]s geschlossen` issues.reopened_at=`hat dieses Issue %[2]s wieder geöffnet` @@ -1984,7 +1984,7 @@ signing.wont_sign.commitssigned=Der Merge-Commit wird nicht signiert werden, da signing.wont_sign.approved=Der Merge-Commit wird nicht signiert werden, da der Pull-Request nicht genehmigt wurde. signing.wont_sign.not_signed_in=Du bist nicht eingeloggt. -ext_wiki=Zugriff auf externes Wiki +ext_wiki=Externes Wiki ext_wiki.desc=Verweis auf externes Wiki. wiki=Wiki @@ -3792,7 +3792,7 @@ management=Secrets verwalten [actions] actions=Actions -unit.desc=Integrierte CI/CD-Pipelines mit Forgejo-Actions verwalten +unit.desc=Integrierte CI/CD-Pipelines mit Forgejo-Actions verwalten. status.unknown=Unbekannt status.waiting=Wartend @@ -3964,4 +3964,24 @@ eib = EiB [translation_meta] -test = ok \ No newline at end of file +test = ok + +[repo.permissions] +code.write = Schreiben: In das Repository pushen, Branches und Tags erstellen. +code.read = Lesen: Zugriff auf das Repository und Klonen. +issues.read = Lesen: Issues und Kommentare lesen und erstellen. +issues.write = Schreiben: Issues schließen und Metadaten wie Labels, Meilensteine, Zuweisungen, Fälligkeitsdaten und Abhängigkeiten verwalten. +pulls.read = Lesen: Pull-Requests lesen und erstellen. +releases.read = Lesen: Releases ansehen und herunterladen. +releases.write = Schreiben: Releases und ihre Assets veröffentlichen, bearbeiten und löschen. +wiki.read = Read: Das integrierte Wiki und seine Historie lesen. +wiki.write = Schreiben: Seiten im integrierten Wiki erstellen, aktualisieren und löschen. +projects.read = Lesen: Zugriff auf Projektboards des Repositories. +projects.write = Schreiben: Projekte und Spalten erstellen und bearbeiten. +packages.read = Lesen: Pakete dieses Repositories betrachten und herunterladen. +packages.write = Schreiben: Pakete dieses Repositories veröffentlichen und löschen. +actions.read = Lesen: Integrierte CI/CD-Pipelines und ihre Logs betrachten. +actions.write = Schreiben: Ausstehende CI/CD-Pipelines manuell auslösen, neustarten, abbrechen oder genehmigen. +ext_issues = Zugriff auf den Link zu einem externen Issue-Tracker. Die Berechtigungen werden extern verwaltet. +ext_wiki = Zugriff auf den Link zu einem externen Wiki. Die Berechtigungen werden extern verwaltet. +pulls.write = Schreiben: Pull-Requests schließen und Metadaten wie Labels, Meilensteine, Zuweisungen, Fälligkeitsdaten und Abhängigkeiten verwalten. \ No newline at end of file diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 55478dacf2..2df8dce95d 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -51,7 +51,7 @@ webauthn_error_empty=Πρέπει να ορίσετε ένα όνομα για webauthn_error_timeout=Το χρονικό όριο έφτασε πριν το κλειδί να διαβαστεί. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά. webauthn_reload=Ανανέωση -repository=Αποθετήριο +repository=Repository organization=Οργανισμός mirror=Αντίγραφο new_repo=Νέο αποθετήριο @@ -129,7 +129,7 @@ archived=Αρχειοθετήθηκε concept_system_global=Γενικό concept_user_individual=Ατομικό -concept_code_repository=Αποθετήριο +concept_code_repository=Repository concept_user_organization=Οργανισμός show_timestamps=Εμφάνιση χρονοσημάνσεων @@ -160,11 +160,11 @@ invalid_data = Τα δεδομένα δεν είναι έγκυρα: %v test = Τεστ copy_generic = Αντιγραφή στο πρόχειρο error413 = Έχετε εξαντλήσει τους διαθέσιμους πόρους σας. -new_repo.link = Νέο αποθετήριο +new_repo.link = Νέο repository new_migrate.link = Νέα μεταφορά new_org.link = Νέος οργανισμός new_migrate.title = Νέα μεταφορά -new_repo.title = Νέο αποθετήριο +new_repo.title = Νέο repository new_org.title = Νέος οργανισμός [aria] @@ -504,8 +504,8 @@ reset_password.text=Εφόσον το αίτημα δημιουργήθηκε α register_success=Η εγγραφή ολοκληρώθηκε επιτυχώς -issue_assigned.pull=Ο/Η @%[1]s σας έχει αναθέσει στο pull request %[2]s στο αποθετήριο %[3]s. -issue_assigned.issue=Ο/Η @%[1]s σας ανέθεσε το ζήτημα %[2]s στο αποθετήριο %[3]s. +issue_assigned.pull=Ο/Η @%[1]s σας έχει αναθέσει στο pull request %[2]s στο repository %[3]s. +issue_assigned.issue=Ο/Η @%[1]s σας ανέθεσε το ζήτημα %[2]s στο repository %[3]s. issue.x_mentioned_you=Ο/Η @%s σας ανέφερε: issue.action.force_push=Ο/Η %[1]s έκανε force-push το %[2]s από %[3]s σε %[4]s. @@ -530,13 +530,13 @@ release.downloads=Λήψεις: release.download.zip=Πηγαίος Κώδικας (ZIP) release.download.targz=Πηγαίος Κώδικας (TAR.GZ) -repo.transfer.subject_to=Ο/Η %s θα ήθελε να μεταφέρει το αποθετήριο «%s» σε %s -repo.transfer.subject_to_you=Ο/Η %s θα ήθελε να σας μεταφέρει το αποθετήριο «%s» +repo.transfer.subject_to=Ο/Η %s θα ήθελε να μεταφέρει το repository «%s» στο %s +repo.transfer.subject_to_you=Ο/Η %s θα ήθελε να σας μεταφέρει το repository «%s» repo.transfer.to_you=εσάς repo.transfer.body=Για να αποδεχτείτε ή να απορρίψετε το αίτημα αυτό, επισκεφθείτε το %s ή απλά αγνοήστε το. repo.collaborator.added.subject=Ο/Η %s σας πρόσθεσε στο %s ως συνεργάτη -repo.collaborator.added.text=Είστε πλέον συνεργάτης στο αποθετήριο: +repo.collaborator.added.text=Είστε πλέον συνεργάτης στο repository: team_invite.subject=Ο/Η %[1]s σας προσκάλεσε να συμμετέχετε στον οργανισμό %[2]s team_invite.text_1=Ο/Η %[1]s σας προσκάλεσε να συμμετέχετε στην ομάδα %[2]s του οργανισμού %[3]s. @@ -615,10 +615,10 @@ username_change_not_local_user=Δεν επιτρέπεται στους μη τ username_has_not_been_changed=Το όνομα χρήστη δεν άλλαξε repo_name_been_taken=Το όνομα του αποθετηρίου χρησιμοποιείται ήδη. repository_force_private=Η επιλογή Μόνο Ιδιωτικά είναι ενεργοποιημένη: τα ιδιωτικά αποθετήρια δεν μπορούν να δημοσιευθούν. -repository_files_already_exist=Αρχεία υπάρχουν ήδη για αυτό το αποθετήριο. Επικοινωνήστε με το διαχειριστή του συστήματος. -repository_files_already_exist.adopt=Αρχεία υπάρχουν ήδη για αυτό το αποθετήριο και μπορούν να Υιοθετηθούν μόνο. -repository_files_already_exist.delete=Τα αρχεία υπάρχουν ήδη για αυτόν το αποθετήριο. Πρέπει να τα διαγράψετε. -repository_files_already_exist.adopt_or_delete=Τα αρχεία υπάρχουν ήδη για αυτόν το αποθετήριο. Είτε υιοθετήστε τα είτε διαγράψτε τα. +repository_files_already_exist=Αρχεία υπάρχουν ήδη για αυτό το repository. Επικοινωνήστε με το διαχειριστή του συστήματος. +repository_files_already_exist.adopt=Αρχεία υπάρχουν ήδη για αυτό το repository και μπορούν μόνο να υιοθετηθούν. +repository_files_already_exist.delete=Τα αρχεία υπάρχουν ήδη για αυτόν το repository. Πρέπει να τα διαγράψετε. +repository_files_already_exist.adopt_or_delete=Υπάρχουν ήδη τα αρχεία για αυτό το repository. Πρέπει να τα υιοθετήσετε ή να τα διαγράψετε. visit_rate_limit=Συναντήθηκε το όριο ρυθμού κατά την απομακρυσμένη πρόσβαση. 2fa_auth_required=Απαιτήθηκε ταυτοποίηση δύο παραγόντων κατά την απομακρυσμένη πρόσβαση. org_name_been_taken=Το όνομα του οργανισμού χρησιμοποιείται ήδη. @@ -925,7 +925,7 @@ access_token_deletion_cancel_action=Άκυρο access_token_deletion_confirm_action=Διαγραφή access_token_deletion_desc=Η διαγραφή ενός διακριτικού θα ανακαλέσει οριστικά την πρόσβαση στο λογαριασμό σας για εφαρμογές που το χρησιμοποιούν. Συνέχεια; delete_token_success=Το διακριτικό έχει διαγραφεί. Οι εφαρμογές που το χρησιμοποιούν δεν έχουν πλέον πρόσβαση στο λογαριασμό σας. -repo_and_org_access=Πρόσβαση στο Αποθετήριο και Οργανισμό +repo_and_org_access=Πρόσβαση στο repository και οργανισμό permissions_public_only=Δημόσια μόνο permissions_access_all=Όλα (δημόσια, ιδιωτικά, και περιορισμένα) select_permissions=Επιλογή δικαιωμάτων @@ -1005,7 +1005,7 @@ remove_account_link_success=Ο συνδεδεμένος λογαριασμός hooks.desc=Προσθήκη webhooks που θα ενεργοποιούνται για όλα τα αποθετήρια που σας ανήκουν. orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό. -repos_none=Δεν σας ανήκει κανένα κάποιο αποθετήριο. +repos_none=Δεν υπάρχει κάποιο repository που σας ανήκει. delete_account=Διαγραφή του λογαριασμού σας delete_prompt=Αυτή η ενέργεια θα διαγράψει μόνιμα το λογαριασμό σας. ΔΕΝ ΘΑ ΜΠΟΡΕΙ να επανέλθει. @@ -1047,7 +1047,7 @@ language.localization_project = Βοηθήστε μας να μεταφράσο language.description = Από εδώ και στο εξής, αυτή η γλώσσα θα χρησιμοποιείται από προεπιλογή για τον λογαριασμό σας. [repo] -new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Ήδη φιλοξενείται αλλού; Μετεγκατάσταση αποθετηρίου. +new_repo_helper=Ένα repository περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Έχετε ήδη ένα που φιλοξενείται κάπου αλλού; Μεταφορά αποθετηρίου. owner=Ιδιοκτήτης owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων. repo_name=Όνομα αποθετηρίου @@ -1055,7 +1055,7 @@ repo_name_helper=Τα καλά ονόματα αποθετηρίων χρησι repo_size=Μέγεθος Αποθετηρίου template=Πρότυπο template_select=Επιλέξτε ένα πρότυπο -template_helper=Μετατροπή σε πρότυπο αποθετήριο +template_helper=Μετατροπή σε πρότυπο repository template_description=Τα πρότυπα αποθετήρια επιτρέπουν στους χρήστες να δημιουργήσουν νέα αποθετήρια με την ίδια δομή, αρχεία και προαιρετικές ρυθμίσεις. visibility=Ορατότητα visibility_description=Μόνο ο ιδιοκτήτης ή τα μέλη του οργανισμού εάν έχουν δικαιώματα, θα είναι σε θέση να το δουν. @@ -1070,7 +1070,7 @@ fork_to_different_account=Fork σε διαφορετικό λογαριασμό fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει. fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork all_branches=Όλοι οι κλάδοι -fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες. +fork_no_valid_owners=Αυτό το repository δεν μπορεί να γίνει fork, επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες. use_template=Χρήση αυτού του πρότυπου clone_in_vsc=Κλωνοποίηση στο VS Code download_zip=Λήψη ZIP @@ -1120,7 +1120,7 @@ mirror_password_blank_placeholder=(Μη ορισμένο) mirror_password_help=Αλλάξτε το όνομα χρήστη για να διαγράψετε έναν αποθηκευμένο κωδικό πρόσβασης. watchers=Παρατηρητές stargazers=Θαυμαστές -stars_remove_warning=Αυτό θα αφαιρέσει όλα τα αστέρια από αυτό το αποθετήριο. +stars_remove_warning=Αυτό θα αφαιρέσει όλα τα αστέρια από αυτό το repository. forks=Forks reactions_more=και %d περισσότερα unit_disabled=Ο διαχειριστής του ιστότοπου έχει απενεργοποιήσει αυτήν την ενότητα αποθετηρίου. @@ -1129,7 +1129,7 @@ adopt_search=Εισάγετε όνομα χρήστη για αναζήτηση adopt_preexisting_label=Υιοθέτηση αρχείων adopt_preexisting=Υιοθετήστε τα προϋπάρχοντα αρχεία adopt_preexisting_content=Δημιουργία αποθετηρίου από %s -adopt_preexisting_success=Υιοθετήθηκαν αρχεία και δημιουργήθηκε το αποθετήριο από %s +adopt_preexisting_success=Υιοθετήθηκαν αρχεία και δημιουργήθηκε το repository από %s delete_preexisting_label=Διαγραφή delete_preexisting=Διαγραφή αρχείων που προϋπήρχαν delete_preexisting_content=Διαγραφή αρχείων στο %s @@ -1159,17 +1159,17 @@ desc.archived=Αρχειοθετημένο template.items=Αντικείμενα προτύπου template.git_content=Περιεχόμενο Git (Προεπιλεγμένος κλάδος) template.git_hooks=Git hooks -template.git_hooks_tooltip=Δεν θα μπορέσετε να αφαιρέσετε ή να τροποποιήσετε τα Git hook αφού τα έχετε προσθέσει. Επιλέξτε την ρύθμιση αυτή μόνο αν εμπιστεύεστε το πρότυπο αποθετήριο. +template.git_hooks_tooltip=Δεν θα μπορέσετε να αφαιρέσετε ή να τροποποιήσετε τα Git hook αφού τα έχετε προσθέσει. Επιλέξτε την ρύθμιση αυτή μόνο αν εμπιστεύεστε το πρότυπο repository. template.webhooks=Webhooks template.topics=Θέματα template.avatar=Εικόνα template.issue_labels=Ταμπέλες ζητημάτων template.one_item=Πρέπει να επιλέξετε τουλάχιστον ένα αντικείμενο στο πρότυπο -template.invalid=Πρέπει να επιλέξετε ένα πρότυπο αποθετήριο +template.invalid=Πρέπει να επιλέξετε ένα πρότυπο repository archive.title=Αυτό το αποθετήρειο αρχειοθετήθηκε. Μπορείτε να προβάλετε αρχεία και να τα κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests. -archive.title_date=Αυτό το αποθετήριο έχει αρχειοθετηθεί στο %s. Μπορείτε να προβάλετε αρχεία και να κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests. -archive.issue.nocomment=Αυτό το αποθετήριο αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε σε ζητήματα. +archive.title_date=Αυτό το repository αρχειοθετήθηκε στις %s. Μπορείτε να δείτε τα αρχεία του και να το κλωνοποιήσετε, αλλά δεν μπορείτε να κάνετε push, να ανοίξετε ζητήματα ή pull requests. +archive.issue.nocomment=Αυτό το repository έχει αρχειοθετηθεί. Δεν μπορείτε να σχολιάσετε σε ζητήματα. archive.pull.nocomment=Αυτό το repo αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε στα pull requests. form.reach_limit_of_creation_1=Έχετε ήδη συμπληρώσει το όριο του %d αποθετηρίου. @@ -1180,7 +1180,7 @@ form.name_pattern_not_allowed=Το μοτίβο «%s» δεν επιτρέπετ need_auth=Εξουσιοδότηση migrate_options=Ρυθμίσεις μεταφοράς migrate_service=Υπηρεσία Μεταφοράς -migrate_options_mirror_helper=Αυτό το αποθετήριο θα είναι είδωλο +migrate_options_mirror_helper=Αυτό το repository θα είναι είδωλο migrate_options_lfs=Μεταφορά αρχείων LFS migrate_options_lfs_endpoint.label=Άκρο LFS migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να καθορίσει τον διακομιστή LFS. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού. @@ -1233,10 +1233,10 @@ migrate.cancel_migrating_confirm=Θέλετε να ακυρώσετε αυτή mirror_from=είδωλο του forked_from=forked από generated_from=παραγμένο από -fork_from_self=Δεν μπορείτε να κάνετε fork σε ένα αποθετήριο που κατέχετε. -fork_guest_user=Συνδεθείτε για να κάνετε fork αυτό το αποθετήριο. -watch_guest_user=Συνδεθείτε για να παρακολουθήσετε αυτό το αποθετήριο. -star_guest_user=Συνδεθείτε για να δώσετε ένα αστέρι σε αυτό το αποθετήριο. +fork_from_self=Δεν μπορείτε να κάνετε fork ένα repository που σας ανήκει. +fork_guest_user=Συνδεθείτε για να κάνετε fork αυτό το repository. +watch_guest_user=Συνδεθείτε για να παρακολουθήσετε αυτό το repository. +star_guest_user=Συνδεθείτε για να δώσετε ένα αστέρι σε αυτό το repository. unwatch=Παύση ακολούθησης watch=Παρακολούθηση unstar=Αφαίρεση αστεριού @@ -1248,11 +1248,11 @@ more_operations=Περισσότερες λειτουργίες no_desc=Χωρίς περιγραφή quick_guide=Γρήγορος οδηγός clone_this_repo=Κλωνοποίηση αυτού του αποθετηρίου -cite_this_repo=Αναφορά σε αυτό το αποθετήριο +cite_this_repo=Αναφορά σε αυτό το repository create_new_repo_command=Δημιουργία νέου αποθετηρίου στη γραμμή εντολών push_exist_repo=Προώθηση ενός υπάρχοντος αποθετηρίου από τη γραμμή εντολών -empty_message=Αυτό το αποθετήριο δεν έχει περιεχόμενο. -broken_message=Τα δεδομένα Git που διέπουν αυτό το αποθετήριο δεν μπορούν να διαβαστούν. Επικοινωνήστε με το διαχειριστή ή διαγράψτε αυτό το αποθετήριο. +empty_message=Αυτό το repository δεν έχει περιεχόμενο. +broken_message=Τα δεδομένα Git που διέπουν αυτό το αποθετήριο δεν μπορούν να διαβαστούν. Επικοινωνήστε με το διαχειριστή ή διαγράψτε αυτό το repository. code=Κώδικας code.desc=Πρόσβαση στον πηγαίο κώδικα, αρχεία, υποβολές και κλάδους. @@ -1330,7 +1330,7 @@ editor.cannot_edit_non_text_files=Τα δυαδικά αρχεία δεν μπο editor.edit_this_file=Επεξεργασία αρχείου editor.this_file_locked=Το αρχείο είναι κλειδωμένο editor.must_be_on_a_branch=Πρέπει να βρίσκεστε σε έναν κλάδο για να κάνετε ή να προτείνετε αλλαγές σε αυτό το αρχείο. -editor.fork_before_edit=Πρέπει να κάνετε fork αυτό το αποθετήριο για να κάνετε ή να προτείνετε αλλαγές σε αυτό το αρχείο. +editor.fork_before_edit=Πρέπει να κάνετε fork αυτό το repository για να κάνετε ή να προτείνετε αλλαγές σε αυτό το αρχείο. editor.delete_this_file=Διαγραφή αρχείου editor.must_have_write_access=Πρέπει να έχετε πρόσβαση εγγραφής για να κάνετε ή να προτείνετε αλλαγές σε αυτό το αρχείο. editor.file_delete_success=Το αρχείο «%s» έχει διαγραφεί. @@ -1359,15 +1359,15 @@ editor.new_branch_name_desc=Όνομα νέου κλάδου… editor.cancel=Ακύρωση 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.branch_does_not_exist=Ο κλάδος "%s" δεν υπάρχει σε αυτό το repository. +editor.branch_already_exists=Ο κλάδος «%s» υπάρχει ήδη σε αυτό το repository. +editor.directory_is_a_file=Το όνομα φακέλου «%s» χρησιμοποιείται ήδη ως όνομα αρχείου σε αυτό το repository. 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.filename_is_a_directory=Το όνομα αρχείου «%s» χρησιμοποιείται ήδη ως όνομα φακέλου σε αυτό το repository. +editor.file_editing_no_longer_exists=Το αρχείο «%s», το οποίο επεξεργάζεστε, δεν υπάρχει πλέον σε αυτό το repository. +editor.file_deleting_no_longer_exists=Το αρχείο «%s», το οποίο διαγράφεται, δεν υπάρχει πλέον σε αυτό το repository. editor.file_changed_while_editing=Προέκυψαν κάποιες αλλαγές στα περιεχόμενα του αρχείου από τότε που ξεκινήσατε να τα επεξεργάζεστε. Κάντε κλικ εδώ για να τα δείτε ή ξανακάντε μία υποβολή των αλλαγών σας για να τις αντικαταστήσετε. -editor.file_already_exists=Υπάρχει ήδη ένα αρχείο με το όνομα «%s» σε αυτό το αποθετήριο. +editor.file_already_exists=Υπάρχει ήδη ένα αρχείο με το όνομα «%s» σε αυτό το repository. editor.commit_empty_file_header=Υποβολή ενός κενού αρχείου editor.commit_empty_file_text=Το αρχείο που πρόκειται να υποβληθεί είναι κενό. Συνέχεια; editor.no_changes_to_show=Δεν υπάρχουν αλλαγές για εμφάνιση. @@ -1620,13 +1620,13 @@ issues.author_helper=Αυτός ο χρήστης είναι ο συγγραφέ issues.role.owner=Ιδιοκτήτης issues.role.owner_helper=Αυτός ο χρήστης είναι ο ιδιοκτήτης αυτού του αποθετηρίου. issues.role.member=Μέλος -issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού, του οποίου ανήκει το αποθετήριο. +issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού, του οποίου ανήκει το repository. issues.role.collaborator=Συνεργάτης -issues.role.collaborator_helper=Ο χρήστης έλαβε πρόσκληση συνεργασίας στο αποθετήριο. +issues.role.collaborator_helper=Ο χρήστης έλαβε πρόσκληση συνεργασίας στο repository. issues.role.first_time_contributor=Συντελεστής για πρώτη φορά -issues.role.first_time_contributor_helper=Αυτή είναι η πρώτη συνεισφορά αυτού του χρήστη στο αποθετήριο. +issues.role.first_time_contributor_helper=Αυτή είναι η πρώτη συνεισφορά αυτού του χρήστη στο repository. issues.role.contributor=Συντελεστής -issues.role.contributor_helper=Αυτός ο χρήστης έχει προηγούμενές υποβολές (commits) στο αποθετήριο. +issues.role.contributor_helper=Αυτός ο χρήστης έχει προηγούμενές υποβολές (commits) στο repository. issues.re_request_review=Επαναίτηση ανασκόπησης issues.is_stale=Έχουν υπάρξει αλλαγές σε αυτό το PR από αυτή την αναθεώρηση issues.remove_request_review=Αφαίρεση αιτήματος αναθεώρησης @@ -1678,7 +1678,7 @@ issues.unlock_comment=: ξεκλείδωσε αυτή τη συνομιλία %s issues.lock_confirm=Κλείδωμα issues.unlock_confirm=Ξεκλείδωμα issues.lock.notice_1=- Άλλοι χρήστες δεν μπορούν να αφήσουν νέα σχόλια σε αυτό το ζήτημα. -issues.lock.notice_2=- Εσείς και άλλοι συνεργάτες που έχουν πρόσβαση στο αποθετήριο θα μπορείτε ακόμα να αφήσετε σχόλια που θα είναι ορατά σε άλλους. +issues.lock.notice_2=- Εσείς, μαζί με τους συνεργάτες σας που έχουν πρόσβαση στο repository, θα μπορείτε ακόμα να αφήσετε σχόλια που θα μπορούν να βλέπουν και άλλοι. issues.lock.notice_3=- Θα μπορείτε να ξεκλειδώσετε αυτό το ζήτημα πιο μετά. issues.unlock.notice_1=- Όλοι θα βρίσκονται πάλι σε θέση να αφήσουν σχόλιο σε αυτό το ζήτημα. issues.unlock.notice_2=- Θα μπορείτε πάντα να ξανακλειδώσετε αυτό το ζήτημα πιο μετά. @@ -1759,7 +1759,7 @@ issues.dependency.add_error_dep_issue_not_exist=Εξαρτώμενο ζήτημ issues.dependency.add_error_dep_not_exist=Δεν υπάρχει η Εξάρτηση. issues.dependency.add_error_dep_exists=Η Εξάρτηση υπάρχει ήδη. issues.dependency.add_error_cannot_create_circular=Δεν μπορείτε να δημιουργήσετε μια εξάρτηση με δύο ζητήματα που μπλοκάρουν το ένα το άλλο. -issues.dependency.add_error_dep_not_same_repo=Και τα δύο ζητήματα πρέπει να βρίσκονται στο ίδιο αποθετήριο. +issues.dependency.add_error_dep_not_same_repo=Και τα δύο ζητήματα πρέπει να βρίσκονται στο ίδιο repository. issues.review.self.approval=Δεν μπορείτε να εγκρίνετε το δικό σας pull request. issues.review.self.rejection=Δεν μπορείτε να ζητήσετε αλλαγές στο δικό σας pull request. issues.review.approve=ενέκρινε τις αλλαγές %s @@ -1922,7 +1922,7 @@ pulls.closed_at=`έκλεισε αυτό το pull request %[2]s` pulls.cmd_instruction_hint=Προβολή οδηγιών γραμμής εντολών pulls.cmd_instruction_checkout_title=Έλεγχος -pulls.cmd_instruction_checkout_desc=Από το αποθετήριο του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές. +pulls.cmd_instruction_checkout_desc=Από το repository του έργου σας, ελέγξτε έναν νέο κλάδο και δοκιμάστε τις αλλαγές. pulls.cmd_instruction_merge_title=Συγχώνευση pulls.cmd_instruction_merge_desc=Συγχώνευση των αλλαγών και ενημέρωση στο Gitea. pulls.clear_merge_message=Εκκαθάριση μηνύματος συγχώνευσης @@ -2101,8 +2101,8 @@ search.code_no_results=Δεν βρέθηκε πηγαίος κώδικας πο search.code_search_unavailable=Η αναζήτηση κώδικα δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ επικοινωνήστε με το διαχειριστή. settings=Ρυθμίσεις -settings.desc=Στις Ρυθμίσεις μπορείτε να διαχειριστείτε τις ρυθμίσεις για το αποθετήριο -settings.options=Αποθετήριο +settings.desc=Στις Ρυθμίσεις μπορείτε να διαχειριστείτε τις ρυθμίσεις για το repository +settings.options=Repository settings.collaboration=Συνεργάτες settings.collaboration.admin=Διαχειριστής settings.collaboration.write=Εγγραφή @@ -2113,18 +2113,18 @@ settings.hooks=Webhooks settings.githooks=Git hooks 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=Ρυθμίστε τον αυτόματο συγχρονισμό των commit, ετικετών και κλάδων του αποθετηρίου σας σε ένα άλλο repository. +settings.mirror_settings.docs.disabled_pull_mirror.instructions=Ρυθμίστε τον αυτόματο συγχρονισμό των commit, ετικετών και κλάδων του έργου σας με ένα άλλο repository. Τα είδωλα τύπου λήψης έχουν απενεργοποιηθεί από τον διαχειριστή σας. +settings.mirror_settings.docs.disabled_push_mirror.instructions=Ρυθμίστε την αυτόματη λήψη υποβολών, ετικετών και κλάδων από ένα άλλο repository. 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.no_new_mirrors=Το repository σας αντιγράφει τις αλλαγές προς ή από ένα άλλο repository. Λάβετε υπόψη ότι δεν μπορείτε να δημιουργήσετε νέα είδωλα αυτή τη στιγμή. 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=το κεφάλαιο "Pulling from a remote repository" της τεκμηρίωσης. -settings.mirror_settings.docs.pulling_remote_title=Έλξη από ένα απομακρυσμένο αποθετήριο +settings.mirror_settings.docs.pulling_remote_title=Pull από ένα απομακρυσμένο repository settings.mirror_settings.mirrored_repository=Είδωλο αποθετηρίου settings.mirror_settings.direction=Κατεύθυνση settings.mirror_settings.direction.pull=Pull @@ -2188,33 +2188,33 @@ settings.reindex_button=Προσθήκη στην ουρά Reindex settings.reindex_requested=Αιτήθηκε reindex settings.admin_enable_close_issues_via_commit_in_any_branch=Κλείσιμο ενός ζητήματος μέσω μιας υποβολής που έγινε σε έναν μη προεπιλεγμένο κλάδο settings.danger_zone=Ζώνη κινδύνου -settings.new_owner_has_same_repo=Ο νέος ιδιοκτήτης έχει ήδη ένα αποθετήριο με το ίδιο όνομα. Παρακαλώ επιλέξτε ένα άλλο όνομα. -settings.convert=Μετατροπή σε κανονικό αποθετήριο -settings.convert_desc=Μπορείτε να μετατρέψετε αυτόν το είδωλο σε κανονικό αποθετήριο. Αυτό δεν μπορεί να αναιρεθεί. -settings.convert_notices_1=Αυτή η λειτουργία θα μετατρέψει το είδωλο σε ένα κανονικό αποθετήριο και δεν μπορεί να αναιρεθεί. +settings.new_owner_has_same_repo=Ο νέος ιδιοκτήτης έχει ήδη ένα repository με το ίδιο όνομα. Παρακαλώ επιλέξτε ένα άλλο όνομα. +settings.convert=Μετατροπή σε κανονικό repository +settings.convert_desc=Μπορείτε να μετατρέψετε αυτόν το είδωλο σε κανονικό repository. Αυτό δεν μπορεί να αναιρεθεί. +settings.convert_notices_1=Αυτή η λειτουργία θα μετατρέψει το είδωλο σε ένα κανονικό repository και δεν μπορεί να αναιρεθεί. settings.convert_confirm=Μετατροπή αποθετηρίου -settings.convert_succeed=Το είδωλο έχει μετατραπεί σε κανονικό αποθετήριο. -settings.convert_fork=Μετατροπή σε κανονικό αποθετήριο -settings.convert_fork_desc=Μπορείτε να μετατρέψετε αυτό το fork σε κανονικό αποθετήριο. Αυτό δεν μπορεί να αναιρεθεί. -settings.convert_fork_notices_1=Αυτή η λειτουργία θα μετατρέψει το fork σε ένα κανονικό αποθετήριο και δεν μπορεί να αναιρεθεί. +settings.convert_succeed=Το είδωλο έχει μετατραπεί σε κανονικό repository. +settings.convert_fork=Μετατροπή σε κανονικό repository +settings.convert_fork_desc=Μπορείτε να μετατρέψετε αυτό το fork σε κανονικό repository. Αυτό δεν μπορεί να αναιρεθεί. +settings.convert_fork_notices_1=Αυτή η λειτουργία θα μετατρέψει το fork σε ένα κανονικό repository και δεν μπορεί να αναιρεθεί. settings.convert_fork_confirm=Μετατροπή αποθετηρίου -settings.convert_fork_succeed=Το fork έχει μετατραπεί σε κανονικό αποθετήριο. +settings.convert_fork_succeed=Το fork έχει μετατραπεί σε κανονικό repository. settings.transfer.title=Μεταβίβαση ιδιοκτησίας settings.transfer.rejected=Η μεταβίβαση του αποθετηρίου απορρίφθηκε. settings.transfer.success=Η μεταβίβαση του αποθετηρίου ήταν επιτυχής. settings.transfer_abort=Ακύρωση μεταβίβασης settings.transfer_abort_invalid=Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου. settings.transfer_abort_success=Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία. -settings.transfer_desc=Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή. +settings.transfer_desc=Μεταβιβάστε αυτό το repository σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή. settings.transfer_form_title=Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση: -settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το αποθετήριο σε άλλο χρήστη. -settings.transfer_notices_1=- Θα χάσετε την πρόσβαση στο αποθετήριο αν το μεταβιβάσετε σε έναν μεμονωμένο χρήστη. -settings.transfer_notices_2=- Θα διατηρήσετε την πρόσβαση στο αποθετήριο αν το μεταβιβάσετε σε έναν οργανισμό που είστε (συν)ιδιοκτήτης. -settings.transfer_notices_3=- Εάν το αποθετήριο είναι ιδιωτικό και μεταβιβάζεται σε μεμονωμένο χρήστη, αυτή η ενέργεια εξασφαλίζει ότι ο χρήστης έχει τουλάχιστον άδεια ανάγνωσης (και αλλάζει τα δικαιώματα εάν είναι απαραίτητο). +settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το repository σε άλλο χρήστη. +settings.transfer_notices_1=- Θα χάσετε την πρόσβαση στο repository αν το μεταβιβάσετε σε έναν μεμονωμένο χρήστη. +settings.transfer_notices_2=- Θα διατηρήσετε την πρόσβαση στο repository αν το μεταβιβάσετε σε έναν οργανισμό που είστε (συν)ιδιοκτήτης. +settings.transfer_notices_3=- Εάν το repository είναι ιδιωτικό και μεταβιβάζεται σε μεμονωμένο χρήστη, αυτή η ενέργεια εξασφαλίζει ότι ο χρήστης έχει τουλάχιστον άδεια ανάγνωσης (και αλλάζει τα δικαιώματα εάν είναι απαραίτητο). settings.transfer_owner=Νέος ιδιοκτήτης settings.transfer_perform=Εκτέλεση μεταφοράς -settings.transfer_started=`Αυτό το αποθετήριο έχει επισημανθεί για μεταφορά και αναμένει επιβεβαίωση από το "%s"` -settings.transfer_succeed=Το αποθετήριο έχει μεταφερθεί. +settings.transfer_started=`Αυτό το repository έχει επισημανθεί για μεταφορά και αναμένει επιβεβαίωση από το "%s"` +settings.transfer_succeed=Το repository έχει μεταφερθεί. settings.signing_settings=Ρυθμίσεις επαλήθευσης υπογραφών settings.trust_model=Μοντέλο εμπιστοσύνης υπογραφών settings.trust_model.default=Προεπιλεγμένο μοντέλο εμπιστοσύνης @@ -2236,33 +2236,33 @@ settings.wiki_deletion_success=Τα δεδομένα wiki του αποθετη settings.delete=Διαγραφή αυτόυ του αποθετηρίου settings.delete_desc=Η διαγραφή ενός αποθετηρίου είναι μόνιμη και δεν μπορεί να αναιρεθεί. settings.delete_notices_1=- Αυτή η ενέργεια ΔΕΝ ΜΠΟΡΕΙ να αναιρεθεί. -settings.delete_notices_2=- Αυτή η ενέργεια θα διαγράψει μόνιμα το αποθετήριο %s μαζί με τον κώδικα, τα ζητημάτα, τα σχόλια, τα δεδομένα των wiki και τις ρυθμίσεις συνεργατών που βρίσκονται μέσα σε αυτό. +settings.delete_notices_2=- Αυτή η ενέργεια θα διαγράψει μόνιμα το repository %s μαζί με τον κώδικα, τα ζητημάτα, τα σχόλια, τα δεδομένα των wiki και τις ρυθμίσεις συνεργατών που βρίσκονται μέσα σε αυτό. settings.delete_notices_fork_1=- Τα Forks αυτού του αποθετηρίου θα γίνουν ανεξάρτητα μετά τη διαγραφή. -settings.deletion_success=Το αποθετήριο έχει διαγραφεί. +settings.deletion_success=Το repository έχει διαγραφεί. settings.update_settings_success=Οι ρυθμίσεις του αποθετηρίου έχουν ενημερωθεί. -settings.update_settings_no_unit=Το αποθετήριο θα πρέπει να επιτρέπει τουλάχιστον κάποιο είδος αλληλεπίδρασης. +settings.update_settings_no_unit=Το repository θα πρέπει να επιτρέπει τουλάχιστον κάποιο είδος αλληλεπίδρασης. 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_duplicate=Ο συνεργάτης έχει ήδη προστεθεί σε αυτό το repository. settings.delete_collaborator=Κατάργηση settings.collaborator_deletion=Κατάργηση συνεργάτη -settings.collaborator_deletion_desc=Η κατάργηση ενός συνεργάτη θα αφαιρέσει και την πρόσβασή του στο αποθετήριο. Είστε βέβαιοι; +settings.collaborator_deletion_desc=Η κατάργηση ενός συνεργάτη θα αφαιρέσει και την πρόσβασή του στο repository. Είστε βέβαιοι; settings.remove_collaborator_success=Ο συνεργάτης έχει καταργηθεί. settings.search_user_placeholder=Αναζήτηση χρήστη… settings.org_not_allowed_to_be_collaborator=Δεν μπορείτε να προσθέσετε έναν οργανισμό ως συνεργάτη. -settings.change_team_access_not_allowed=Η αλλαγή της πρόσβασης ομάδας για το αποθετήριο έχει περιοριστεί στον ιδιοκτήτη του οργανισμού -settings.team_not_in_organization=Η ομάδα δεν είναι στον ίδιο οργανισμό με το αποθετήριο +settings.change_team_access_not_allowed=Η αλλαγή της πρόσβασης ομάδας για το repository έχει περιοριστεί στον ιδιοκτήτη του οργανισμού +settings.team_not_in_organization=Η ομάδα δεν είναι στον ίδιο οργανισμό με το repository settings.teams=Ομάδες settings.add_team=Προσθήκη ομάδας -settings.add_team_duplicate=Η ομάδα έχει ήδη το αποθετήριο -settings.add_team_success=Η ομάδα έχει πλέον πρόσβαση στο αποθετήριο. +settings.add_team_duplicate=Η ομάδα έχει ήδη το repository +settings.add_team_success=Η ομάδα έχει πλέον πρόσβαση στο repository. settings.search_team=Αναζήτηση Ομάδας… -settings.change_team_permission_tip=Τα δικαιώματα της ομάδας έχουν οριστεί στη σελίδα ρυθμίσεων της ομάδας και δεν μπορούν να αλλάξουν ανά αποθετήριο +settings.change_team_permission_tip=Τα δικαιώματα της ομάδας έχουν οριστεί στη σελίδα ρυθμίσεων της ομάδας και δεν μπορούν να αλλάξουν ανά repository settings.delete_team_tip=Αυτή η ομάδα έχει πρόσβαση σε όλα τα αποθετήρια και δεν μπορεί να αφαιρεθεί -settings.remove_team_success=Έχει αφαιρεθεί η πρόσβαση της ομάδας στο αποθετήριο. +settings.remove_team_success=Έχει αφαιρεθεί η πρόσβαση της ομάδας στο repository. settings.add_webhook=Προσθήκη webhook settings.add_webhook.invalid_channel_name=Το όνομα του καναλιού Webhook δεν μπορεί να είναι κενό και δεν μπορεί να περιέχει μόνο έναν χαρακτήρα #. settings.hooks_desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούνται ορισμένα γεγονότα στο Forgejo. Διαβάστε περισσότερα στον οδηγό webhooks. @@ -2305,15 +2305,15 @@ settings.event_create_desc=Ο κλάδος ή η ετικέτα δημιουργ settings.event_delete=Διαγραφή settings.event_delete_desc=Ο κλάδος ή η ετικέτα διαγράφηκε. settings.event_fork=Fork -settings.event_fork_desc=Το αποθετήριο έγινε fork. +settings.event_fork_desc=Το repository έγινε fork. settings.event_wiki=Wiki settings.event_wiki_desc=Η σελίδα Wiki δημιουργήθηκε, μετονομάστηκε, επεξεργάστηκε ή διαγράφηκε. settings.event_release=Κυκλοφορία settings.event_release_desc=Η έκδοση δημοσιεύτηκε, ενημερώθηκε ή διαγράφηκε από ένα αποθετήριο. settings.event_push=Push -settings.event_push_desc=Git push σε ένα αποθετήριο. -settings.event_repository=Αποθετήριο -settings.event_repository_desc=Το αποθετήριο δημιουργήθηκε ή διαγράφηκε. +settings.event_push_desc=Git push σε ένα repository. +settings.event_repository=Repository +settings.event_repository_desc=Το repository δημιουργήθηκε ή διαγράφηκε. settings.event_header_issue=Συμβάντα ζητημάτων settings.event_issues=Ζητήματα settings.event_issues_desc=Το ζήτημα άνοιξε, έκλεισε, ανοίχθηκε εκ νέου ή επεξεργάστηκε. @@ -2345,7 +2345,7 @@ settings.event_pull_request_review_request_desc=Ζητήθηκε η αξιολό settings.event_pull_request_approvals=Εγκρίσεις pull request settings.event_pull_request_merge=Συγχώνευση pull request settings.event_package=Πακέτο -settings.event_package_desc=Το πακέτο δημιουργήθηκε ή διαγράφηκε σε ένα αποθετήριο. +settings.event_package_desc=Το πακέτο δημιουργήθηκε ή διαγράφηκε σε ένα repository. settings.branch_filter=Φίλτρο κλάδου settings.branch_filter_desc=Λίστα επιτρεπόμενων κλάδων για ωθήσεις, δημιουργία κλάδων και γεγονότα διαγραφής κλάδων, που ορίζονται ως μοτίβο glob. Εάν είναι κενό ή *, αναφέρονται συμβάντα για όλους τους κλάδους. Δείτε τη τεκμηρίωση%[2]s για σύνταξη. Παραδείγματα: master, {master,release*}. settings.authorization_header=Κεφαλίδα authorization @@ -2361,7 +2361,7 @@ settings.hook_type=Είδος hook settings.slack_token=Διακριτικό settings.slack_domain=Domain settings.slack_channel=Κανάλι -settings.add_web_hook_desc=Ενσωμάτωσε το %s στο αποθετήριο σας. +settings.add_web_hook_desc=Ενσωμάτωσε το %s στο repository σας. settings.web_hook_name_gitea=Gitea settings.web_hook_name_forgejo = Forgejo settings.web_hook_name_gogs=Gogs @@ -2381,9 +2381,9 @@ settings.packagist_api_token=Διακριτικό API settings.packagist_package_url=URL πακέτων Packagist settings.deploy_keys=Κλειδιά διάθεσης settings.add_deploy_key=Προσθήκη κλειδιού διάθεσης -settings.deploy_key_desc=Τα κλειδιά διάθεσης έχουν πρόσβαση μόνο-ανάγνωσης στο αποθετήριο. +settings.deploy_key_desc=Τα κλειδιά διάθεσης έχουν πρόσβαση μόνο-ανάγνωσης στο repository. settings.is_writable=Ενεργοποίηση πρόσβασης εγγραφής -settings.is_writable_info=Επιτρέψτε σε αυτό το κλειδί διάθεσης να ωθήσει στο αποθετήριο. +settings.is_writable_info=Επιτρέψτε σε αυτό το κλειδί διάθεσης να ωθήσει στο repository. settings.no_deploy_keys=Δεν υπάρχουν ακόμα κλειδιά διάθεσης. settings.title=Τίτλος settings.deploy_key_content=Περιεχόμενο @@ -2391,7 +2391,7 @@ settings.key_been_used=Ένα κλειδί διάθεσης με το ίδιο settings.key_name_used=Ένα κλειδί διάθεσης με το ίδιο όνομα υπάρχει ήδη. settings.add_key_success=Το κλειδί διάθεσης «%s» προστέθηκε. settings.deploy_key_deletion=Αφαίρεση κλειδιού διάθεσης -settings.deploy_key_deletion_desc=Η κατάργηση ενός κλειδί διάθεσης θα ανακαλέσει την πρόσβασή του σε αυτό το αποθετήριο. Συνέχεια; +settings.deploy_key_deletion_desc=Η κατάργηση ενός κλειδί διάθεσης θα ανακαλέσει την πρόσβασή του σε αυτό το repository. Συνέχεια; settings.deploy_key_deletion_success=Το κλειδί διάθεσης έχει αφαιρεθεί. settings.branches=Κλάδοι settings.protected_branch=Προστασία κλάδου @@ -2424,7 +2424,7 @@ settings.protect_check_status_contexts=Ενεργοποίηση ελέγχου settings.protect_status_check_patterns=Μοτίβα ελέγχου κατάστασης: settings.protect_status_check_patterns_desc=Ορίστε μοτίβα για να καθορίσετε ποιοι έλεγχοι κατάστασης πρέπει να περάσουν πριν οι κλάδοι να μπορούν να συγχωνευτούν σε έναν κλάδο που ταιριάζει με αυτόν τον κανόνα. Κάθε γραμμή καθορίζει ένα μοτίβο. Τα μοτίβα δεν μπορούν να είναι κενά. settings.protect_check_status_contexts_desc=Απαιτείται έλεγχος κατάστασης για να περάσει το pull request πριν από τη συγχώνευση. Επιλέξτε ποιοι έλεγχοι κατάστασης πρέπει να περάσουν πριν κλάδοι μπορούν να συγχωνευτούν σε έναν κλάδο που ταιριάζει με αυτόν τον κανόνα. Όταν είναι ενεργοποιημένο, οι υποβολές πρέπει πρώτα να γίνονται push σε άλλο κλάδο, στη συνέχεια, να συγχωνεύονται ή γίνονται push απευθείας σε ένα κλάδο που ταιριάζει με αυτόν τον κανόνα, αφού έχουν ολοκληρωθεί οι έλεγχοι κατάστασης. Αν δεν επιλεχθεί κανένα πλαίσιο, η τελευταία υποβολή πρέπει να είναι επιτυχής ανεξάρτητα από το πλαίσιο. -settings.protect_check_status_contexts_list=Έλεγχοι κατάστασης που βρέθηκαν την τελευταία εβδομάδα για αυτό το αποθετήριο +settings.protect_check_status_contexts_list=Έλεγχοι κατάστασης που βρέθηκαν την τελευταία εβδομάδα για αυτό το repository settings.protect_status_check_matched=Ταιριάζει settings.protect_invalid_status_check_pattern=Μη έγκυρο μοτίβο ελέγχου κατάστασης: "%s". settings.protect_no_valid_status_check_patterns=Μη έγκυρα μοτίβα ελέγχου κατάστασης. @@ -2486,20 +2486,20 @@ settings.matrix.message_type=Είδος μηνύματος settings.archive.button=Αρχειοθέτηση αποθετηρίου settings.archive.header=Αρχειοθέτηση αποθετηρίου settings.archive.text=Η αρχειοθέτηση του αποθετηρίου θα το αλλάξει σε μόνο για ανάγνωση. Δε θα φαίνεται στον αρχικό πίνακα. Κανείς (ακόμα και εσείς!) δε θα μπορεί να κάνει νέες υποβολές, ή να ανοίξει ζητήματα ή pull request. -settings.archive.success=Το αποθετήριο αρχειοθετήθηκε με επιτυχία. +settings.archive.success=Το repository αρχειοθετήθηκε με επιτυχία. settings.archive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια αρχειοθέτησης του αποθετηρίου. Δείτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες. settings.archive.error_ismirror=Δε μπορείτε να αρχειοθετήσετε ένα είδωλο αποθετηρίου. -settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλάδου δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο. -settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το αποθετήριο είναι αρχειοθετημένο. +settings.archive.branchsettings_unavailable=Οι ρυθμίσεις του κλάδου δεν είναι διαθέσιμες αν το repository είναι αρχειοθετημένο. +settings.archive.tagsettings_unavailable=Οι ρυθμίσεις της ετικέτας δεν είναι διαθέσιμες αν το repository είναι αρχειοθετημένο. settings.unarchive.button=Αναίρεση αρχειοθέτησης αποθετηρίου settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests. -settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία. +settings.unarchive.success=Το repository απο-αρχειοθετήθηκε με επιτυχία. settings.unarchive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια απο-αρχειοθέτησης του αποθετηρίου. Δείτε τις καταγραφές για περισσότερες λεπτομέρειες. settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί. settings.lfs=LFS -settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο -settings.lfs_no_lfs_files=Δεν υπάρχουν αρχεία LFS σε αυτό το αποθετήριο +settings.lfs_filelist=Αρχεία LFS σε αυτό το repository +settings.lfs_no_lfs_files=Δεν υπάρχουν αρχεία LFS σε αυτό το repository settings.lfs_findcommits=Εύρεση υποβολών settings.lfs_lfs_file_no_commits=Δεν βρέθηκαν υποβολές για αυτό το αρχείο LFS settings.lfs_noattribute=Αυτή η διαδρομή δεν έχει λειτουργία κλειδώματος στον προεπιλεγμένο κλάδο @@ -2518,7 +2518,7 @@ settings.lfs_force_unlock=Εξαγκαναστικό ξεκλείδωμα settings.lfs_pointers.found=Βρέθηκαν %d δείκτης(ες) blob - %d συσχετίστηκαν, %d δεν συσχετίστηκαν (%d λείπουν από το χώρο αποθήκευσης) settings.lfs_pointers.sha=Blob hash settings.lfs_pointers.oid=OID -settings.lfs_pointers.inRepo=Στο αποθετήριο +settings.lfs_pointers.inRepo=Στο repository settings.lfs_pointers.exists=Υπάρχει στο χώρο αποθήκευσης settings.lfs_pointers.accessible=Προσβάσιμο στον χρήστη settings.lfs_pointers.associateAccessible=Συσχετισμός προσιτών %d OID @@ -2621,7 +2621,7 @@ release.delete_tag=Διαγραφή ετικέτας release.deletion=Διαγραφή κυκλοφορίας release.deletion_desc=Διαγράφοντας μια κυκλοφορία, αυτή αφαιρείται μόνο από το Gitea. Δε θα επηρεάσει την ετικέτα Git, τα περιεχόμενα του αποθετηρίου σας ή το ιστορικό της. Συνέχεια; release.deletion_success=Η κυκλοφορία έχει διαγραφεί. -release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το αποθετήριο. Τα περιεχόμενα του αποθετηρίου και το ιστορικό παραμένουν αμετάβλητα. Συνέχεια; +release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το repository. Τα περιεχόμενα του repository και το ιστορικό δεν θα πειραχτούν. Να γίνει συνέχεια; release.deletion_tag_success=Η ετικέτα έχει διαγραφεί. release.tag_name_already_exist=Υπάρχει ήδη μια έκδοση με αυτό το όνομα ετικέτας. release.tag_name_invalid=Το όνομα της ετικέτας δεν είναι έγκυρο. @@ -2646,7 +2646,7 @@ branch.delete_branch_has_new_commits=Ο κλάδος «%s» δεν μπορεί branch.create_branch=Δημιουργία κλάδου %s branch.create_from=`από το «%s»` branch.create_success=Ο κλάδος «%s» δημιουργήθηκε. -branch.branch_already_exists=Ο κλάδος «%s» υπάρχει ήδη σε αυτό το αποθετήριο. +branch.branch_already_exists=Ο κλάδος «%s» υπάρχει ήδη σε αυτό το repository. branch.branch_name_conflict=Το όνομα του κλάδου «%s» συγκρούεται με το ήδη υπάρχον κλάδο «%s». branch.tag_collision=Ο κλάδος «%s» δεν μπορεί να δημιουργηθεί επειδή μια ετικέτα με το ίδιο όνομα υπάρχει ήδη στο αποθετήριο. branch.deleted_by=Διαγράφηκε από %s @@ -2691,7 +2691,7 @@ error.csv.invalid_field_count=Δεν είναι δυνατή η απόδοση commits.renamed_from = Μετονομάστηκε από %σ settings.wiki_rename_branch_main_desc = Ο κλάδος, ο οποίος χρησιμοποιείται εσωτερικά από το wiki, θα μετονομαστεί (μόνιμα και μη αναστράψιμα) σε «%s». issues.comment.blocked_by_user = Δεν μπορείτε να αφήσετε σχόλιο σε αυτό το ζήτημα, επειδή ο κάτοχος του αποθετηρίου ή το άτομο που δημιούργησε το ζήτημα σας έχει αποκλείσει. -pulls.blocked_by_user = Δεν μπορείτε να δημιουργήσετε pull request σε αυτό το αποθετήριο, επειδή ο κάτοχος του αποθετηρίου σας έχει αποκλείσει. +pulls.blocked_by_user = Δεν μπορείτε να δημιουργήσετε pull request σε αυτό το repository, επειδή ο κάτοχος του repository σας έχει αποκλείσει. pulls.made_using_agit = AGit wiki.cancel = Ακύρωση settings.units.add_more = Προσθήκη μονάδων... @@ -2710,13 +2710,13 @@ rss.must_be_on_branch = Για να αποκτήσετε ένα RSS feed, πρέ clone_in_vscodium = Κλωνοποίηση στο VSCodium editor.invalid_commit_mail = Αυτή η διεύθυνση email δεν είναι έγκυρη για την δημιουργία μίας υποβολής. pulls.nothing_to_compare_have_tag = Ο επιλεγμένος κλάδος/tag είναι όμοιος. -issues.blocked_by_user = Δεν μπορείτε να δημιουργήσετε ζητήματα σε αυτό το αποθετήριο, επειδή ο κάτοχος του αποθετηρίου σας έχει αποκλείσει. +issues.blocked_by_user = Δεν μπορείτε να δημιουργήσετε ζητήματα σε αυτό το repository, επειδή ο κάτοχος του repository σας έχει αποκλείσει. pulls.agit_explanation = Δημιουργημένο μέσω του AGit. Το AGit επιτρέπει σε συνεισφέροντες να προτείνουν αλλαγές χρησιμοποιώντας την εντολή «git push», χωρίς την δημιουργία fork ή έναν νέο κλάδο. activity.navbar.recent_commits = Πρόσφατες υποβολές settings.wiki_globally_editable = Να επιτρέπεται η επεξεργασία του wiki σε όλους admin.manage_flags = Διαχείριση σημάνσεων -admin.enabled_flags = Το αποθετήριο έχει τις εξής σημάνσεις: -settings.mirror_settings.pushed_repository = Προοριζόμενο αποθετήριο +admin.enabled_flags = Το repository έχει τις εξής σημάνσεις: +settings.mirror_settings.pushed_repository = Προοριζόμενο repository admin.flags_replaced = Οι σημάνσεις του αποθετηρίου αντικαταστάθηκαν activity.navbar.code_frequency = Συχνότητα κώδικα settings.wiki_branch_rename_success = Το όνομα κλάδου wiki του αποθετηρίου κανονικοποιήθηκε επιτυχώς. @@ -2767,7 +2767,7 @@ editor.commit_id_not_matching = Το αρχείο άλλαξε όσο το επ settings.sourcehut_builds.visibility = Ορατότητα εργασιών object_format = Μορφή αντικειμένων («object format») settings.ignore_stale_approvals_desc = Οι εγκρίσεις, οι οποίες αναφέρονται σε παλαιότερες υποβολές, δεν θα προσμετρούνται στο σύνολο των απαιτούμενων εγκρίσεων του pull request. Εφόσον αυτές οι εγκρίσεις έχουν ήδη ανακληθεί, τότε αυτή η ρύθμιση δεν θα παίξει κάποιον ρόλο. -settings.archive.mirrors_unavailable = Οι λειτουργίες ειδώλου δεν είναι διαθέσιμες εφόσον το αποθετήριο έχει αρχειοθετηθεί. +settings.archive.mirrors_unavailable = Οι λειτουργίες ειδώλου δεν είναι διαθέσιμες εφόσον το repository έχει αρχειοθετηθεί. settings.web_hook_name_sourcehut_builds = SourceHut Builds settings.enforce_on_admins = Εφαρμογή κανόνα σε διαχειριστές του αποθετηρίου object_format_helper = Η μορφή αντικειμένων («object format») του αποθετηρίου. Δεν θα μπορείτε να το αλλάξετε μεταγενέστερα. Η πιο συμβατή μορφή είναι η SHA1. @@ -2805,7 +2805,7 @@ release.type_attachment = Συνημμένο activity.published_prerelease_label = Προδημοσίευση activity.published_tag_label = Ετικέτα settings.pull_mirror_sync_quota_exceeded = Έχετε υπερβεί τους διαθέσιμους πόρους σας, για αυτό δεν θα γίνει λήψη των πιο πρόσφατων αλλαγών. -settings.transfer_quota_exceeded = Ο νέος ιδιοκτήτης (%s) έχει υπερβεί τους διαθέσιμους πόρους του. Το αποθετήριο δεν μπορεί να μεταφερθεί. +settings.transfer_quota_exceeded = Ο νέος ιδιοκτήτης (%s) έχει υπερβεί τους διαθέσιμους πόρους του. Το repository δεν μπορεί να μεταφερθεί. release.asset_name = Όνομα αρχείου release.asset_external_url = Εξωτερικό URL release.invalid_external_url = Μη έγκυρο εξωτερικό URL: «%s» @@ -2901,7 +2901,7 @@ teams.join=Συμμετοχή teams.leave=Αποχώρηση teams.leave.detail=Σίγουρα θέλετε να αποχωρήσετε από την ομάδα %s; teams.can_create_org_repo=Δημιουργία αποθετηρίων -teams.can_create_org_repo_helper=Τα μέλη μπορούν να δημιουργήσουν νέα αποθετήρια στον οργανισμό. Ο δημιουργός θα αποκτήσει πρόσβαση διαχειριστή στο νέο αποθετήριο. +teams.can_create_org_repo_helper=Τα μέλη μπορούν να δημιουργήσουν νέα αποθετήρια στον οργανισμό. Ο δημιουργός θα αποκτήσει πρόσβαση διαχειριστή στο νέο repository. teams.none_access=Καμία πρόσβαση teams.none_access_helper=Τα μέλη δεν μπορούν να δουν ή να κάνουν οποιαδήποτε άλλη ενέργεια σε αυτή τη μονάδα. teams.general_access=Γενική πρόσβαση @@ -2922,7 +2922,7 @@ teams.add_team_member=Προσθήκη μέλους ομάδας teams.invite_team_member=Πρόσκληση στην ομάδα %s teams.invite_team_member.list=Εκκρεμείς προσκλήσεις teams.delete_team_title=Διαγραφή ομάδας -teams.delete_team_desc=Η διαγραφή μιας ομάδας ανακαλεί τη πρόσβαση στο αποθετήριο από τα μέλη της. Συνέχεια; +teams.delete_team_desc=Η διαγραφή μιας ομάδας ανακαλεί τη πρόσβαση στο repository από τα μέλη της. Συνέχεια; teams.delete_team_success=Η ομάδα έχει διαγραφεί. teams.read_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση Ανάγνωσης: τα μέλη μπορούν να δουν και να κλωνοποιήσουν τα αποθετήρια της ομάδας. teams.write_permission_desc=Αυτή η ομάδα χορηγεί πρόσβαση Εγγραφής: τα μέλη μπορούν να διαβάσουν και να κάνουν push στα αποθετήρια της ομάδας. @@ -2934,9 +2934,9 @@ teams.remove_all_repos_title=Αφαίρεση όλων των αποθετηρί teams.remove_all_repos_desc=Αυτό θα αφαιρέσει όλα τα αποθετήρια από την ομάδα. teams.add_all_repos_title=Προσθήκη όλων των αποθετηρίων teams.add_all_repos_desc=Αυτό θα προσθέσει όλα τα αποθετήρια του οργανισμού στην ομάδα. -teams.add_nonexistent_repo=Το αποθετήριο που προσπαθείτε να προσθέσετε δεν υπάρχει, παρακαλώ δημιουργήστε το πρώτα. +teams.add_nonexistent_repo=Το repository που προσπαθείτε να προσθέσετε δεν υπάρχει, παρακαλώ δημιουργήστε το πρώτα. teams.add_duplicate_users=Ο χρήστης είναι ήδη μέλος της ομάδας. -teams.repos.none=Αυτή η ομάδα δεν έχει πρόσβαση σε κανένα αποθετήριο. +teams.repos.none=Αυτή η ομάδα δεν έχει πρόσβαση σε κανένα repository. teams.members.none=Δεν υπάρχουν μέλη σε αυτήν την ομάδα. teams.specific_repositories=Συγκεκριμένα αποθετήρια teams.specific_repositories_helper=Τα μέλη θα έχουν πρόσβαση μόνο σε αποθετήρια που προστίθενται ρητά στην ομάδα. Επιλέγοντας το δεν θα θα αφαιρεθούν αυτόματα τα αποθετήρια που έχουν ήδη προστεθεί με το Όλα τα αποθετήρια. @@ -3154,7 +3154,7 @@ packages.creator=Δημιουργός packages.name=Όνομα packages.version=Έκδοση packages.type=Τύπος -packages.repository=Αποθετήριο +packages.repository=Repository packages.size=Μέγεθος packages.published=Δημοσιευμένα @@ -3473,7 +3473,7 @@ notices.inverse_selection=Αντιστροφή επιλογής notices.delete_selected=Διαγραφή επιλεγμένων notices.delete_all=Διαγραφή όλων των ειδοποιήσεων notices.type=Τύπος -notices.type_1=Αποθετήριο +notices.type_1=Repository notices.type_2=Εργασία notices.desc=Περιγραφή notices.op=Λειτ. @@ -3511,7 +3511,7 @@ users.organization_creation.description = Να επιτρέπεται η δημ [action] create_repo=δημιούργησε το αποθετήριο %s -rename_repo=μετονόμασε το αποθετήριο από %[1]s σε %[3]s +rename_repo=μετονόμασε το repository από %[1]s σε %[3]s commit_repo=έκανε push στο %[3]s του %[4]s create_issue=`άνοιξε το ζήτημα %[3]s#%[2]s` close_issue=`έκλεισε το ζήτημα %[3]s#%[2]s` @@ -3523,7 +3523,7 @@ comment_issue=`άφησε σχόλιο στο ζήτημα %[3]s comment_pull=`σχολίασε στο pull request %[3]s#%[2]s` merge_pull_request=`συγχώνευσε το pull request %[3]s#%[2]s` auto_merge_pull_request=`αυτόματη συγχώνευση του pull request %[3]s#%[2]s` -transfer_repo=μετέφερε το αποθετήριο %s σε %s +transfer_repo=μετέφερε το repository %s σε %s push_tag=ώθησε την ετικέτα %[3]s σε %[4]s delete_tag=διέγραψε την ετικέτα %[2]s από %[3]s delete_branch=διέγραψε το κλάδο %[2]s από %[3]s @@ -3603,7 +3603,7 @@ title=Πακέτα desc=Διαχείριση πακέτων μητρώου. empty=Δεν υπάρχουν πακέτα ακόμα. empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, συμβουλευτείτε τον οδηγό. -empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις ρυθμίσεις πακέτων και συνδέστε το σε αυτό το αποθετήριο. +empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις ρυθμίσεις πακέτων και συνδέστε το σε αυτό το repository. registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, συμβουλευτείτε τον οδηγό. filter.type=Τύπος filter.type.all=Όλα @@ -3644,10 +3644,10 @@ composer.registry=Ρυθμίστε αυτό το μητρώο στο αρχεί composer.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Composer, εκτελέστε την ακόλουθη εντολή: composer.dependencies=Εξαρτήσεις composer.dependencies.development=Εξαρτήσεις Ανάπτυξης -conan.details.repository=Αποθετήριο +conan.details.repository=Repository conan.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: conan.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Conan, εκτελέστε την ακόλουθη εντολή: -conda.registry=Ρυθμίστε αυτό το μητρώο ως αποθετήριο Conda στο αρχείο .condarc: +conda.registry=Ρυθμίστε αυτό το μητρώο ως repository Conda στο αρχείο .condarc: conda.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Conda, εκτελέστε την ακόλουθη εντολή: container.details.type=Τύπος Εικόνας container.details.platform=Πλατφόρμα @@ -3705,8 +3705,8 @@ swift.registry=Ρυθμίστε αυτό το μητρώο από τη γραμ swift.install=Προσθέστε το πακέτο στο αρχείο Package.swift: swift.install2=και εκτελέστε την ακόλουθη εντολή: vagrant.install=Για προσθήκη ενός κυτίου Vagrant, εκτελέστε την ακόλουθη εντολή: -settings.link=Σύνδεση αυτού του πακέτου με ένα αποθετήριο -settings.link.description=Εάν συνδέσετε ένα πακέτο με ένα αποθετήριο, το πακέτο περιλαμβάνεται στη λίστα πακέτων του αποθετηρίου. +settings.link=Σύνδεση αυτού του πακέτου με ένα repository +settings.link.description=Εάν συνδέσετε ένα πακέτο με ένα repository, το πακέτο περιλαμβάνεται στη λίστα πακέτων του repository. settings.link.select=Επιλογή Αποθετηρίου settings.link.button=Ενημέρωση Συνδέσμου Αποθετηρίου settings.link.success=Ο σύνδεσμος αποθετηρίου ενημερώθηκε επιτυχώς. @@ -3718,7 +3718,7 @@ settings.delete.success=Το πακέτο έχει διαγραφεί. settings.delete.error=Αποτυχία διαγραφής του πακέτου. owner.settings.cargo.title=Ευρετήριο μητρώου Cargo owner.settings.cargo.initialize=Αρχικοποίηση ευρετηρίου -owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό αποθετήριο ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το αποθετήριο και θα ρυθμιστεί αυτόματα. +owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό repository ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το repository και θα ρυθμιστεί αυτόματα. owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία. owner.settings.cargo.rebuild=Ανανέωση ευρετηρίου @@ -3811,7 +3811,7 @@ runners.task_list=Πρόσφατες εργασίες στον εκτελεστ runners.task_list.no_tasks=Δεν υπάρχει καμία εργασία ακόμα. runners.task_list.run=Εκτέλεση runners.task_list.status=Κατάσταση -runners.task_list.repository=Αποθετήριο +runners.task_list.repository=Repository runners.task_list.commit=Υποβολή runners.task_list.done_at=Ολοκλήρωσε Στις runners.edit_runner=Επεξεργασία Εκτελεστή diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 643a61b456..75ee89b545 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -147,6 +147,11 @@ value = Arvo rerun = Suorita uudelleen filter.clear = Tyhjennä suodattimet invalid_data = Virheellistä dataa: %v +new_repo.title = Uusi repositorio +new_org.title = Uusi organisaatio +new_org.link = Uusi organisaatio +new_repo.link = Uusi repositorio +new_migrate.link = Uusi siirto [aria] footer.links = Linkit diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 656ead34e1..0df97ec31b 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -207,7 +207,7 @@ string.desc=Z - A [error] occurred=Une erreur s’est produite -report_message=Si vous pensez qu'il s'agit d'un bug Forgejo, veuillez consulter notre board Codeberg ou ouvrir un nouveau ticket si nécessaire. +report_message=Si vous pensez qu'il s'agit d'un bug Forgejo, veuillez consulter les tickets de Codeberg ou ouvrir un nouveau ticket si nécessaire. missing_csrf=Requête incorrecte : aucun jeton CSRF présent invalid_csrf=Requête incorrecte : jeton CSRF invalide not_found=La cible n'a pu être trouvée. @@ -223,7 +223,7 @@ platform_desc=Forgejo est confirmé fonctionner sur des systèmes d'exploitation lightweight=Léger lightweight_desc=Forgejo utilise peu de ressources. Il peut même tourner sur un Raspberry Pi très bon marché. Économisez l'énergie de vos serveurs ! license=Open Source -license_desc=Toutes les sources sont sur Forgejo ! Rejoignez-nous et contribuez à rendre ce projet encore meilleur ! +license_desc=Toutes les sources sont sur Forgejo ! Rejoignez-nous et contribuez à rendre ce projet encore meilleur. Ne craignez pas de devenir un·e contributeur·trice ! [install] install=Installation @@ -558,6 +558,11 @@ removed_security_key.subject = Une clé de sécurité a été supprimée totp_disabled.subject = TOTP a été désactivé removed_security_key.no_2fa = Il n'y a plus de méthodes 2FA configurées ce qui signifie qu'il n'est plus nécessaire d'utiliser 2FA pour se connecter à votre compte. account_security_caution.text_1 = Si vous êtes à l’origine de cette action, vous pouvez ignorer ce courriel. +totp_enrolled.text_1.no_webauthn = Vous venez d'activer TOTP pour votre compte. Cela signifie que pour toutes les prochaines connexions à votre compte, vous devrez utiliser TOTP comme méthode 2FA. +totp_enrolled.subject = Vous avez activé TOTP comme méthode 2FA +totp_enrolled.text_1.has_webauthn = Vous venez d'activer TOTP pour votre compte. Cela signifie que pour toutes les prochaines connexions à votre compte, vous pouvez utiliser TOTP comme méthode 2FA ou l'une de vos clés de sécurité. +removed_security_key.text_1 = La clé de sécurité « %[1]s » vient d'être supprimée de votre compte. +account_security_caution.text_2 = S'il ne s'agissait pas de vous, votre compte est compromis. Veuillez contacter les administrateurs du site. [modal] yes=Oui @@ -823,7 +828,7 @@ add_new_email=Ajouter une nouvelle adresse e-mail add_new_openid=Ajouter une nouvelle URI OpenID add_email=Ajouter une adresse courriel add_openid=Ajouter une URI OpenID -add_email_confirmation_sent=Un e-mail de confirmation a été envoyé à "%s". Veuillez vérifier votre boîte de réception dans les %s suivants pour confirmer votre adresse e-mail. +add_email_confirmation_sent=Un courriel de confirmation a été envoyé à « %s ». Pour confirmer votre adresse de courriel, veuillez vérifier votre boîte de réception et suivre le lien indiqué dans les prochains %s. add_email_success=La nouvelle adresse e-mail a été ajoutée. email_preference_set_success=L'e-mail de préférence a été défini avec succès. add_openid_success=La nouvelle adresse OpenID a été ajoutée. @@ -1042,6 +1047,8 @@ pronouns = Pronoms pronouns_unspecified = Non spécifiés language.title = Langue par défaut keep_activity_private.description = Vous seul pourrez voir votre activité publique, ainsi que les administrateurs de l'instance. +language.localization_project = Aidez-nous à traduire Forgejo dans votre langue ! En savoir plus. +language.description = Cette langue sera enregistrée dans votre compte et utilisée comme langue par défaut après votre connexion. [repo] new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l’historique de leurs modifications. Vous avez déjà ça ailleurs ? Migrez-le ici. @@ -1421,7 +1428,7 @@ commitstatus.failure=Échec commitstatus.pending=En attente commitstatus.success=Succès -ext_issues=Accès aux tickets externes +ext_issues=Tickets externes ext_issues.desc=Lien vers un gestionnaire de tickets externe. projects=Projets @@ -1602,9 +1609,9 @@ issues.no_content=Sans contenu. issues.close=Fermer le ticket issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s -issues.close_comment_issue=Commenter et fermer +issues.close_comment_issue=Fermer avec le commentaire issues.reopen_issue=Rouvrir -issues.reopen_comment_issue=Commenter et réouvrir +issues.reopen_comment_issue=Réouvrir avec le commentaire issues.create_comment=Commenter issues.closed_at=`a fermé ce ticket %[2]s.` issues.reopened_at=`a réouvert ce ticket %[2]s.` @@ -1993,7 +2000,7 @@ signing.wont_sign.commitssigned=La fusion ne sera pas signée car ses révisions signing.wont_sign.approved=La fusion ne sera pas signée car la demande d'ajout n'a pas été approuvée. signing.wont_sign.not_signed_in=Vous n'êtes pas connecté. -ext_wiki=Accès au wiki externe +ext_wiki=Wiki externe ext_wiki.desc=Lier un wiki externe. wiki=Wiki @@ -2418,14 +2425,14 @@ settings.protect_enable_merge_desc=Toute personne ayant un accès en écriture s settings.protect_whitelist_committers=Liste blanche des soumissions (push) settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront soumettre sur cette branche (sans forcer). settings.protect_whitelist_deploy_keys=Mettez les clés de déploiement sur liste blanche avec accès en écriture pour soumettre. -settings.protect_whitelist_users=Utilisateurs sur liste blanche : +settings.protect_whitelist_users=Utilisateurs sur liste blanche pour pousser settings.protect_whitelist_search_users=Rechercher des utilisateurs… -settings.protect_whitelist_teams=Équipes sur liste blanche : +settings.protect_whitelist_teams=Équipes sur liste blanche pour pousser settings.protect_whitelist_search_teams=Rechercher des équipes… settings.protect_merge_whitelist_committers=Activer la liste blanche pour la fusion settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateurs et les équipes en liste blanche d'appliquer les demandes de fusion sur cette branche. -settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion : -settings.protect_merge_whitelist_teams=Équipes en liste blanche de fusion : +settings.protect_merge_whitelist_users=Utilisateurs en liste blanche pour fusionner +settings.protect_merge_whitelist_teams=Équipes en liste blanche pour fusionner settings.protect_check_status_contexts=Activer le contrôle de status settings.protect_status_check_patterns=Motifs de vérification des statuts : settings.protect_status_check_patterns_desc=Entrez des motifs pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide. @@ -2434,12 +2441,12 @@ settings.protect_check_status_contexts_list=Contrôles qualité trouvés au cour settings.protect_status_check_matched=Correspondant settings.protect_invalid_status_check_pattern=Motif de vérification des statuts incorrect : « %s ». settings.protect_no_valid_status_check_patterns=Aucun motif de vérification des statuts valide. -settings.protect_required_approvals=Minimum d'approbations requis : +settings.protect_required_approvals=Approbations requises settings.protect_required_approvals_desc=Permet de fusionner les demandes d’ajout lorsque suffisamment d’évaluation sont positives. settings.protect_approvals_whitelist_enabled=Restreindre les approbations aux utilisateurs ou aux équipes en liste blanche settings.protect_approvals_whitelist_enabled_desc=Seuls les évaluations des utilisateurs ou des équipes suivantes compteront dans les approbations requises. Si laissé vide, les évaluations de toute personne ayant un accès en écriture seront comptabilisées à la place. -settings.protect_approvals_whitelist_users=Évaluateurs autorisés : -settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés : +settings.protect_approvals_whitelist_users=Évaluateurs autorisés +settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées. settings.ignore_stale_approvals=Ignorer les approbations obsolètes @@ -2449,9 +2456,9 @@ settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche l settings.protect_branch_name_pattern=Motif de nom de branche protégé settings.protect_branch_name_pattern_desc=Motifs de nom de branche protégé. Consultez la documentation pour la syntaxe du motif. Exemples : main, release/** settings.protect_patterns=Motifs -settings.protect_protected_file_patterns=Liste des fichiers et motifs protégés (séparés par un point virgule ";") : -settings.protect_protected_file_patterns_desc=Les fichiers protégés ne peuvent être modifiés, même si l'utilisateur a le droit d'ajouter, éditer ou supprimer des fichiers dans cette branche. Plusieurs motifs peuvent être séparés par un point-virgule (";"). Veuillez voir github.com/gobwas/glob la documentation pour la syntaxe des motifs. Exemples : .drone.yml, /docs/**/*.txt. -settings.protect_unprotected_file_patterns=Liste des fichiers et motifs exclus (séparés par un point virgule ";") : +settings.protect_protected_file_patterns=Motifs de fichiers protégés (séparés par un point virgule ";") +settings.protect_protected_file_patterns_desc=Les fichiers protégés ne peuvent être modifiés, même si l'utilisateur a le droit d'ajouter, éditer ou supprimer des fichiers dans cette branche. Plusieurs motifs peuvent être séparés par un point-virgule (";"). Veuillez voir %[2]s la documentation pour la syntaxe des motifs. Exemples : .drone.yml, /docs/**/*.txt. +settings.protect_unprotected_file_patterns=Motifs de fichiers non protégés (séparés par un point virgule ";") settings.protect_unprotected_file_patterns_desc=Les fichiers non-protégés qui peuvent être modifiés si l'utilisateur a le droit d'écriture, prenant le pas sur les restrictions de push. Plusieurs motifs peuvent être séparés par un point-virgule (";"). Veuillez voir %[2]s la documentation pour la syntaxe des motifs. Exemples : .drone.yml, /docs/**/*.txt. settings.add_protected_branch=Activer la protection settings.delete_protected_branch=Désactiver la protection @@ -2811,6 +2818,23 @@ settings.federation_following_repos = Les URL des dépôts suivis séparés par settings.federation_not_enabled = La fédération n'est pas activée pour votre instance. comments.edit.already_changed = Impossible de sauvegarder les changements du commentaire car son contenu a déjà été modifié par un autre utilisateur. Veuillez recharger la page et essayer de l'éditer à nouveau pour éviter d'écraser ses changements settings.federation_apapiurl = URL de fédération de ce dépôt. A copier-coller dans les paramètres de fédérations d'un autre dépôt comme URL d'un dépôt à suivre. +mirror_denied_combination = Il n'est pas possible de combiner une authentification par clé publique et par mot de passe. +mirror_public_key = Clé SSH publique +mirror_use_ssh.text = Utiliser l'authentification SSH +mirror_use_ssh.helper = Forgejo va créer un miroir du dépôt via Git sur SSH et créer une paire de clés pour vous lorsque vous sélectionnez cette option. Vous devez vous assurer que la clé publique générée est autorisée à pousser dans le dépôt de destination. Il n'est pas possible d'utiliser l'autorisation basée sur un mot de passe si vous choisissez cette option. +no_eol.text = Pas d'EOL +mirror_use_ssh.not_available = L'authentification par SSH n'est pas disponible. +no_eol.tooltip = Ce fichier ne contient pas de caractère final de fin de ligne. +release.type_attachment = Pièce jointe +settings.transfer_quota_exceeded = Le nouvel utilisateur (%s) a dépassé son quota. Le dépôt n'a pas été transféré. +settings.pull_mirror_sync_quota_exceeded = Quota dépassé, les modifications ne sont pas tirées. +activity.commit = Activité de commit +settings.mirror_settings.push_mirror.copy_public_key = Copier la clé publique +release.asset_external_url = URL externe +release.invalid_external_url = URL externe non valable : « %s » +milestones.filter_sort.name = Nom +settings.mirror_settings.push_mirror.none_ssh = Aucun +settings.protect_new_rule = Créer une nouvelle règle de protection de branche [graphs] component_loading=Chargement de %s… diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index e34da348a5..0cb6c0f7ab 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -3,7 +3,7 @@ home=Forsíða dashboard=Stjórnborð explore=Vafra help=Hjálp -sign_in=Skrá Inn +sign_in=Skrá inn sign_in_or=eða sign_out=Skrá Út sign_up=Nýskráning @@ -15,9 +15,9 @@ page=Síða template=Sniðmát language=Tungumál notifications=Tilkynningar -active_stopwatch=Virk Tímamæling +active_stopwatch=Virk tímamæling create_new=Skapa… -user_profile_and_more=Notandasíða og Stillingar… +user_profile_and_more=Notandasíða og stillingar… signed_in_as=Skráð(ur) inn sem toc=Efnisyfirlit licenses=Hugbúnaðarleyfi @@ -111,6 +111,8 @@ concept_code_repository=Hugbúnaðarsafn name=Heiti value=Gildi +sign_in_with_provider = Skrá inn með %s +enable_javascript = Þessi síða krefst JavaScript. [aria] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 0860176b18..f6fcd5135f 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -471,6 +471,11 @@ openid_signin_desc = Inserisci il tuo URI OpenID. Per esempio: alice.openid.exam password_pwned = La password che hai scelto è in un elenco di password rubate precedentemente esposte a violazioni di dati pubblici. Riprova con una password diversa e valuta di modificare questa password anche altrove. tab_signup = Registrati tab_signin = Accedi +back_to_sign_in = Torna alla schermata d'accesso +sign_in_openid = Procedi con OpenID +hint_login = Hai già un'utenza? Accedi! +hint_register = Non hai un'utenza? Registrati ora. +sign_up_button = Registrati ora. [mail] view_it_on=Visualizza su %s @@ -539,6 +544,21 @@ activate_email.title = %s, verifica il tuo indirizzo email admin.new_user.text = Clicca qui per gestire questo utente dal pannello di amministrazione. team_invite.text_1 = %[1]s ti ha invitato a far parte del team %[2]s nell'organizzazione %[3]s. team_invite.text_3 = Nota: Questo invito è destinato a %[1]s. Se non ti aspettavi questo invito, puoi ignorare questa email. +primary_mail_change.subject = La tua mail principale è stata cambiata +removed_security_key.no_2fa = Non ci sono più altri metodi di autenticazione a due fattori configurati, ergo non c'è più bisogno di accedere alla tua utenza tramite tale autenticazione. +primary_mail_change.text_1 = La mail principale della tua utenza è appena stata cambiata in %[1]s. Ciò significa che questo indirizzo di posta elettronica non riceverà più notifiche mail da quest'utenza. +totp_disabled.subject = La TOTP è stata disabilitata +totp_disabled.no_2fa = Non ci sono più altri metodi d'autenticazione a due fattori configurati, ergo non c'è più bisogno di accedere alla tua utenza con tale autenticazione. +removed_security_key.subject = È stata rimossa una chiave di sicurezza +removed_security_key.text_1 = La chiave di sicurezza "%[1]s" è appena stata rimossa dalla tua utenza. +totp_disabled.text_1 = La password a tempo usa e getta (TOTP) della tua utenza è appena stata disabilitata. +totp_enrolled.subject = Hai attivato la TOTP come metodo d'autenticazione a due fattori +totp_enrolled.text_1.no_webauthn = Hai appena attivato la TOTP per la tua utenza. Ciò significa che dovrai usarla come metodo d'autenticazione a due fattori per tutti i tuoi accessi futuri. +totp_enrolled.text_1.has_webauthn = Hai appena attivato la TOTP per la tua utenza. Ciò significa che dovrai usare come metodo d'autenticazione a due fattori per i tuoi accessi futuri tale TOTP o una delle tue chiavi di sicurezza. +password_change.subject = La tua password è stata modificata +password_change.text_1 = La password della tua utenza è appena stata modificata. +account_security_caution.text_1 = Se sei statə tu, puoi ignorare questa mail. +account_security_caution.text_2 = Se non sei statə tu, la tua utenza è compromessa. Contatta l'amministrazione del sito. [modal] @@ -1022,6 +1042,8 @@ pronouns = Pronomi pronouns_custom = Personalizzato pronouns_unspecified = Non specificato language.title = Lingua predefinita +language.description = Questa lingua verrà salvata nella tua utenza e verrà usata come predefinita ogni volta che farai l'accesso. +language.localization_project = Aiutaci a tradurre Forgejo nella tua lingua! Più informazioni. [repo] owner=Proprietario @@ -3863,6 +3885,7 @@ exact_tooltip = Includi solo i risultati che corrispondono esattamente al termin issue_kind = Cerca segnalazioni... pull_kind = Cerca richieste... exact = Esatto +milestone_kind = Ricerca tappe... [munits.data] gib = GiB diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index f53356ee71..fba7f37808 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -158,6 +158,13 @@ filter.not_template = テンプレートではない invalid_data = 無効なデータ: %v more_items = さらに表示 copy_generic = クリップボードへコピー +new_repo.title = 新しいリポジトリ +new_migrate.title = 新しいマイグレーション +new_org.title = 新しい組織 +new_repo.link = 新しいリポジトリ +new_migrate.link = 新しいマイグレーション +new_org.link = 新しい組織 +test = テスト [aria] navbar=ナビゲーションバー diff --git a/options/locale/locale_lt.ini b/options/locale/locale_lt.ini index 35f56fa4fd..b55a749cb2 100644 --- a/options/locale/locale_lt.ini +++ b/options/locale/locale_lt.ini @@ -159,6 +159,18 @@ fuzzy_tooltip = Įtraukti rezultatus, kurie taip pat labai atitinka paieškos te repo_kind = Ieškoti saugyklų... code_search_unavailable = Kodų paieška šiuo metu nepasiekiama. Kreipkis į svetainės administratorių. org_kind = Ieškoti organizacijų... +union = Bendrinis +code_search_by_git_grep = Dabartiniai kodo paieškos rezultatai pateikiami atliekant „git grep“. Rezultatai gali būti geresni, jei svetainės administratorius įjungs kodo indeksuotoją. +package_kind = Ieškoti paketų... +project_kind = Ieškoti projektų... +commit_kind = Ieškoti įsipareigojimų... +runner_kind = Ieškoti vykdyklių... +no_results = Nerasta atitinkamų rezultatų. +issue_kind = Ieškoti problemų... +branch_kind = Ieškoti šakų... +milestone_kind = Ieškoti gairių... +pull_kind = Ieškoti sujungimų... +keyword_search_unavailable = Ieškoti pagal raktažodį šiuo metu nepasiekiamas. Susisiekite su svetainės administratoriumi. [actions] workflow.disable = Išjungti darbo eigą @@ -171,6 +183,9 @@ runs.empty_commit_message = (tuščias įsipareigojimo pranešimas) submodule = Pomodulis changed_filemode = %[1]s → %[2]s symbolic_link = Virtualusis aplankas +directory = Katalogas +executable_file = Vykdomasis failas +normal_file = Įprastas failas [projects] deleted.display_name = Ištrintas projektas @@ -182,4 +197,64 @@ type-3.display_name = Organizacijos projektas filepreview.truncated = Peržiūra buvo sutrumpinta [mail] -reset_password.text = Jei tai buvote jūs, spustelėkite toliau esančią nuorodą, kad atkurtumėte savo paskyrą per %s: \ No newline at end of file +reset_password.text = Jei tai buvote jūs, spustelėkite toliau esančią nuorodą, kad atkurtumėte savo paskyrą per %s: + +[heatmap] +contributions_one = įnašas +contributions_few = įnašai +less = Mažiau +more = Daugiau +number_of_contributions_in_the_last_12_months = %s įnašų per pastaruosius 12 mėnesių +contributions_zero = Įnašų nėra +contributions_format = {contributions} {year} {month} {day} + +[aria] +navbar = Naršymo juosta +footer = Puslapinė poraštė +footer.software = Apie šią programinę įrangą +footer.links = Nuorodos + +[editor] +buttons.quote.tooltip = Cituoti tekstą +buttons.code.tooltip = Pridėti kodą +buttons.link.tooltip = Pridėti nuorodą +buttons.heading.tooltip = Pridėti antraštę +buttons.bold.tooltip = Pridėti pusjuodį tekstą +buttons.italic.tooltip = Pridėti kursyvinį tekstą + +[error] +network_error = Tinklo klaida +server_internal = Vidinio serverio klaida + +[startpage] +app_desc = Nesudėtinga, savarankiškai teikiama „Git“ paslauga +install = Lengva įdiegti + +[install] +path = Kelias +err_admin_name_is_reserved = Administratoriaus naudotojo vardas netinkamas. Naudotojo vardas yra rezervuotas. +enable_update_checker = Įjungti naujinimų tikrintuvą +env_config_keys = Aplinkos konfigūracija +db_title = Duomenų bazės nustatymai +db_type = Duomenų bazės tipas +user = Naudotojo vardas +password = Slaptažodis +db_name = Duomenų bazės pavadinimas +db_schema = Schema +ssl_mode = SSL +host = Pagrindinis komputeris +general_title = Bendrieji nustatymai +email_title = El. pašto nustatymai +federated_avatar_lookup.description = Ieškokite pseudoportretų naudojant „Libravatar“. +db_schema_helper = Palikite tuščią, jei tai numatytoji duomenų bazė („public“). +err_empty_admin_password = Administratoriaus slaptažodis negali būti tuščias. +err_empty_admin_email = Administratoriaus el. paštas negali būti tuščias. + +[explore] +go_to = Eiti į +code = Kodas + +[auth] +remember_me = Prisiminti šį įrenginį +forgot_password_title = Pamirštas slaptažodis +forgot_password = Pamiršote slaptažodį? \ No newline at end of file diff --git a/options/locale/locale_nb_NO.ini b/options/locale/locale_nb_NO.ini index aae4ae788f..349d56ce13 100644 --- a/options/locale/locale_nb_NO.ini +++ b/options/locale/locale_nb_NO.ini @@ -23,4 +23,115 @@ language = Språk notifications = Varslinger create_new = Opprett… user_profile_and_more = Profil og innstillinger… -signed_in_as = Logget inn som \ No newline at end of file +signed_in_as = Logget inn som +confirm_delete_selected = Bekreft sletting av alle valgte elementer? +dashboard = Dashbord +download_logs = Last ned logger +copy_hash = Kopier hash +more_items = Flere elementer +passcode = Adgangskode +webauthn_insert_key = Skriv inn din sikkerhetsnøkkel +webauthn_use_twofa = Bruk tofaktorkode fra din mobil +organization = Organisasjon +mirror = Speil +new_mirror = Ny speiling +repository = Repositorium +new_project = Nytt prosjekt +new_project_column = Ny kolonne +webauthn_error = Klarte ikke lese sikkerhetsnøkkelen. +webauthn_unsupported_browser = Nettleseren din støtter ikke WebAuthn. +webauthn_error_unknown = En ukjent feil oppstod. Vennligst prøv igjen. +webauthn_error_insecure = WebAuhn støtter kun sikre forbindelser. For testing over HTTP kan du bruke verten "localhost" eller "127.0.0.1" +admin_panel = Nettsideadministrasjon +settings = Innstillinger +your_profile = Profil +your_starred = Stjernemerket +your_settings = Innstillinger +new_repo.title = Nytt repositorium +new_migrate.title = Ny migrasjon +new_org.title = Ny organisasjon +new_repo.link = Nytt repositorium +new_migrate.link = Ny migrasjon +new_org.link = Ny organisasjon +all = Alle +sources = Kilder +mirrors = Speilinger +activities = Aktiviteter +rss_feed = RSS feed +retry = Prøv igjen +rerun = Kjør på nytt +rerun_all = Kjør alle jobber på nytt +save = Lagre +cancel = Avbryt +forks = Forks +milestones = Milepæler +ok = OK +test = Test +loading = Laster inn… +error = Feil +go_back = Gå tilbake +never = Aldri +invalid_data = Ugyldig data: %v +unknown = Ukjent +pin = Pin +artifacts = Artefakter +archived = Arkivert +concept_system_global = Global +add = Legg til +add_all = Legg til alle +remove = Fjern +remove_all = Fjern alle +remove_label_str = Fjern element "%s" +edit = Rediger +view = Vis +enabled = Aktivert +disabled = Deaktivert +locked = Låst +copy = Kopier +copy_generic = Kopier til utklippstavlen +copy_url = Kopier URL +copy_content = Kopier innhold +copy_success = Kopiert! +copy_error = Kopiering mislyktes +copy_type_unsupported = Denne filtypen kan ikke kopieres +write = Skriv +preview = Forhåndsvis +concept_user_individual = Individuell +concept_code_repository = Repositorium +concept_user_organization = Organisasjon +show_timestamps = Vis tidsstempler +show_log_seconds = Vis sekunder +show_full_screen = Vis fullskjerm +name = Navn +value = Verdi +filter = Filter +filter.clear = Tøm filtre +filter.is_archived = Arkivert +filter.not_archived = Ikke arkivert +filter.is_mirror = Speilinger +filter.not_mirror = Ikke speilinger +filter.is_template = Maler +filter.not_template = Ikke maler +filter.public = Offentlig +filter.private = Privat +explore = Utforsk +active_stopwatch = Aktiv tidsregistrering +home = Hjem +help = Hjelp +logo = Logo +sign_in = Logg inn +sign_in_with_provider = Logg inn med %s +sign_in_or = eller +sign_out = Logg ut +sign_up = Opprett konto +confirm_delete_artifact = Er du sikker på at du vil slette artefakten "%s" ? + +[search] +search = Søk... +type_tooltip = Søketype +fuzzy = Fuzzy +union = Union + +[auth] +verify = Bekreft +sign_up_button = Opprett konto nå. \ No newline at end of file diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 0938d4099c..e13b1341c4 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1406,7 +1406,7 @@ commitstatus.failure=Неудача commitstatus.pending=Ожидание commitstatus.success=Успешно -ext_issues=Доступ ко внешним задачам +ext_issues=Внешние задачи ext_issues.desc=Ссылка на внешнюю систему отслеживания задач. projects=Проекты @@ -1587,9 +1587,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.close_comment_issue=Закрыть комментарием issues.reopen_issue=Открыть снова -issues.reopen_comment_issue=Прокомментировать и открыть снова +issues.reopen_comment_issue=Открыть снова комментарием issues.create_comment=Комментировать issues.closed_at=`задача была закрыта %[2]s` issues.reopened_at=`задача была открыта снова %[2]s` @@ -1964,7 +1964,7 @@ signing.wont_sign.commitssigned=Слияние не будет подписан signing.wont_sign.approved=Слияние не будет подписано, так как запрос на слияние не одобрен. signing.wont_sign.not_signed_in=Вы не вошли в систему. -ext_wiki=Доступ ко внешней вики +ext_wiki=Внешняя вики ext_wiki.desc=Ссылка на внешнюю вики. wiki=Вики @@ -3332,7 +3332,7 @@ config.allow_only_external_registration=Регистрация только че config.enable_openid_signup=Саморегистрация через OpenID config.enable_openid_signin=Вход через OpenID config.show_registration_button=Кнопка регистрации -config.require_sign_in_view=Для просмотра содержимого необходима авторизация +config.require_sign_in_view=Требовать авторизацию для просмотра содержимого config.mail_notify=Уведомления по эл. почте config.enable_captcha=CAPTCHA config.active_code_lives=Срок действия кода активации учётной записи @@ -3963,4 +3963,8 @@ filepreview.lines = Строки с %[1]d по %[2]d в %[3]s filepreview.truncated = Предпросмотр был обрезан [translation_meta] -test = хи-хи! \ No newline at end of file +test = хи-хи! + +[repo.permissions] +code.write = Запись: отправка изменений в репозиторий, создание веток и тегов. +code.read = Чтение: доступ к исходному коду репозитория и клонированию. \ No newline at end of file diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 1873b11478..7a1a77c3e5 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1427,7 +1427,7 @@ commitstatus.failure=失败 commitstatus.pending=待定 commitstatus.success=成功 -ext_issues=访问外部工单 +ext_issues=外部工单 ext_issues.desc=链接到外部工单跟踪系统。 projects=项目 @@ -1608,9 +1608,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.close_comment_issue=关闭评论 issues.reopen_issue=重新开启 -issues.reopen_comment_issue=评论并重新开启 +issues.reopen_comment_issue=重新打开评论 issues.create_comment=评论 issues.closed_at=`于 %[2]s 关闭此工单` issues.reopened_at=`重新打开此问题 %[2]s` @@ -1999,7 +1999,7 @@ signing.wont_sign.commitssigned=合并将不会被签名,因为所有相关的 signing.wont_sign.approved=合并将不会被签名,因为合并请求未被批准。 signing.wont_sign.not_signed_in=您还没有登录。 -ext_wiki=访问外部百科 +ext_wiki=外部百科 ext_wiki.desc=链接到外部 wiki。 wiki=百科 @@ -2330,7 +2330,7 @@ settings.event_repository_desc=创建或删除仓库 settings.event_header_issue=工单事件 settings.event_issues=工单 settings.event_issues_desc=工单已打开、已关闭、已重新打开或已编辑。 -settings.event_issue_assign=工单已分配 +settings.event_issue_assign=工单已指派 settings.event_issue_assign_desc=工单已被指派或取消指派。 settings.event_issue_label=工单已分类 settings.event_issue_label_desc=工单标签被更新或清除。 @@ -3818,7 +3818,7 @@ management=密钥管理 [actions] actions=Actions -unit.desc=使用 Forgejo Actions 管理集成的 CI/CD 管道 +unit.desc=使用 Forgejo Actions 管理集成的 CI/CD 管道。 status.unknown=未知 status.waiting=等待中 @@ -3980,4 +3980,24 @@ filepreview.lines = %[3]s 中的第 %[1]d 到 %[2]d 行 filepreview.truncated = 预览已被截断 [translation_meta] -test = 好的 \ No newline at end of file +test = 好的 + +[repo.permissions] +code.write = 写入:推送到仓库,创建分支和标签。 +code.read = 读取:访问并克隆仓库的代码。 +actions.read = 读取:查看集成的 CI/CD 管道及其日志。 +issues.write = 写入:关闭工单并管理元数据,如标签、里程碑、指派成员、截止日期和依赖。 +releases.write = 写入:发布、编辑和删除版本发布及其资产。 +issues.read = 读取:阅读并创建工单和评论。 +pulls.read = 读取:阅读并创建合并请求。 +releases.read = 读取:查看并下载版本发布。 +wiki.read = 读取:阅读集成的百科及其历史。 +wiki.write = 写入:在集成的百科中创建、更新和删除页面。 +projects.read = 读取:访问仓库项目看板。 +packages.read = 读取:查看并下载指派给仓库的软件包。 +packages.write = 写入:发布并删除指派给仓库的软件包。 +actions.write = 写入:手动触发、重启、取消或批准待处理的 CI/CD 管道。 +ext_issues = 访问外部工单系统的链接。权限由外部管理。 +ext_wiki = 访问外部百科的链接。权限由外部管理。 +projects.write = 写入:创建项目和列并进行编辑。 +pulls.write = 写入:关闭合并请求并管理元数据,如标签、里程碑、指派成员、截止日期和依赖。 \ No newline at end of file From a75862bd7d76654a4706769a01f693b425f34b05 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 29 Sep 2024 08:03:17 +0000 Subject: [PATCH 16/89] Update dependency webpack to v5.95.0 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7dd1cba03a..7da312bb80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.94.0", + "webpack": "5.95.0", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, @@ -16379,9 +16379,9 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.95.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", + "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.5", @@ -16494,9 +16494,9 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, "node_modules/webpack/node_modules/ajv": { diff --git a/package.json b/package.json index 12c6a05c12..a9cf80684b 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "vue-chartjs": "5.3.1", "vue-loader": "17.4.2", "vue3-calendar-heatmap": "2.0.5", - "webpack": "5.94.0", + "webpack": "5.95.0", "webpack-cli": "5.1.4", "wrap-ansi": "9.0.0" }, From 14c7055494b995476d9d2ec1948784bf36dd9e4d Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 22 Sep 2024 13:01:09 +0200 Subject: [PATCH 17/89] Fix artifact v4 upload above 8MB (#31664) Multiple chunks are uploaded with type "block" without using "appendBlock" and eventually out of order for bigger uploads. 8MB seems to be the chunk size This change parses the blockList uploaded after all blocks to get the final artifact size and order them correctly before calculating the sha256 checksum over all blocks Fixes #31354 (cherry picked from commit b594cec2bda6f861effedb2e8e0a7ebba191c0e9) Conflicts: routers/api/actions/artifactsv4.go conflict because of Refactor AppURL usage (#30885) 67c1a07285008cc00036a87cef966c3bd519a50c that was not cherry-picked in Forgejo the resolution consist of removing the extra ctx argument --- routers/api/actions/artifacts_chunks.go | 50 +++++- routers/api/actions/artifactsv4.go | 144 +++++++++++++----- .../api_actions_artifact_v4_test.go | 130 ++++++++++++++++ 3 files changed, 285 insertions(+), 39 deletions(-) diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index b0c96585cb..cdb56584b8 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -123,6 +123,54 @@ func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chun return chunksMap, nil } +func listChunksByRunIDV4(st storage.ObjectStorage, runID, artifactID int64, blist *BlockList) ([]*chunkFileItem, error) { + storageDir := fmt.Sprintf("tmpv4%d", runID) + var chunks []*chunkFileItem + chunkMap := map[string]*chunkFileItem{} + dummy := &chunkFileItem{} + for _, name := range blist.Latest { + chunkMap[name] = dummy + } + if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error { + baseName := filepath.Base(fpath) + if !strings.HasPrefix(baseName, "block-") { + return nil + } + // when read chunks from storage, it only contains storage dir and basename, + // no matter the subdirectory setting in storage config + item := chunkFileItem{Path: storageDir + "/" + baseName, ArtifactID: artifactID} + var size int64 + var b64chunkName string + if _, err := fmt.Sscanf(baseName, "block-%d-%d-%s", &item.RunID, &size, &b64chunkName); err != nil { + return fmt.Errorf("parse content range error: %v", err) + } + rchunkName, err := base64.URLEncoding.DecodeString(b64chunkName) + if err != nil { + return fmt.Errorf("failed to parse chunkName: %v", err) + } + chunkName := string(rchunkName) + item.End = item.Start + size - 1 + if _, ok := chunkMap[chunkName]; ok { + chunkMap[chunkName] = &item + } + return nil + }); err != nil { + return nil, err + } + for i, name := range blist.Latest { + chunk, ok := chunkMap[name] + if !ok || chunk.Path == "" { + return nil, fmt.Errorf("missing Chunk (%d/%d): %s", i, len(blist.Latest), name) + } + chunks = append(chunks, chunk) + if i > 0 { + chunk.Start = chunkMap[blist.Latest[i-1]].End + 1 + chunk.End += chunk.Start + } + } + return chunks, nil +} + func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error { // read all db artifacts by name artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ @@ -230,7 +278,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st rawChecksum := hash.Sum(nil) actualChecksum := hex.EncodeToString(rawChecksum) if !strings.HasSuffix(checksum, actualChecksum) { - return fmt.Errorf("update artifact error checksum is invalid") + return fmt.Errorf("update artifact error checksum is invalid %v vs %v", checksum, actualChecksum) } } diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 7b2f9c4360..677e89da2f 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -24,8 +24,15 @@ package actions // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block // 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock -// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now +// 1.4. BlockList xml payload to Blobstorage (unauthenticated request) +// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList +// Request +// +// +// blockId1 +// blockId2 +// // 1.5. FinalizeArtifact // Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact // Request @@ -82,6 +89,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" + "encoding/xml" "fmt" "io" "net/http" @@ -153,31 +161,34 @@ func ArtifactsV4Routes(prefix string) *web.Route { return m } -func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte { +func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID, artifactID int64) []byte { mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endp)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) mac.Write([]byte(fmt.Sprint(taskID))) + mac.Write([]byte(fmt.Sprint(artifactID))) return mac.Sum(nil) } -func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string { +func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID, artifactID int64) string { expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") + - "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "&artifactID=" + fmt.Sprint(artifactID) return uploadURL } func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) { rawTaskID := ctx.Req.URL.Query().Get("taskID") + rawArtifactID := ctx.Req.URL.Query().Get("artifactID") sig := ctx.Req.URL.Query().Get("sig") expires := ctx.Req.URL.Query().Get("expires") artifactName := ctx.Req.URL.Query().Get("artifactName") dsig, _ := base64.URLEncoding.DecodeString(sig) taskID, _ := strconv.ParseInt(rawTaskID, 10, 64) + artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) - expecedsig := r.buildSignature(endp, expires, artifactName, taskID) + expecedsig := r.buildSignature(endp, expires, artifactName, taskID, artifactID) if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") ctx.Error(http.StatusUnauthorized, "Error unauthorized") @@ -272,6 +283,8 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { return } artifact.ContentEncoding = ArtifactV4ContentEncoding + artifact.FileSize = 0 + artifact.FileCompressedSize = 0 if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") @@ -280,7 +293,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { respData := CreateArtifactResponse{ Ok: true, - SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID), + SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID), } r.sendProtbufBody(ctx, &respData) } @@ -306,38 +319,77 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { comp := ctx.Req.URL.Query().Get("comp") switch comp { case "block", "appendBlock": - // get artifact by name - artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) - if err != nil { - log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") - return - } + blockid := ctx.Req.URL.Query().Get("blockid") + if blockid == "" { + // get artifact by name + artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) + if err != nil { + log.Error("Error artifact not found: %v", err) + ctx.Error(http.StatusNotFound, "Error artifact not found") + return + } - if comp == "block" { - artifact.FileSize = 0 - artifact.FileCompressedSize = 0 + _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } + artifact.FileCompressedSize += ctx.Req.ContentLength + artifact.FileSize += ctx.Req.ContentLength + if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { + log.Error("Error UpdateArtifactByID: %v", err) + ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + return + } + } else { + _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/block-%d-%d-%s", task.Job.RunID, task.Job.RunID, ctx.Req.ContentLength, base64.URLEncoding.EncodeToString([]byte(blockid))), ctx.Req.Body, -1) + if err != nil { + log.Error("Error runner api getting task: task is not running") + ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + return + } } - - _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) + ctx.JSON(http.StatusCreated, "appended") + case "blocklist": + rawArtifactID := ctx.Req.URL.Query().Get("artifactID") + artifactID, _ := strconv.ParseInt(rawArtifactID, 10, 64) + _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/%d-%d-blocklist", task.Job.RunID, task.Job.RunID, artifactID), ctx.Req.Body, -1) if err != nil { log.Error("Error runner api getting task: task is not running") ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } - artifact.FileCompressedSize += ctx.Req.ContentLength - artifact.FileSize += ctx.Req.ContentLength - if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { - log.Error("Error UpdateArtifactByID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") - return - } - ctx.JSON(http.StatusCreated, "appended") - case "blocklist": ctx.JSON(http.StatusCreated, "created") } } +type BlockList struct { + Latest []string `xml:"Latest"` +} + +type Latest struct { + Value string `xml:",chardata"` +} + +func (r *artifactV4Routes) readBlockList(runID, artifactID int64) (*BlockList, error) { + blockListName := fmt.Sprintf("tmpv4%d/%d-%d-blocklist", runID, runID, artifactID) + s, err := r.fs.Open(blockListName) + if err != nil { + return nil, err + } + + xdec := xml.NewDecoder(s) + blockList := &BlockList{} + err = xdec.Decode(blockList) + + delerr := r.fs.Delete(blockListName) + if delerr != nil { + log.Warn("Failed to delete blockList %s: %v", blockListName, delerr) + } + return blockList, err +} + func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { var req FinalizeArtifactRequest @@ -356,18 +408,34 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { ctx.Error(http.StatusNotFound, "Error artifact not found") return } - chunkMap, err := listChunksByRunID(r.fs, runID) + + var chunks []*chunkFileItem + blockList, err := r.readBlockList(runID, artifact.ID) if err != nil { - log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") - return - } - chunks, ok := chunkMap[artifact.ID] - if !ok { - log.Error("Error merge chunks") - ctx.Error(http.StatusInternalServerError, "Error merge chunks") - return + log.Warn("Failed to read BlockList, fallback to old behavior: %v", err) + chunkMap, err := listChunksByRunID(r.fs, runID) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + chunks, ok = chunkMap[artifact.ID] + if !ok { + log.Error("Error merge chunks") + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + } else { + chunks, err = listChunksByRunIDV4(r.fs, runID, artifact.ID, blockList) + if err != nil { + log.Error("Error merge chunks: %v", err) + ctx.Error(http.StatusInternalServerError, "Error merge chunks") + return + } + artifact.FileSize = chunks[len(chunks)-1].End + 1 + artifact.FileCompressedSize = chunks[len(chunks)-1].End + 1 } + checksum := "" if req.Hash != nil { checksum = req.Hash.Value @@ -468,7 +536,7 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { } } if respData.SignedUrl == "" { - respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID) + respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID, artifact.ID) } r.sendProtbufBody(ctx, &respData) } diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index b2c25a2e70..96668b1ddf 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -7,12 +7,14 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "encoding/xml" "io" "net/http" "strings" "testing" "time" + "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/routers/api/actions" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/tests" @@ -175,6 +177,134 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) { assert.True(t, finalizeResp.Ok) } +func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifactWithPotentialHarmfulBlockID", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload urls + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile" + blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + + // upload artifact chunk + body := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)) + MakeRequest(t, req, http.StatusCreated) + + // verify that the exploit didn't work + _, err = storage.Actions.Stat("myfile") + assert.Error(t, err) + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + "/../myfile", + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(body)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithPotentialHarmfulBlockID", + Size: 1024, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + +func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token, err := actions_service.CreateAuthorizationToken(48, 792, 193) + assert.NoError(t, err) + + // acquire artifact upload url + req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ + Version: 4, + Name: "artifactWithChunksOutOfOrder", + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + var uploadResp actions.CreateArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &uploadResp) + assert.True(t, uploadResp.Ok) + assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact") + + // get upload urls + idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/") + block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1" + block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2" + blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist" + + // upload artifact chunks + bodyb := strings.Repeat("B", 1024) + req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb)) + MakeRequest(t, req, http.StatusCreated) + + bodya := strings.Repeat("A", 1024) + req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya)) + MakeRequest(t, req, http.StatusCreated) + + // upload artifact blockList + blockList := &actions.BlockList{ + Latest: []string{ + "block1", + "block2", + }, + } + rawBlockList, err := xml.Marshal(blockList) + assert.NoError(t, err) + req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) + MakeRequest(t, req, http.StatusCreated) + + t.Logf("Create artifact confirm") + + sha := sha256.Sum256([]byte(bodya + bodyb)) + + // confirm artifact upload + req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{ + Name: "artifactWithChunksOutOfOrder", + Size: 2048, + Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])), + WorkflowRunBackendId: "792", + WorkflowJobRunBackendId: "193", + })). + AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var finalizeResp actions.FinalizeArtifactResponse + protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp) + assert.True(t, finalizeResp.Ok) +} + func TestActionsArtifactV4DownloadSingle(t *testing.T) { defer tests.PrepareTestEnv(t)() From b2483b2ae031bbfd8829595fbff19cd323d80e7f Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Sun, 29 Sep 2024 09:58:47 +0200 Subject: [PATCH 18/89] Fix artifact v4 upload above 8MB (#31664) (fix lint errors) --- tests/integration/api_actions_artifact_v4_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go index 96668b1ddf..f55250f6c1 100644 --- a/tests/integration/api_actions_artifact_v4_test.go +++ b/tests/integration/api_actions_artifact_v4_test.go @@ -181,7 +181,7 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing defer tests.PrepareTestEnv(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) - assert.NoError(t, err) + require.NoError(t, err) // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ @@ -208,7 +208,7 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing // verify that the exploit didn't work _, err = storage.Actions.Stat("myfile") - assert.Error(t, err) + require.Error(t, err) // upload artifact blockList blockList := &actions.BlockList{ @@ -217,7 +217,7 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing }, } rawBlockList, err := xml.Marshal(blockList) - assert.NoError(t, err) + require.NoError(t, err) req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) @@ -244,7 +244,7 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { defer tests.PrepareTestEnv(t)() token, err := actions_service.CreateAuthorizationToken(48, 792, 193) - assert.NoError(t, err) + require.NoError(t, err) // acquire artifact upload url req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{ @@ -282,7 +282,7 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) { }, } rawBlockList, err := xml.Marshal(blockList) - assert.NoError(t, err) + require.NoError(t, err) req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList)) MakeRequest(t, req, http.StatusCreated) From b28a070a528b2df5472018d296e4e9d9e128b3bc Mon Sep 17 00:00:00 2001 From: cloudchamb3r Date: Tue, 24 Sep 2024 02:09:57 +0900 Subject: [PATCH 19/89] Fix Bug in Issue/pulls list (#32081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #32080 ## After ### for opened issues Screenshot 2024-09-19 at 6 29 31 PM ### for closed issues Screenshot 2024-09-19 at 6 29 37 PM ### for all issues Screenshot 2024-09-20 at 12 07 12 PM (cherry picked from commit e1f0598c8f5af5ac95f5e13b74fbab99506762db) --- routers/web/repo/issue.go | 1 + templates/repo/issue/filter_actions.tmpl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 01fd1e2725..5d13ccc97c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -476,6 +476,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["PosterID"] = posterID ctx.Data["IsFuzzy"] = isFuzzy ctx.Data["Keyword"] = keyword + ctx.Data["IsShowClosed"] = isShowClosed switch { case isShowClosed.Value(): ctx.Data["State"] = "closed" diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index a341448bcc..58b1ef8ecd 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -1,9 +1,9 @@