From 4500757acd3fb3c5f4fea94ee71247eedc4013d6 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Mon, 9 Dec 2024 18:59:11 +0300 Subject: [PATCH 0001/1157] feat: add synchronization for SSH keys with OpenID Connect Co-authored-by: Kirill Kolmykov --- routers/web/admin/auths.go | 1 + services/auth/source/oauth2/providers_base.go | 4 + .../auth/source/oauth2/providers_openid.go | 4 + services/auth/source/oauth2/source.go | 26 ++++-- services/auth/source/oauth2/source_sync.go | 92 +++++++++++++++++++ services/forms/auth_form.go | 1 + templates/admin/auth/edit.tmpl | 25 +++-- templates/admin/auth/source/oauth.tmpl | 26 ++++-- web_src/js/features/admin/common.js | 6 +- 9 files changed, 158 insertions(+), 27 deletions(-) diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 799b7e8a84..dcdc8e6a2a 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -197,6 +197,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { CustomURLMapping: customURLMapping, IconURL: form.Oauth2IconURL, Scopes: scopes, + AttributeSSHPublicKey: form.Oauth2AttributeSSHPublicKey, RequiredClaimName: form.Oauth2RequiredClaimName, RequiredClaimValue: form.Oauth2RequiredClaimValue, SkipLocalTwoFA: form.SkipLocalTwoFA, diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go index 9d4ab106e5..63318b84ef 100644 --- a/services/auth/source/oauth2/providers_base.go +++ b/services/auth/source/oauth2/providers_base.go @@ -48,4 +48,8 @@ func (b *BaseProvider) CustomURLSettings() *CustomURLSettings { return nil } +func (b *BaseProvider) CanProvideSSHKeys() bool { + return false +} + var _ Provider = &BaseProvider{} diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go index 285876d5ac..f606581271 100644 --- a/services/auth/source/oauth2/providers_openid.go +++ b/services/auth/source/oauth2/providers_openid.go @@ -51,6 +51,10 @@ func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings { return nil } +func (o *OpenIDProvider) CanProvideSSHKeys() bool { + return true +} + var _ GothProvider = &OpenIDProvider{} func init() { diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go index 3454c9ad55..fe4823e778 100644 --- a/services/auth/source/oauth2/source.go +++ b/services/auth/source/oauth2/source.go @@ -4,6 +4,8 @@ package oauth2 import ( + "strings" + "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/json" ) @@ -17,15 +19,16 @@ type Source struct { CustomURLMapping *CustomURLMapping IconURL string - Scopes []string - RequiredClaimName string - RequiredClaimValue string - GroupClaimName string - AdminGroup string - GroupTeamMap string - GroupTeamMapRemoval bool - RestrictedGroup string - SkipLocalTwoFA bool `json:",omitempty"` + Scopes []string + AttributeSSHPublicKey string + RequiredClaimName string + RequiredClaimValue string + GroupClaimName string + AdminGroup string + GroupTeamMap string + GroupTeamMapRemoval bool + RestrictedGroup string + SkipLocalTwoFA bool `json:",omitempty"` // reference to the authSource authSource *auth.Source @@ -41,6 +44,11 @@ func (source *Source) ToDB() ([]byte, error) { return json.Marshal(source) } +// ProvidesSSHKeys returns if this source provides SSH Keys +func (source *Source) ProvidesSSHKeys() bool { + return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 +} + // SetAuthSource sets the related AuthSource func (source *Source) SetAuthSource(authSource *auth.Source) { source.authSource = authSource diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 5e30313c8f..667c0957fc 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -5,15 +5,20 @@ package oauth2 import ( "context" + "fmt" "time" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" "github.com/markbates/goth" + "github.com/markbates/goth/providers/openidConnect" "golang.org/x/oauth2" + + asymkey_model "code.gitea.io/gitea/models/asymkey" ) // Sync causes this OAuth2 source to synchronize its users with the db. @@ -108,7 +113,94 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us u.RefreshToken = token.RefreshToken } + needUserFetch := source.ProvidesSSHKeys() + + if needUserFetch { + fetchedUser, err := fetchUser(provider, token) + if err != nil { + log.Error("fetchUser: %v", err) + } else { + err = updateSSHKeys(ctx, source, user, &fetchedUser) + if err != nil { + log.Error("updateSshKeys: %v", err) + } + } + } + err = user_model.UpdateExternalUserByExternalID(ctx, u) return err } + +func fetchUser(provider goth.Provider, token *oauth2.Token) (goth.User, error) { + state, err := util.CryptoRandomString(40) + if err != nil { + return goth.User{}, err + } + + session, err := provider.BeginAuth(state) + if err != nil { + return goth.User{}, err + } + + if s, ok := session.(*openidConnect.Session); ok { + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + s.IDToken = token.Extra("id_token").(string) + } + + gothUser, err := provider.FetchUser(session) + if err != nil { + return goth.User{}, err + } + + return gothUser, nil +} + +func updateSSHKeys( + ctx context.Context, + source *Source, + user *user_model.User, + fetchedUser *goth.User, +) error { + if source.ProvidesSSHKeys() { + sshKeys, err := getSSHKeys(source, fetchedUser) + if err != nil { + return err + } + + if asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sshKeys) { + err = asymkey_model.RewriteAllPublicKeys(ctx) + if err != nil { + return err + } + } + } + + return nil +} + +func getSSHKeys(source *Source, gothUser *goth.User) ([]string, error) { + key := source.AttributeSSHPublicKey + value, exists := gothUser.RawData[key] + if !exists { + return nil, fmt.Errorf("attribute '%s' not found in user data", key) + } + + rawSlice, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value) + } + + sshKeys := make([]string, 0, len(rawSlice)) + for i, v := range rawSlice { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("unexpected element type at index %d in SSH public key array, expected string but got %T", i, v) + } + sshKeys = append(sshKeys, str) + } + + return sshKeys, nil +} diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index f0da63155a..39aae51756 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -75,6 +75,7 @@ type AuthenticationForm struct { Oauth2RestrictedGroup string Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"` Oauth2GroupTeamMapRemoval bool + Oauth2AttributeSSHPublicKey string SkipLocalTwoFA bool SSPIAutoCreateUsers bool SSPIAutoActivateUsers bool diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index a8b2049f92..84fefc0484 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -326,19 +326,28 @@ - {{range .OAuth2Providers}}{{if .CustomURLSettings}} - - - - - - - {{end}}{{end}} + {{range .OAuth2Providers}} + {{if .CustomURLSettings}} + + + + + + + {{end}} + {{if .CanProvideSSHKeys}} + + {{end}} + {{end}}
+
+ + +
diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index 0560cc8256..7d0a64d269 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -63,19 +63,27 @@
- {{range .OAuth2Providers}}{{if .CustomURLSettings}} - - - - - - - {{end}}{{end}} - + {{range .OAuth2Providers}} + {{if .CustomURLSettings}} + + + + + + + {{end}} + {{if .CanProvideSSHKeys}} + + {{end}} + {{end}}
+
+ + +
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index 1a5bd6e490..a42d8261f1 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -62,7 +62,7 @@ export function initAdminCommon() { } function onOAuth2Change(applyDefaultValues) { - hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url'); + hideElem('.open_id_connect_auto_discovery_url, .oauth2_use_custom_url, .oauth2_attribute_ssh_public_key'); for (const input of document.querySelectorAll('.open_id_connect_auto_discovery_url input[required]')) { input.removeAttribute('required'); } @@ -85,6 +85,10 @@ export function initAdminCommon() { } } } + const canProvideSSHKeys = document.getElementById(`${provider}_canProvideSSHKeys`); + if (canProvideSSHKeys) { + showElem('.oauth2_attribute_ssh_public_key'); + } onOAuth2UseCustomURLChange(applyDefaultValues); } From 47fd9a421fd9d3a8cba87c1574ea3ae3a60f4cdb Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Tue, 10 Dec 2024 07:41:04 +0000 Subject: [PATCH 0002/1157] style: run make fmt --- services/auth/source/oauth2/source_sync.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 667c0957fc..b0b08d1a50 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" @@ -17,8 +18,6 @@ import ( "github.com/markbates/goth" "github.com/markbates/goth/providers/openidConnect" "golang.org/x/oauth2" - - asymkey_model "code.gitea.io/gitea/models/asymkey" ) // Sync causes this OAuth2 source to synchronize its users with the db. @@ -188,7 +187,7 @@ func getSSHKeys(source *Source, gothUser *goth.User) ([]string, error) { return nil, fmt.Errorf("attribute '%s' not found in user data", key) } - rawSlice, ok := value.([]interface{}) + rawSlice, ok := value.([]any) if !ok { return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value) } From a87e0d0e46724e7c5fd06bdfc4a7413bc098461c Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Tue, 10 Dec 2024 14:01:02 +0000 Subject: [PATCH 0003/1157] fix: do not rewrite when there are no keys --- services/auth/source/oauth2/source_sync.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index b0b08d1a50..59590a8bb3 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -169,6 +169,10 @@ func updateSSHKeys( return err } + if len(sshKeys) == 0 { + return nil + } + if asymkey_model.SynchronizePublicKeys(ctx, user, source.authSource, sshKeys) { err = asymkey_model.RewriteAllPublicKeys(ctx) if err != nil { From 7685a1e98e96a2602d7b50cd767d18e0d1d86128 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Sat, 14 Dec 2024 19:36:09 +0100 Subject: [PATCH 0004/1157] feat: Add summary card for repos and releases --- models/repo/release.go | 55 ++- models/repo/release_test.go | 24 ++ models/repo/repo.go | 5 + modules/card/card.go | 34 +- routers/web/repo/card.go | 522 ++++++++++++++++++++++++++++ routers/web/repo/issue.go | 222 ------------ routers/web/repo/release.go | 12 +- routers/web/web.go | 4 +- services/context/repo.go | 2 + templates/base/head_opengraph.tmpl | 24 +- tests/integration/opengraph_test.go | 16 +- 11 files changed, 673 insertions(+), 247 deletions(-) create mode 100644 routers/web/repo/card.go diff --git a/models/repo/release.go b/models/repo/release.go index 38e38c6572..86cb553da0 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -97,13 +97,11 @@ func init() { // LoadAttributes load repo and publisher attributes for a release func (r *Release) LoadAttributes(ctx context.Context) error { - var err error - if r.Repo == nil { - r.Repo, err = GetRepositoryByID(ctx, r.RepoID) - if err != nil { - return err - } + err := r.LoadRepo(ctx) + if err != nil { + return err } + if r.Publisher == nil { r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) if err != nil { @@ -123,6 +121,18 @@ func (r *Release) LoadAttributes(ctx context.Context) error { return GetReleaseAttachments(ctx, r) } +// LoadRepo load repo attribute for release +func (r *Release) LoadRepo(ctx context.Context) error { + if r.Repo != nil { + return nil + } + + var err error + r.Repo, err = GetRepositoryByID(ctx, r.RepoID) + + return err +} + // LoadArchiveDownloadCount loads the download count for the source archives func (r *Release) LoadArchiveDownloadCount(ctx context.Context) error { var err error @@ -130,6 +140,25 @@ func (r *Release) LoadArchiveDownloadCount(ctx context.Context) error { return err } +// GetTotalDownloadCount returns the summary of all dowlaod count of files attached to the release +func (r *Release) GetTotalDownloadCount(ctx context.Context) (int64, error) { + var archiveCount int64 + if !r.HideArchiveLinks { + _, err := db.GetEngine(ctx).SQL("SELECT SUM(count) FROM repo_archive_download_count WHERE release_id = ?", r.ID).Get(&archiveCount) + if err != nil { + return 0, err + } + } + + var attachmentCount int64 + _, err := db.GetEngine(ctx).SQL("SELECT SUM(download_count) FROM attachment WHERE release_id = ?", r.ID).Get(&attachmentCount) + if err != nil { + return 0, err + } + + return archiveCount + attachmentCount, nil +} + // APIURL the api url for a release. release must have attributes loaded func (r *Release) APIURL() string { return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10) @@ -160,6 +189,20 @@ func (r *Release) Link() string { return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) } +// SummaryCardURL returns the absolute URL to an image providing a summary of the release +func (r *Release) SummaryCardURL() string { + return fmt.Sprintf("%s/releases/summary-card/%d", r.Repo.HTMLURL(), r.ID) +} + +// DisplayName retruns the name of the release +func (r *Release) DisplayName() string { + if r.IsTag && r.Title == "" { + return r.TagName + } + + return r.Title +} + // IsReleaseExist returns true if release with given tag name already exists. func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, error) { if len(tagName) == 0 { diff --git a/models/repo/release_test.go b/models/repo/release_test.go index 4e61a2805d..c8e489b9a7 100644 --- a/models/repo/release_test.go +++ b/models/repo/release_test.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,3 +26,26 @@ func TestMigrate_InsertReleases(t *testing.T) { err := InsertReleases(db.DefaultContext, r) require.NoError(t, err) } + +func TestReleaseLoadRepo(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + release := unittest.AssertExistsAndLoadBean(t, &Release{ID: 1}) + assert.Nil(t, release.Repo) + + require.NoError(t, release.LoadRepo(db.DefaultContext)) + + assert.Equal(t, int64(1), release.Repo.ID) +} + +func TestReleaseDisplayName(t *testing.T) { + release := Release{TagName: "TagName"} + + assert.Empty(t, release.DisplayName()) + + release.IsTag = true + assert.Equal(t, "TagName", release.DisplayName()) + + release.Title = "Title" + assert.Equal(t, "Title", release.DisplayName()) +} diff --git a/models/repo/repo.go b/models/repo/repo.go index cd6be48b90..bdf0de2f85 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -327,6 +327,11 @@ func (repo *Repository) HTMLURL() string { return setting.AppURL + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) } +// SummaryCardURL returns the absolute URL to an image providing a summary of the repo +func (repo *Repository) SummaryCardURL() string { + return fmt.Sprintf("%s/-/summary-card", repo.HTMLURL()) +} + // CommitLink make link to by commit full ID // note: won't check whether it's an right id func (repo *Repository) CommitLink(commitID string) (result string) { diff --git a/modules/card/card.go b/modules/card/card.go index bb160d7ea3..dc3fa7e860 100644 --- a/modules/card/card.go +++ b/modules/card/card.go @@ -5,6 +5,7 @@ package card import ( "bytes" + "fmt" "image" "image/color" "io" @@ -35,12 +36,19 @@ type Card struct { Img *image.RGBA Font *truetype.Font Margin int + Width int + Height int } var fontCache = sync.OnceValues(func() (*truetype.Font, error) { return truetype.Parse(goregular.TTF) }) +// DefaultSize returns the default sie for a card +func DefaultSize() (int, int) { + return 1200, 600 +} + // NewCard creates a new card with the given dimensions in pixels func NewCard(width, height int) (*Card, error) { img := image.NewRGBA(image.Rect(0, 0, width, height)) @@ -55,6 +63,8 @@ func NewCard(width, height int) (*Card, error) { Img: img, Font: font, Margin: 0, + Width: width, + Height: height, }, nil } @@ -67,14 +77,14 @@ func (c *Card) Split(vertical bool, percentage int) (*Card, *Card) { mid := (bounds.Dx() * percentage / 100) + bounds.Min.X subleft := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, mid, bounds.Max.Y)).(*image.RGBA) subright := c.Img.SubImage(image.Rect(mid, bounds.Min.Y, bounds.Max.X, bounds.Max.Y)).(*image.RGBA) - return &Card{Img: subleft, Font: c.Font}, - &Card{Img: subright, Font: c.Font} + return &Card{Img: subleft, Font: c.Font, Width: subleft.Bounds().Dx(), Height: subleft.Bounds().Dy()}, + &Card{Img: subright, Font: c.Font, Width: subright.Bounds().Dx(), Height: subright.Bounds().Dy()} } mid := (bounds.Dy() * percentage / 100) + bounds.Min.Y subtop := c.Img.SubImage(image.Rect(bounds.Min.X, bounds.Min.Y, bounds.Max.X, mid)).(*image.RGBA) subbottom := c.Img.SubImage(image.Rect(bounds.Min.X, mid, bounds.Max.X, bounds.Max.Y)).(*image.RGBA) - return &Card{Img: subtop, Font: c.Font}, - &Card{Img: subbottom, Font: c.Font} + return &Card{Img: subtop, Font: c.Font, Width: subtop.Bounds().Dx(), Height: subtop.Bounds().Dy()}, + &Card{Img: subbottom, Font: c.Font, Width: subbottom.Bounds().Dx(), Height: subbottom.Bounds().Dy()} } // SetMargin sets the margins for the card @@ -244,9 +254,14 @@ func (c *Card) fetchExternalImage(url string) (image.Image, bool) { }, } + // Go expects a absolute URL, so we must change a relative to an absolute one + if !strings.Contains(url, "://") { + url = fmt.Sprintf("%s%s", setting.AppURL, strings.TrimPrefix(url, "/")) + } + resp, err := client.Get(url) if err != nil { - log.Warn("error when fetching external image from %s: %w", url, err) + log.Warn("error when fetching external image from %s: %v", url, err) return nil, false } defer resp.Body.Close() @@ -321,3 +336,12 @@ func (c *Card) DrawExternalImage(url string) { } c.DrawImage(image) } + +// DrawRect draws a rect with the given color +func (c *Card) DrawRect(startX, startY, endX, endY int, color color.Color) { + for x := startX; x <= endX; x++ { + for y := startY; y <= endY; y++ { + c.Img.Set(x, y, color) + } + } +} diff --git a/routers/web/repo/card.go b/routers/web/repo/card.go new file mode 100644 index 0000000000..3564d6486c --- /dev/null +++ b/routers/web/repo/card.go @@ -0,0 +1,522 @@ +package repo + +import ( + "bytes" + "encoding/hex" + "fmt" + "image" + "image/color" + "image/png" + "net/http" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + issue_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/card" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/services/context" +) + +// drawUser draws a user avator in a summary card +func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error { + if user.UseCustomAvatar { + posterAvatarPath := user.CustomAvatarRelativePath() + if posterAvatarPath != "" { + userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath()) + if err != nil { + return err + } + userAvatarImage, _, err := image.Decode(userAvatarFile) + if err != nil { + return err + } + card.DrawImage(userAvatarImage) + } + } else { + posterAvatarLink := user.AvatarLinkWithSize(ctx, 256) + card.DrawExternalImage(posterAvatarLink) + } + return nil +} + +// drawRepoIcon draws the repo icon in a summary card +func drawRepoIcon(ctx *context.Context, card *card.Card, repo *repo_model.Repository) error { + repoAvatarPath := repo.CustomAvatarRelativePath() + if repoAvatarPath != "" { + repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath) + if err != nil { + return err + } + repoAvatarImage, _, err := image.Decode(repoAvatarFile) + if err != nil { + return err + } + card.DrawImage(repoAvatarImage) + return nil + } else { + // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon + err := repo.LoadOwner(ctx) + if err != nil { + return err + } + if repo.Owner != nil { + err = drawUser(ctx, card, repo.Owner) + if err != nil { + return err + } + } + + return nil + } +} + +// hexToColor converts a hex color to a go color +func hexToColor(colorStr string) (*color.RGBA, error) { + colorStr = strings.TrimLeft(colorStr, "#") + + b, err := hex.DecodeString(colorStr) + if err != nil { + return nil, err + } + + if len(b) < 3 { + return nil, fmt.Errorf("expected at least 3 bytes from DecodeString, got %d", len(b)) + } + + color := color.RGBA{b[0], b[1], b[2], 255} + + return &color, nil +} + +func drawLanguagesCard(ctx *context.Context, card *card.Card) error { + languageList, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) + if err != nil { + return err + } + if len(languageList) == 0 { + card.DrawRect(0, 0, card.Width, card.Height, color.White) + return nil + } + + currentX := 0 + var langColor *color.RGBA + + for _, lang := range languageList { + langColor, err = hexToColor(lang.Color) + if err != nil { + return err + } + + langWidth := float32(card.Width) * (lang.Percentage / 100) + card.DrawRect(currentX, 0, currentX+int(langWidth), card.Width, langColor) + currentX += int(langWidth) + } + + if currentX < card.Width { + card.DrawRect(currentX, 0, card.Width, card.Height, langColor) + } + + return nil +} + +func drawRepoSummaryCard(ctx *context.Context, repo *repo_model.Repository) (*card.Card, error) { + width, height := card.DefaultSize() + mainCard, err := card.NewCard(width, height) + if err != nil { + return nil, err + } + + contentCard, languageBarCard := mainCard.Split(false, 90) + + contentCard.SetMargin(60) + topSection, bottomSection := contentCard.Split(false, 75) + issueSummary, issueIcon := topSection.Split(true, 80) + repoInfo, issueDescription := issueSummary.Split(false, 30) + + repoInfo.SetMargin(10) + _, err = repoInfo.DrawText(repo.FullName(), color.Black, 56, card.Top, card.Left) + if err != nil { + return nil, err + } + + issueDescription.SetMargin(10) + _, err = issueDescription.DrawText(repo.Description, color.Gray{128}, 36, card.Top, card.Left) + if err != nil { + return nil, err + } + + issueIcon.SetMargin(10) + err = drawRepoIcon(ctx, issueIcon, repo) + if err != nil { + return nil, err + } + + topCountCard, bottomCountCard := bottomSection.Split(false, 50) + + releaseCount, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ + // only show draft releases for users who can write, read-only users shouldn't see draft releases. + IncludeDrafts: ctx.Repo.CanWrite(unit_model.TypeReleases), + RepoID: ctx.Repo.Repository.ID, + }) + if err != nil { + return nil, err + } + + starsText := ctx.Locale.TrN( + repo.NumStars, + "explore.stars_one", + "explore.stars_few", + repo.NumStars, + ) + forksText := ctx.Locale.TrN( + repo.NumForks, + "explore.forks_one", + "explore.forks_few", + repo.NumForks, + ) + releasesText := ctx.Locale.TrN( + releaseCount, + "repo.activity.title.releases_1", + "repo.activity.title.releases_n", + releaseCount, + ) + + topCountText := fmt.Sprintf("%s • %s • %s", starsText, forksText, releasesText) + + topCountCard.SetMargin(10) + _, err = topCountCard.DrawText(topCountText, color.Gray{128}, 36, card.Top, card.Left) + if err != nil { + return nil, err + } + + issuesText := ctx.Locale.TrN( + repo.NumOpenIssues, + "repo.activity.title.issues_1", + "repo.activity.title.issues_n", + repo.NumOpenIssues, + ) + pullRequestsText := ctx.Locale.TrN( + repo.NumOpenPulls, + "repo.activity.title.prs_1", + "repo.activity.title.prs_n", + repo.NumOpenPulls, + ) + + bottomCountText := fmt.Sprintf("%s • %s", issuesText, pullRequestsText) + + bottomCountCard.SetMargin(10) + _, err = bottomCountCard.DrawText(bottomCountText, color.Gray{128}, 36, card.Top, card.Left) + if err != nil { + return nil, err + } + + err = drawLanguagesCard(ctx, languageBarCard) + if err != nil { + return nil, err + } + + return mainCard, nil +} + +func drawIssueSummaryCard(ctx *context.Context, issue *issue_model.Issue) (*card.Card, error) { + width, height := issue.SummaryCardSize() + mainCard, err := card.NewCard(width, height) + if err != nil { + return nil, err + } + + mainCard.SetMargin(60) + topSection, bottomSection := mainCard.Split(false, 75) + issueSummary, issueIcon := topSection.Split(true, 80) + repoInfo, issueDescription := issueSummary.Split(false, 15) + + repoInfo.SetMargin(10) + _, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left) + if err != nil { + return nil, err + } + + issueDescription.SetMargin(10) + _, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left) + if err != nil { + return nil, err + } + + issueIcon.SetMargin(10) + err = drawRepoIcon(ctx, issueIcon, issue.Repo) + if err != nil { + return nil, err + } + + issueStats, issueAttribution := bottomSection.Split(false, 50) + + var state string + if issue.IsPull && issue.PullRequest.HasMerged { + if issue.PullRequest.Status == 3 { + state = ctx.Locale.TrString("repo.pulls.manually_merged") + } else { + state = ctx.Locale.TrString("repo.pulls.merged") + } + } else if issue.IsClosed { + state = ctx.Locale.TrString("repo.issues.closed_title") + } else if issue.IsPull { + if issue.PullRequest.IsWorkInProgress(ctx) { + state = ctx.Locale.TrString("repo.issues.draft_title") + } else { + state = ctx.Locale.TrString("repo.issues.open_title") + } + } else { + state = ctx.Locale.TrString("repo.issues.open_title") + } + state = strings.ToLower(state) + + issueStats.SetMargin(10) + if issue.IsPull { + reviews := map[int64]bool{} + for _, comment := range issue.Comments { + if comment.Review != nil { + reviews[comment.Review.ID] = true + } + } + _, err = issueStats.DrawText( + fmt.Sprintf("%s, %s, %s", + ctx.Locale.TrN( + issue.NumComments, + "repo.issues.num_comments_1", + "repo.issues.num_comments", + issue.NumComments, + ), + ctx.Locale.TrN( + len(reviews), + "repo.issues.num_reviews_one", + "repo.issues.num_reviews_few", + len(reviews), + ), + state, + ), + color.Gray{128}, 36, card.Top, card.Left) + } else { + _, err = issueStats.DrawText( + fmt.Sprintf("%s, %s", + ctx.Locale.TrN( + issue.NumComments, + "repo.issues.num_comments_1", + "repo.issues.num_comments", + issue.NumComments, + ), + state, + ), + color.Gray{128}, 36, card.Top, card.Left) + } + if err != nil { + return nil, err + } + + issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8) + issueAttributionText.SetMargin(5) + _, err = issueAttributionText.DrawText( + fmt.Sprintf( + "%s - %s", + issue.Poster.Name, + issue.Created.AsTime().Format(time.DateOnly), + ), + color.Gray{128}, 36, card.Middle, card.Left) + if err != nil { + return nil, err + } + err = drawUser(ctx, issueAttributionIcon, issue.Poster) + if err != nil { + return nil, err + } + + return mainCard, nil +} + +func drawReleaseSummaryCard(ctx *context.Context, release *repo_model.Release) (*card.Card, error) { + width, height := card.DefaultSize() + mainCard, err := card.NewCard(width, height) + if err != nil { + return nil, err + } + + mainCard.SetMargin(60) + topSection, bottomSection := mainCard.Split(false, 75) + releaseSummary, repoIcon := topSection.Split(true, 80) + repoInfo, releaseDescription := releaseSummary.Split(false, 15) + + repoInfo.SetMargin(10) + _, err = repoInfo.DrawText(release.Repo.FullName(), color.Gray{128}, 36, card.Top, card.Left) + if err != nil { + return nil, err + } + + releaseDescription.SetMargin(10) + _, err = releaseDescription.DrawText(release.DisplayName(), color.Black, 56, card.Top, card.Left) + if err != nil { + return nil, err + } + + repoIcon.SetMargin(10) + err = drawRepoIcon(ctx, repoIcon, release.Repo) + if err != nil { + return nil, err + } + + downloadCountCard, releaseDateCard := bottomSection.Split(true, 75) + + downloadCount, err := release.GetTotalDownloadCount(ctx) + if err != nil { + return nil, err + } + + downloadCountText := ctx.Locale.TrN( + strconv.FormatInt(downloadCount, 10), + "repo.release.download_count_one", + "repo.release.download_count_few", + strconv.FormatInt(downloadCount, 10), + ) + + _, err = downloadCountCard.DrawText(string(downloadCountText), color.Gray{128}, 36, card.Bottom, card.Left) + if err != nil { + return nil, err + } + + _, err = releaseDateCard.DrawText(release.CreatedUnix.AsTime().Format(time.DateOnly), color.Gray{128}, 36, card.Bottom, card.Left) + if err != nil { + return nil, err + } + + return mainCard, nil +} + +// checkCardCache checks if a card in cahce and serves it +func checkCardCache(ctx *context.Context, cacheKey string) bool { + cache := cache.GetCache() + pngData, ok := cache.Get(cacheKey).([]byte) + if ok && pngData != nil && len(pngData) > 0 { + ctx.Resp.Header().Set("Content-Type", "image/png") + ctx.Resp.WriteHeader(http.StatusOK) + _, err := ctx.Resp.Write(pngData) + if err != nil { + ctx.ServerError("GetSummaryCard", err) + } + return true + } + + return false +} + +// serveCard server a Crad to the user adds it to the cache +func serveCard(ctx *context.Context, card *card.Card, cacheKey string) { + cache := cache.GetCache() + + // Encode image, store in cache + var imageBuffer bytes.Buffer + err := png.Encode(&imageBuffer, card.Img) + if err != nil { + ctx.ServerError("GetSummaryCard", err) + return + } + imageBytes := imageBuffer.Bytes() + err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds()) + if err != nil { + // don't abort serving the image if we just had a cache storage failure + log.Warn("failed to cache issue summary card: %v", err) + } + + // Finish the uncached image response + ctx.Resp.Header().Set("Content-Type", "image/png") + ctx.Resp.WriteHeader(http.StatusOK) + _, err = ctx.Resp.Write(imageBytes) + if err != nil { + ctx.ServerError("GetSummaryCard", err) + return + } +} + +func DrawRepoSummaryCard(ctx *context.Context) { + cacheKey := fmt.Sprintf("summary_card:repo:%s:%d", ctx.Locale.Language(), ctx.Repo.Repository.ID) + + if checkCardCache(ctx, cacheKey) { + return + } + + card, err := drawRepoSummaryCard(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("drawRepoSummaryCar", err) + return + } + + serveCard(ctx, card, cacheKey) +} + +func DrawIssueSummaryCard(ctx *context.Context) { + issue, err := issue_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if issue_model.IsErrIssueNotExist(err) { + ctx.Error(http.StatusNotFound) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) + } + return + } + + if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { + ctx.Error(http.StatusNotFound) + return + } + + cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID) + + if checkCardCache(ctx, cacheKey) { + return + } + + card, err := drawIssueSummaryCard(ctx, issue) + if err != nil { + ctx.ServerError("drawIssueSummaryCar", err) + return + } + + serveCard(ctx, card, cacheKey) +} + +func DrawReleaseSummaryCard(ctx *context.Context) { + release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":releaseID")) + if err != nil { + if repo_model.IsErrReleaseNotExist(err) { + ctx.NotFound("", nil) + } else { + ctx.ServerError("GetReleaseForRepoByID", err) + } + return + } + + err = release.LoadRepo(ctx) + if err != nil { + ctx.ServerError("LoadRepo", err) + return + } + + cacheKey := fmt.Sprintf("summary_card:release:%s:%d", ctx.Locale.Language(), release.ID) + + if checkCardCache(ctx, cacheKey) { + return + } + + card, err := drawReleaseSummaryCard(ctx, release) + if err != nil { + ctx.ServerError("drawRepoSummaryCar", err) + return + } + + serveCard(ctx, card, cacheKey) +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 78fb5e6c01..b86749ec69 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -10,9 +10,6 @@ import ( "errors" "fmt" "html/template" - "image" - "image/color" - "image/png" "math/big" "net/http" "net/url" @@ -34,8 +31,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/cache" - "code.gitea.io/gitea/modules/card" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" @@ -47,7 +42,6 @@ import ( "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" @@ -2218,222 +2212,6 @@ func GetIssueInfo(ctx *context.Context) { ctx.JSON(http.StatusOK, convert.ToIssue(ctx, ctx.Doer, issue)) } -// GetSummaryCard get an issue of a repository -func GetSummaryCard(ctx *context.Context) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) - if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.Error(http.StatusNotFound) - } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) - } - return - } - - if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { - ctx.Error(http.StatusNotFound) - return - } - - cache := cache.GetCache() - cacheKey := fmt.Sprintf("summary_card:issue:%s:%d", ctx.Locale.Language(), issue.ID) - pngData, ok := cache.Get(cacheKey).([]byte) - if ok && pngData != nil && len(pngData) > 0 { - ctx.Resp.Header().Set("Content-Type", "image/png") - ctx.Resp.WriteHeader(http.StatusOK) - _, err = ctx.Resp.Write(pngData) - if err != nil { - ctx.ServerError("GetSummaryCard", err) - } - return - } - - card, err := drawSummaryCard(ctx, issue) - if err != nil { - ctx.ServerError("GetSummaryCard", err) - return - } - - // Encode image, store in cache - var imageBuffer bytes.Buffer - err = png.Encode(&imageBuffer, card.Img) - if err != nil { - ctx.ServerError("GetSummaryCard", err) - return - } - imageBytes := imageBuffer.Bytes() - err = cache.Put(cacheKey, imageBytes, setting.CacheService.TTLSeconds()) - if err != nil { - // don't abort serving the image if we just had a cache storage failure - log.Warn("failed to cache issue summary card: %v", err) - } - - // Finish the uncached image response - ctx.Resp.Header().Set("Content-Type", "image/png") - ctx.Resp.WriteHeader(http.StatusOK) - _, err = ctx.Resp.Write(imageBytes) - if err != nil { - ctx.ServerError("GetSummaryCard", err) - return - } -} - -func drawSummaryCard(ctx *context.Context, issue *issues_model.Issue) (*card.Card, error) { - width, height := issue.SummaryCardSize() - mainCard, err := card.NewCard(width, height) - if err != nil { - return nil, err - } - - mainCard.SetMargin(60) - topSection, bottomSection := mainCard.Split(false, 75) - issueSummary, issueIcon := topSection.Split(true, 80) - repoInfo, issueDescription := issueSummary.Split(false, 15) - - repoInfo.SetMargin(10) - _, err = repoInfo.DrawText(fmt.Sprintf("%s - #%d", issue.Repo.FullName(), issue.Index), color.Gray{128}, 36, card.Top, card.Left) - if err != nil { - return nil, err - } - - issueDescription.SetMargin(10) - _, err = issueDescription.DrawText(issue.Title, color.Black, 56, card.Top, card.Left) - if err != nil { - return nil, err - } - - issueIcon.SetMargin(10) - - repoAvatarPath := issue.Repo.CustomAvatarRelativePath() - if repoAvatarPath != "" { - repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath) - if err != nil { - return nil, err - } - repoAvatarImage, _, err := image.Decode(repoAvatarFile) - if err != nil { - return nil, err - } - issueIcon.DrawImage(repoAvatarImage) - } else { - // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon - err = issue.Repo.LoadOwner(ctx) - if err != nil { - return nil, err - } - if issue.Repo.Owner != nil { - err = drawUser(ctx, issueIcon, issue.Repo.Owner) - if err != nil { - return nil, err - } - } - } - - issueStats, issueAttribution := bottomSection.Split(false, 50) - - var state string - if issue.IsPull && issue.PullRequest.HasMerged { - if issue.PullRequest.Status == 3 { - state = ctx.Locale.TrString("repo.pulls.manually_merged") - } else { - state = ctx.Locale.TrString("repo.pulls.merged") - } - } else if issue.IsClosed { - state = ctx.Locale.TrString("repo.issues.closed_title") - } else if issue.IsPull { - if issue.PullRequest.IsWorkInProgress(ctx) { - state = ctx.Locale.TrString("repo.issues.draft_title") - } else { - state = ctx.Locale.TrString("repo.issues.open_title") - } - } else { - state = ctx.Locale.TrString("repo.issues.open_title") - } - state = strings.ToLower(state) - - issueStats.SetMargin(10) - if issue.IsPull { - reviews := map[int64]bool{} - for _, comment := range issue.Comments { - if comment.Review != nil { - reviews[comment.Review.ID] = true - } - } - _, err = issueStats.DrawText( - fmt.Sprintf("%s, %s, %s", - ctx.Locale.TrN( - issue.NumComments, - "repo.issues.num_comments_1", - "repo.issues.num_comments", - issue.NumComments, - ), - ctx.Locale.TrN( - len(reviews), - "repo.issues.num_reviews_one", - "repo.issues.num_reviews_few", - len(reviews), - ), - state, - ), - color.Gray{128}, 36, card.Top, card.Left) - } else { - _, err = issueStats.DrawText( - fmt.Sprintf("%s, %s", - ctx.Locale.TrN( - issue.NumComments, - "repo.issues.num_comments_1", - "repo.issues.num_comments", - issue.NumComments, - ), - state, - ), - color.Gray{128}, 36, card.Top, card.Left) - } - if err != nil { - return nil, err - } - - issueAttributionIcon, issueAttributionText := issueAttribution.Split(true, 8) - issueAttributionText.SetMargin(5) - _, err = issueAttributionText.DrawText( - fmt.Sprintf( - "%s - %s", - issue.Poster.Name, - issue.Created.AsTime().Format("2006-01-02"), - ), - color.Gray{128}, 36, card.Middle, card.Left) - if err != nil { - return nil, err - } - err = drawUser(ctx, issueAttributionIcon, issue.Poster) - if err != nil { - return nil, err - } - - return mainCard, nil -} - -func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error { - if user.UseCustomAvatar { - posterAvatarPath := user.CustomAvatarRelativePath() - if posterAvatarPath != "" { - userAvatarFile, err := storage.Avatars.Open(user.CustomAvatarRelativePath()) - if err != nil { - return err - } - userAvatarImage, _, err := image.Decode(userAvatarFile) - if err != nil { - return err - } - card.DrawImage(userAvatarImage) - } - } else { - posterAvatarLink := user.AvatarLinkWithSize(ctx, 256) - card.DrawExternalImage(posterAvatarLink) - } - return nil -} - // UpdateIssueTitle change issue's title func UpdateIssueTitle(ctx *context.Context) { issue := GetActionIssue(ctx) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 65d526d2f2..ae7a645791 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -365,11 +365,7 @@ func SingleRelease(ctx *context.Context) { addVerifyTagToContext(ctx) ctx.Data["PageIsSingleTag"] = release.IsTag - if release.IsTag { - ctx.Data["Title"] = release.TagName - } else { - ctx.Data["Title"] = release.Title - } + ctx.Data["Title"] = release.DisplayName() err = release.LoadArchiveDownloadCount(ctx) if err != nil { @@ -378,6 +374,12 @@ func SingleRelease(ctx *context.Context) { } ctx.Data["Releases"] = releases + + ctx.Data["OpenGraphTitle"] = fmt.Sprintf("%s - %s", release.DisplayName(), release.Repo.FullName()) + ctx.Data["OpenGraphDescription"] = base.EllipsisString(release.Note, 300) + ctx.Data["OpenGraphURL"] = release.HTMLURL() + ctx.Data["OpenGraphImageURL"] = release.SummaryCardURL() + ctx.HTML(http.StatusOK, tplReleasesList) } diff --git a/routers/web/web.go b/routers/web/web.go index 6061863895..c624ea6e59 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1146,9 +1146,10 @@ func registerRoutes(m *web.Route) { m.Group("/{type:issues|pulls}", func() { m.Group("/{index}", func() { m.Get("/info", repo.GetIssueInfo) - m.Get("/summary-card", repo.GetSummaryCard) + m.Get("/summary-card", repo.DrawIssueSummaryCard) }) }) + m.Get("/-/summary-card", repo.DrawRepoSummaryCard) }, ignSignIn, context.RepoAssignment, context.UnitTypes()) // for "/{username}/{reponame}" which doesn't require authentication // Grouping for those endpoints that do require authentication @@ -1298,6 +1299,7 @@ func registerRoutes(m *web.Route) { m.Get("/latest", repo.LatestRelease) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) + m.Get("/summary-card/{releaseID}", repo.DrawReleaseSummaryCard) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) diff --git a/services/context/repo.go b/services/context/repo.go index 45a046eff6..de4e33eb1f 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -632,6 +632,8 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID) } + ctx.Data["OpenGraphImageURL"] = repo.SummaryCardURL() + if repo.IsFork { RetrieveBaseRepo(ctx, repo) if ctx.Written() { diff --git a/templates/base/head_opengraph.tmpl b/templates/base/head_opengraph.tmpl index be9829bf97..bcdfa25695 100644 --- a/templates/base/head_opengraph.tmpl +++ b/templates/base/head_opengraph.tmpl @@ -1,4 +1,16 @@ {{- /* og:description - a one to two sentence description of your object, maybe it only needs at most 300 bytes */ -}} +{{if .OpenGraphTitle}} + +{{end}} +{{if .OpenGraphDescription}} + +{{end}} +{{if .OpenGraphURL}} + +{{end}} +{{if .OpenGraphImageURL}} + +{{end}} {{if .PageIsUserProfile}} @@ -35,14 +47,18 @@ {{end}} {{else}} - - - {{if .Repository.Description}} + {{if not .OpenGraphTitle}} + + {{end}} + {{if not .OpenGraphURL}} + + {{end}} + {{if and (.Repository.Description) (not .OpenGraphDescription)}} {{end}} {{end}} - {{if not .Issue}} + {{if and (not .Issue) (not .OpenGraphImageURL)}} {{if (.Repository.AvatarLink ctx)}} {{else}} diff --git a/tests/integration/opengraph_test.go b/tests/integration/opengraph_test.go index 40013bd247..89e2a85acd 100644 --- a/tests/integration/opengraph_test.go +++ b/tests/integration/opengraph_test.go @@ -97,7 +97,7 @@ func TestOpenGraphProperties(t *testing.T) { "og:title": "repo49/test/test.txt at master", "og:url": setting.AppURL + "/user27/repo49/src/branch/master/test/test.txt", "og:type": "object", - "og:image": setting.AppURL + "assets/img/avatar_default.png", + "og:image": setting.AppURL + "user27/repo49/-/summary-card", "og:site_name": siteName, }, }, @@ -108,7 +108,7 @@ func TestOpenGraphProperties(t *testing.T) { "og:title": "Page With Spaced Name", "og:url": setting.AppURL + "/user2/repo1/wiki/Page-With-Spaced-Name", "og:type": "object", - "og:image": setting.AppURL + "avatars/ab53a2911ddf9b4817ac01ddcd3d975f", + "og:image": setting.AppURL + "user2/repo1/-/summary-card", "og:site_name": siteName, }, }, @@ -119,7 +119,7 @@ func TestOpenGraphProperties(t *testing.T) { "og:title": "repo1", "og:url": setting.AppURL + "user2/repo1", "og:type": "object", - "og:image": setting.AppURL + "avatars/ab53a2911ddf9b4817ac01ddcd3d975f", + "og:image": setting.AppURL + "user2/repo1/-/summary-card", "og:site_name": siteName, }, }, @@ -131,7 +131,7 @@ func TestOpenGraphProperties(t *testing.T) { "og:url": setting.AppURL + "user27/repo49", "og:description": "A wonderful repository with more than just a README.md", "og:type": "object", - "og:image": setting.AppURL + "assets/img/avatar_default.png", + "og:image": setting.AppURL + "user27/repo49/-/summary-card", "og:site_name": siteName, }, }, @@ -166,6 +166,10 @@ func TestOpenGraphSummaryCard(t *testing.T) { name string url string }{ + { + name: "repo", + url: "/user2/repo1/-/summary-card", + }, { name: "issue", url: "/user2/repo1/issues/1/summary-card", @@ -174,6 +178,10 @@ func TestOpenGraphSummaryCard(t *testing.T) { name: "pull request", url: "/user2/repo1/pulls/2/summary-card", }, + { + name: "release", + url: "/user2/repo1/releases/summary-card/1", + }, } for _, tc := range cases { From 25bfd2983ef349163b8961cbd6d369fec5841660 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Wed, 18 Dec 2024 20:26:39 +0100 Subject: [PATCH 0005/1157] Update --- models/issues/issue.go | 14 -------------- options/locale/locale_en-US.ini | 2 ++ routers/web/repo/card.go | 31 +++++++++++++++--------------- routers/web/repo/issue.go | 2 ++ routers/web/repo/release.go | 1 + services/context/repo.go | 5 +++++ templates/base/head_opengraph.tmpl | 13 +++++++++---- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/models/issues/issue.go b/models/issues/issue.go index 17391ffe6c..fbbc4828a2 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -416,20 +416,6 @@ func (issue *Issue) SummaryCardURL() string { return fmt.Sprintf("%s/summary-card", issue.HTMLURL()) } -func (issue *Issue) SummaryCardSize() (int, int) { - return 1200, 600 -} - -func (issue *Issue) SummaryCardWidth() int { - width, _ := issue.SummaryCardSize() - return width -} - -func (issue *Issue) SummaryCardHeight() int { - _, height := issue.SummaryCardSize() - return height -} - // Link returns the issue's relative URL. func (issue *Issue) Link() string { var path string diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3a1639db7e..40ed989e21 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1148,6 +1148,7 @@ blame_prior = View blame prior to this change blame.ignore_revs = Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view. blame.ignore_revs.failed = Failed to ignore revisions in .git-blame-ignore-revs. author_search_tooltip = Shows a maximum of 30 users +summary_card_alt = Summary card of repository %s tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s @@ -2740,6 +2741,7 @@ release.asset_name = Asset name release.asset_external_url = External URL release.add_external_asset = Add external asset release.invalid_external_url = Invalid external URL: "%s" +release.summary_card_alt = Summary card of an release titled "%s" in repository %s branch.name = Branch name branch.already_exists = A branch named "%s" already exists. diff --git a/routers/web/repo/card.go b/routers/web/repo/card.go index 3564d6486c..0afdf98d0a 100644 --- a/routers/web/repo/card.go +++ b/routers/web/repo/card.go @@ -25,7 +25,7 @@ import ( "code.gitea.io/gitea/services/context" ) -// drawUser draws a user avator in a summary card +// drawUser draws a user avatar in a summary card func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) error { if user.UseCustomAvatar { posterAvatarPath := user.CustomAvatarRelativePath() @@ -50,6 +50,7 @@ func drawUser(ctx *context.Context, card *card.Card, user *user_model.User) erro // drawRepoIcon draws the repo icon in a summary card func drawRepoIcon(ctx *context.Context, card *card.Card, repo *repo_model.Repository) error { repoAvatarPath := repo.CustomAvatarRelativePath() + if repoAvatarPath != "" { repoAvatarFile, err := storage.RepoAvatars.Open(repoAvatarPath) if err != nil { @@ -61,21 +62,21 @@ func drawRepoIcon(ctx *context.Context, card *card.Card, repo *repo_model.Reposi } card.DrawImage(repoAvatarImage) return nil - } else { - // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon - err := repo.LoadOwner(ctx) + } + + // If the repo didn't have an avatar, fallback to the repo owner's avatar for the right-hand-side icon + err := repo.LoadOwner(ctx) + if err != nil { + return err + } + if repo.Owner != nil { + err = drawUser(ctx, card, repo.Owner) if err != nil { return err } - if repo.Owner != nil { - err = drawUser(ctx, card, repo.Owner) - if err != nil { - return err - } - } - - return nil } + + return nil } // hexToColor converts a hex color to a go color @@ -227,7 +228,7 @@ func drawRepoSummaryCard(ctx *context.Context, repo *repo_model.Repository) (*ca } func drawIssueSummaryCard(ctx *context.Context, issue *issue_model.Issue) (*card.Card, error) { - width, height := issue.SummaryCardSize() + width, height := card.DefaultSize() mainCard, err := card.NewCard(width, height) if err != nil { return nil, err @@ -397,7 +398,7 @@ func drawReleaseSummaryCard(ctx *context.Context, release *repo_model.Release) ( return mainCard, nil } -// checkCardCache checks if a card in cahce and serves it +// checkCardCache checks if a card in cache and serves it func checkCardCache(ctx *context.Context, cacheKey string) bool { cache := cache.GetCache() pngData, ok := cache.Get(cacheKey).([]byte) @@ -414,7 +415,7 @@ func checkCardCache(ctx *context.Context, cacheKey string) bool { return false } -// serveCard server a Crad to the user adds it to the cache +// serveCard server a Card to the user adds it to the cache func serveCard(ctx *context.Context, card *card.Card, cacheKey string) { cache := cache.GetCache() diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index b86749ec69..0248485dac 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -2055,6 +2055,8 @@ func ViewIssue(ctx *context.Context) { ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() ctx.Data["NewPinAllowed"] = pinAllowed ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0 + ctx.Data["OpenGraphImageURL"] = issue.SummaryCardURL() + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.issues.summary_card_alt", issue.Title, issue.Repo.FullName()) prepareHiddenCommentType(ctx) if ctx.Written() { diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index ae7a645791..1791788743 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -379,6 +379,7 @@ func SingleRelease(ctx *context.Context) { ctx.Data["OpenGraphDescription"] = base.EllipsisString(release.Note, 300) ctx.Data["OpenGraphURL"] = release.HTMLURL() ctx.Data["OpenGraphImageURL"] = release.SummaryCardURL() + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.release.summary_card_alt", release.DisplayName(), release.Repo.FullName()) ctx.HTML(http.StatusOK, tplReleasesList) } diff --git a/services/context/repo.go b/services/context/repo.go index de4e33eb1f..462d843bfc 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -25,6 +25,7 @@ import ( unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/card" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" code_indexer "code.gitea.io/gitea/modules/indexer/code" @@ -632,7 +633,11 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID) } + cardWidth, cardHeight := card.DefaultSize() ctx.Data["OpenGraphImageURL"] = repo.SummaryCardURL() + ctx.Data["OpenGraphImageWidth"] = cardWidth + ctx.Data["OpenGraphImageHeight"] = cardHeight + ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.summary_card_alt", repo.FullName()) if repo.IsFork { RetrieveBaseRepo(ctx, repo) diff --git a/templates/base/head_opengraph.tmpl b/templates/base/head_opengraph.tmpl index bcdfa25695..692f1797b6 100644 --- a/templates/base/head_opengraph.tmpl +++ b/templates/base/head_opengraph.tmpl @@ -10,6 +10,15 @@ {{end}} {{if .OpenGraphImageURL}} + {{if .OpenGraphImageWidth}} + + {{end}} + {{if .OpenGraphImageHeight}} + + {{end}} + {{if .OpenGraphImageAltText}} + + {{end}} {{end}} {{if .PageIsUserProfile}} @@ -26,10 +35,6 @@ {{if .Issue.Content}} {{end}} - - - - {{else if or .PageIsDiff .IsViewFile}} From aa64f6515c6cb34a9d70ef891b3d187878a71cd8 Mon Sep 17 00:00:00 2001 From: JakobDev Date: Wed, 18 Dec 2024 20:41:40 +0100 Subject: [PATCH 0006/1157] Add copyright header --- routers/web/repo/card.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routers/web/repo/card.go b/routers/web/repo/card.go index 0afdf98d0a..23cd705ba5 100644 --- a/routers/web/repo/card.go +++ b/routers/web/repo/card.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package repo import ( From 4568ebc91318a3451da14989eeb321544b95743a Mon Sep 17 00:00:00 2001 From: JakobDev Date: Wed, 18 Dec 2024 22:33:22 +0100 Subject: [PATCH 0007/1157] Update --- models/repo/release.go | 2 +- routers/web/repo/card.go | 2 +- routers/web/web.go | 2 +- tests/integration/opengraph_test.go | 70 ++++++++++++++++++++--------- 4 files changed, 51 insertions(+), 25 deletions(-) diff --git a/models/repo/release.go b/models/repo/release.go index 86cb553da0..cd6f322435 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -191,7 +191,7 @@ func (r *Release) Link() string { // SummaryCardURL returns the absolute URL to an image providing a summary of the release func (r *Release) SummaryCardURL() string { - return fmt.Sprintf("%s/releases/summary-card/%d", r.Repo.HTMLURL(), r.ID) + return fmt.Sprintf("%s/releases/summary-card/%s", r.Repo.HTMLURL(), util.PathEscapeSegments(r.TagName)) } // DisplayName retruns the name of the release diff --git a/routers/web/repo/card.go b/routers/web/repo/card.go index 23cd705ba5..1fe7283c8d 100644 --- a/routers/web/repo/card.go +++ b/routers/web/repo/card.go @@ -494,7 +494,7 @@ func DrawIssueSummaryCard(ctx *context.Context) { } func DrawReleaseSummaryCard(ctx *context.Context) { - release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":releaseID")) + release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*")) if err != nil { if repo_model.IsErrReleaseNotExist(err) { ctx.NotFound("", nil) diff --git a/routers/web/web.go b/routers/web/web.go index c624ea6e59..4d8d280c89 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1299,7 +1299,7 @@ func registerRoutes(m *web.Route) { m.Get("/latest", repo.LatestRelease) m.Get(".rss", feedEnabled, repo.ReleasesFeedRSS) m.Get(".atom", feedEnabled, repo.ReleasesFeedAtom) - m.Get("/summary-card/{releaseID}", repo.DrawReleaseSummaryCard) + m.Get("/summary-card/*", repo.DrawReleaseSummaryCard) }, ctxDataSet("EnableFeed", setting.Other.EnableFeed), repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefTag, true)) m.Get("/releases/attachments/{uuid}", repo.MustBeNotEmpty, repo.GetAttachment) diff --git a/tests/integration/opengraph_test.go b/tests/integration/opengraph_test.go index 89e2a85acd..d54f59c9b8 100644 --- a/tests/integration/opengraph_test.go +++ b/tests/integration/opengraph_test.go @@ -94,45 +94,71 @@ func TestOpenGraphProperties(t *testing.T) { name: "file in repo", url: "/user27/repo49/src/branch/master/test/test.txt", expected: map[string]string{ - "og:title": "repo49/test/test.txt at master", - "og:url": setting.AppURL + "/user27/repo49/src/branch/master/test/test.txt", - "og:type": "object", - "og:image": setting.AppURL + "user27/repo49/-/summary-card", - "og:site_name": siteName, + "og:title": "repo49/test/test.txt at master", + "og:url": setting.AppURL + "/user27/repo49/src/branch/master/test/test.txt", + "og:type": "object", + "og:image": setting.AppURL + "user27/repo49/-/summary-card", + "og:image:alt": "Summary card of repository user27/repo49", + "og:image:width": "1200", + "og:image:height": "600", + "og:site_name": siteName, }, }, { name: "wiki page for repo without description", url: "/user2/repo1/wiki/Page-With-Spaced-Name", expected: map[string]string{ - "og:title": "Page With Spaced Name", - "og:url": setting.AppURL + "/user2/repo1/wiki/Page-With-Spaced-Name", - "og:type": "object", - "og:image": setting.AppURL + "user2/repo1/-/summary-card", - "og:site_name": siteName, + "og:title": "Page With Spaced Name", + "og:url": setting.AppURL + "/user2/repo1/wiki/Page-With-Spaced-Name", + "og:type": "object", + "og:image": setting.AppURL + "user2/repo1/-/summary-card", + "og:image:alt": "Summary card of repository user2/repo1", + "og:image:width": "1200", + "og:image:height": "600", + "og:site_name": siteName, }, }, { name: "index page for repo without description", url: "/user2/repo1", expected: map[string]string{ - "og:title": "repo1", - "og:url": setting.AppURL + "user2/repo1", - "og:type": "object", - "og:image": setting.AppURL + "user2/repo1/-/summary-card", - "og:site_name": siteName, + "og:title": "repo1", + "og:url": setting.AppURL + "user2/repo1", + "og:type": "object", + "og:image": setting.AppURL + "user2/repo1/-/summary-card", + "og:image:alt": "Summary card of repository user2/repo1", + "og:image:width": "1200", + "og:image:height": "600", + "og:site_name": siteName, }, }, { name: "index page for repo with description", url: "/user27/repo49", expected: map[string]string{ - "og:title": "repo49", - "og:url": setting.AppURL + "user27/repo49", - "og:description": "A wonderful repository with more than just a README.md", - "og:type": "object", - "og:image": setting.AppURL + "user27/repo49/-/summary-card", - "og:site_name": siteName, + "og:title": "repo49", + "og:url": setting.AppURL + "user27/repo49", + "og:description": "A wonderful repository with more than just a README.md", + "og:type": "object", + "og:image": setting.AppURL + "user27/repo49/-/summary-card", + "og:image:alt": "Summary card of repository user27/repo49", + "og:image:width": "1200", + "og:image:height": "600", + "og:site_name": siteName, + }, + }, + { + name: "release", + url: "/user2/repo1/releases/tag/v1.1", + expected: map[string]string{ + "og:title": "testing-release - user2/repo1", + "og:url": setting.AppURL + "user2/repo1/releases/tag/v1.1", + "og:type": "object", + "og:image": setting.AppURL + "user2/repo1/releases/summary-card/v1.1", + "og:image:alt": "Summary card of an release titled \"testing-release\" in repository user2/repo1", + "og:image:width": "1200", + "og:image:height": "600", + "og:site_name": siteName, }, }, } @@ -180,7 +206,7 @@ func TestOpenGraphSummaryCard(t *testing.T) { }, { name: "release", - url: "/user2/repo1/releases/summary-card/1", + url: "/user2/repo1/releases/summary-card/v1.1", }, } From 8bd1c7ff319bc4cec9f0e979edd91e1002c0407d Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 19 Dec 2024 10:47:47 +0000 Subject: [PATCH 0008/1157] feat: add synchronization for SSH keys in handleOAuth2SignIn --- routers/web/auth/oauth.go | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index fbdd47479a..16c3c9f9d4 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -48,6 +48,8 @@ import ( "github.com/markbates/goth/providers/openidConnect" "github.com/markbates/goth/providers/zoom" go_oauth2 "golang.org/x/oauth2" + + asymkey_model "code.gitea.io/gitea/models/asymkey" ) const ( @@ -1183,8 +1185,62 @@ func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) { } } +func getSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) { + key := source.AttributeSSHPublicKey + value, exists := gothUser.RawData[key] + if !exists { + return nil, fmt.Errorf("attribute '%s' not found in user data", key) + } + + rawSlice, ok := value.([]any) + if !ok { + return nil, fmt.Errorf("unexpected type for SSH public key, expected []interface{} but got %T", value) + } + + sshKeys := make([]string, 0, len(rawSlice)) + for i, v := range rawSlice { + str, ok := v.(string) + if !ok { + return nil, fmt.Errorf("unexpected element type at index %d in SSH public key array, expected string but got %T", i, v) + } + sshKeys = append(sshKeys, str) + } + + return sshKeys, nil +} + +func updateSshPubIfNeed( + ctx *context.Context, + authSource *auth.Source, + fetchedUser *goth.User, + user *user_model.User, +) error { + oauth2Source := authSource.Cfg.(*oauth2.Source) + + if oauth2Source.ProvidesSSHKeys() { + sshKeys, err := getSSHKeys(oauth2Source, fetchedUser) + if err != nil { + return err + } + + if len(sshKeys) == 0 { + return nil + } + + if asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) { + err = asymkey_model.RewriteAllPublicKeys(ctx) + if err != nil { + return err + } + } + } + + return nil +} + func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) { updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) + updateSshPubIfNeed(ctx, source, &gothUser, u) needs2FA := false if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA { From 32de0745e438a55bb4fc3cb10c29580af89f4618 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 19 Dec 2024 11:17:28 +0000 Subject: [PATCH 0009/1157] style: fix fmt and lint --- routers/web/auth/oauth.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 16c3c9f9d4..48aaca17cb 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -17,6 +17,7 @@ import ( "sort" "strings" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" @@ -48,8 +49,6 @@ import ( "github.com/markbates/goth/providers/openidConnect" "github.com/markbates/goth/providers/zoom" go_oauth2 "golang.org/x/oauth2" - - asymkey_model "code.gitea.io/gitea/models/asymkey" ) const ( @@ -1209,7 +1208,7 @@ func getSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) { return sshKeys, nil } -func updateSshPubIfNeed( +func updateSSHPubIfNeed( ctx *context.Context, authSource *auth.Source, fetchedUser *goth.User, @@ -1240,7 +1239,11 @@ func updateSshPubIfNeed( func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) { updateAvatarIfNeed(ctx, gothUser.AvatarURL, u) - updateSshPubIfNeed(ctx, source, &gothUser, u) + err := updateSSHPubIfNeed(ctx, source, &gothUser, u) + if err != nil { + ctx.ServerError("updateSSHPubIfNeed", err) + return + } needs2FA := false if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA { From 515eec3d1a02603c07e2bc39bb351792d4ac95e8 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Sat, 21 Dec 2024 21:25:38 +0000 Subject: [PATCH 0010/1157] remove `len(sshKeys) == 0` check --- routers/web/auth/oauth.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 48aaca17cb..4d8fbcb7a5 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1222,10 +1222,6 @@ func updateSSHPubIfNeed( return err } - if len(sshKeys) == 0 { - return nil - } - if asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) { err = asymkey_model.RewriteAllPublicKeys(ctx) if err != nil { From 283d883e5ae01eef16e2c9f63929122cd816162e Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Sat, 21 Dec 2024 21:25:45 +0000 Subject: [PATCH 0011/1157] add integration tests for synchronization for SSH keys with OpenID Connect --- tests/integration/oauth_test.go | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index f385b99e46..675573d6cb 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -691,6 +691,117 @@ func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) { assert.Equal(t, "/login/oauth/authorize?redirect_uri=https://translate.example.org", test.RedirectURL(resp)) } +func setupMockOIDCServer() *httptest.Server { + var mockServer *httptest.Server + mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/openid-configuration": + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{ + "issuer": "` + mockServer.URL + `", + "authorization_endpoint": "` + mockServer.URL + `/authorize", + "token_endpoint": "` + mockServer.URL + `/token", + "userinfo_endpoint": "` + mockServer.URL + `/userinfo" + }`)) + default: + http.NotFound(w, r) + } + })) + return mockServer +} + +func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { + defer tests.PrepareTestEnv(t)() + mockServer := setupMockOIDCServer() + defer mockServer.Close() + + sourceName := "oidc" + authPayload := authSourcePayloadOpenIDConnect(sourceName, mockServer.URL+"/") + authPayload["oauth2_attribute_ssh_public_key"] = "sshpubkey" + authSource := addAuthSource(t, authPayload) + + userID := "5678" + user := &user_model.User{ + Name: "oidc.user", + Email: "oidc.user@example.com", + Passwd: "oidc.userpassword", + Type: user_model.UserTypeIndividual, + LoginType: auth_model.OAuth2, + LoginSource: authSource.ID, + LoginName: userID, + IsActive: true, + } + defer createUser(context.Background(), t, user)() + + for _, tt := range []struct { + name string + rawData map[string]interface{} + parsedKeySets []string + }{ + { + name: "Add keys", + rawData: map[string]interface{}{ + "sshpubkey": []interface{}{ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINDRDoephkaFELacrNNe2fqAwedhRB1MKOpLEHlPuczO nocomment", + }, + }, + parsedKeySets: []string{ + "SHA256:X/mW7JUQ8J8yhrKBbZ/pJni8qx7zPA1DTFsi8ftpDwg", + }, + }, + { + name: "Update keys", + rawData: map[string]interface{}{ + "sshpubkey": []interface{}{ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMLLMOLFMouSJmzOASKKv178d+7op4utSxcugF9tVVch nocomment", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGyDh9sg1IGQGa0U363wcGXrDlGBhZI3UHvS7we/0d+T nocomment", + }, + }, + parsedKeySets: []string{ + "SHA256:gsyG4JNmY5XoLBK5lSzuwD3EXcaDBiDKBkqDkpQTH6Q", + "SHA256:bbEKB1Qpumgk6QrgiN6t/kIvtUZvIQ8rqQBz8yYPzYw", + }, + }, + { + name: "Remove keys", + rawData: map[string]interface{}{ + "sshpubkey": []interface{}{}, + }, + parsedKeySets: []string{}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { + return goth.User{ + Provider: sourceName, + UserID: userID, + Email: user.Email, + RawData: tt.rawData, + }, nil + })() + + session := emptyTestSession(t) + + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", sourceName)) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) + + req = NewRequest(t, "GET", "/user/settings/keys") + resp = session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + divs := htmlDoc.doc.Find("#keys-ssh .flex-item .flex-item-body:not(:last-child)") + + syncedKeys := make([]string, divs.Length()) + for i := 0; i < divs.Length(); i++ { + syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text()) + } + + assert.ElementsMatch(t, tt.parsedKeySets, syncedKeys, "Unequal number of keys") + }) + } +} + func TestSignUpViaOAuthWithMissingFields(t *testing.T) { defer tests.PrepareTestEnv(t)() // enable auto-creation of accounts via OAuth2 From 4f92b738b50da841a757c42bb34c565cde417615 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Sat, 21 Dec 2024 21:44:05 +0000 Subject: [PATCH 0012/1157] style: make fmt --- tests/integration/oauth_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 675573d6cb..6b5a1feb16 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -735,13 +735,13 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { for _, tt := range []struct { name string - rawData map[string]interface{} + rawData map[string]any parsedKeySets []string }{ { name: "Add keys", - rawData: map[string]interface{}{ - "sshpubkey": []interface{}{ + rawData: map[string]any{ + "sshpubkey": []any{ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINDRDoephkaFELacrNNe2fqAwedhRB1MKOpLEHlPuczO nocomment", }, }, @@ -751,8 +751,8 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { }, { name: "Update keys", - rawData: map[string]interface{}{ - "sshpubkey": []interface{}{ + rawData: map[string]any{ + "sshpubkey": []any{ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMLLMOLFMouSJmzOASKKv178d+7op4utSxcugF9tVVch nocomment", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGyDh9sg1IGQGa0U363wcGXrDlGBhZI3UHvS7we/0d+T nocomment", }, @@ -764,8 +764,8 @@ func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) { }, { name: "Remove keys", - rawData: map[string]interface{}{ - "sshpubkey": []interface{}{}, + rawData: map[string]any{ + "sshpubkey": []any{}, }, parsedKeySets: []string{}, }, From 6836ded397fb7132868fac5a7b1593b932fa46e5 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 25 Dec 2024 11:21:59 +0100 Subject: [PATCH 0013/1157] chore(release): first commit of v11.0 --- release-notes-published/10.0.0.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 release-notes-published/10.0.0.md diff --git a/release-notes-published/10.0.0.md b/release-notes-published/10.0.0.md new file mode 100644 index 0000000000..e69de29bb2 From 72dcf046900b66d86c1a31022c2b4d62304c6858 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Wed, 25 Dec 2024 12:00:15 +0100 Subject: [PATCH 0014/1157] chore(release): v10.0 is cut, v9.0 is soon to be EOL --- renovate.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 4e869db579..64e610cd57 100644 --- a/renovate.json +++ b/renovate.json @@ -9,8 +9,8 @@ "baseBranches": [ "$default", "/^v7\\.\\d+/forgejo$/", - "/^v9\\.\\d+/forgejo$/", - "/^v10\\.\\d+/forgejo$/" + "/^v10\\.\\d+/forgejo$/", + "/^v11\\.\\d+/forgejo$/" ], "postUpdateOptions": ["gomodTidy", "gomodUpdateImportPaths", "npmDedupe"], "prConcurrentLimit": 10, From dd4a1107ed8977594be48d06aaec2039c983c943 Mon Sep 17 00:00:00 2001 From: Litchi Pi Date: Mon, 16 Dec 2024 13:44:25 +0100 Subject: [PATCH 0015/1157] template: repo: compare: display a warning if the user is not logged in Signed-off-by: Litchi Pi --- options/locale/locale_en-US.ini | 1 + routers/web/repo/compare.go | 1 + templates/repo/diff/compare.tmpl | 4 ++++ tests/integration/compare_test.go | 40 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3b413b8f92..f00d738026 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1845,6 +1845,7 @@ pulls.new = New pull request pulls.view = View pull request pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes pulls.compare_changes = New pull request +pulls.sign_in_require = Sign in to create a new pull request. pulls.allow_edits_from_maintainers = Allow edits from maintainers pulls.allow_edits_from_maintainers_desc = Users with write access to the base branch can also push to this branch pulls.allow_edits_from_maintainers_err = Updating failed diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index e5eab2bffa..03d49fa692 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -51,6 +51,7 @@ const ( func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner, headName string) { ctx.Data["BeforeCommit"] = before ctx.Data["HeadCommit"] = head + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob { if commit == nil { diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 612d08ec4f..024577afcc 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -215,6 +215,10 @@ {{ctx.Locale.Tr "repo.archive.title_date" (DateUtils.AbsoluteLong .Repository.ArchivedUnix)}} {{end}}
+ {{else}} +
+ {{ctx.Locale.Tr "repo.pulls.sign_in_require" .SignInLink}} +
{{end}} {{if $.IsSigned}}
diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index a6f994835b..2d27a430ac 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -291,3 +291,43 @@ func TestCompareCodeExpand(t *testing.T) { }) }) } + +func TestCompareSignedIn(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // Setup the test with a connected user + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testCreateBranch(t, session, "user1", "repo1", "branch/master", "recent-push", http.StatusSeeOther) + testEditFile(t, session, "user1", "repo1", "recent-push", "README.md", "Hello recently!\n") + + newPrSelector := "button.ui.button.primary.show-form" + + t.Run("PR creation button displayed if logged in", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user1/repo1/compare/master...recent-push") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Check that the "Sign in" button doesn't show up + htmlDoc.AssertElement(t, "a[href='/user/login?redirect_to=%2Fuser1%2Frepo1%2Fcompare%2Fmaster...recent-push']", false) + + // Check that the "New pull request" button shows up + htmlDoc.AssertElement(t, newPrSelector, true) + }) + + t.Run("no PR creation button but display warning", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/user1/repo1/compare/master...recent-push") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + // Check that the "Sign in" button shows up + htmlDoc.AssertElement(t, "a[href='/user/login?redirect_to=%2Fuser1%2Frepo1%2Fcompare%2Fmaster...recent-push']", true) + + // Check that the "New pull request" button doesn't show up + htmlDoc.AssertElement(t, newPrSelector, false) + }) + }) +} From 64deec434a5910a3515d18c5a135848436cc76a7 Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 25 Dec 2024 23:08:03 +0100 Subject: [PATCH 0016/1157] Revert "Update dependency idiomorph to v0.4.0" This reverts commit f9aaefd107bc6583a348e89fb76f41443224a821. I've not not yet been able to determine what commit caused it, but 0.4.0 is broken for Forgejo's usecase it's not morphing and instead replacing (it seems) elements when there's no need to. --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ae637779dd..e081796a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.2", "htmx.org": "1.9.12", - "idiomorph": "0.4.0", + "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.18", "mermaid": "11.4.1", @@ -9314,10 +9314,10 @@ } }, "node_modules/idiomorph": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.4.0.tgz", - "integrity": "sha512-VdXFpZOTXhLatJmhCWJR5oQKLXT01O6sFCJqT0/EqG71C4tYZdPJ5etvttwWsT2WKRYWz160XkNr1DUqXNMZHg==", - "license": "BSD-2-Clause" + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz", + "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==", + "license": "BSD 2-Clause" }, "node_modules/ieee754": { "version": "1.2.1", diff --git a/package.json b/package.json index c674c011dd..fe7c30471b 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.2", "htmx.org": "1.9.12", - "idiomorph": "0.4.0", + "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.18", "mermaid": "11.4.1", From 5eb08773814513bd688e714c4ee1ebf905c44f55 Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 26 Dec 2024 09:40:21 +0100 Subject: [PATCH 0017/1157] chore(i18n): user/label translations in danish/latvian Refs: https://codeberg.org/forgejo/forgejo/pulls/6331 --- build/lint-locale.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/lint-locale.go b/build/lint-locale.go index c49e236938..44d320994c 100644 --- a/build/lint-locale.go +++ b/build/lint-locale.go @@ -59,9 +59,9 @@ func initRemoveTags() { oldnew := []string{} for _, el := range []string{ "email@example.com", "correu@example.com", "epasts@domens.lv", "email@exemplo.com", "eposta@ornek.com", "email@példa.hu", "email@esempio.it", - "user", "utente", "lietotājs", "gebruiker", "usuário", "Benutzer", "Bruker", + "user", "utente", "lietotājs", "gebruiker", "usuário", "Benutzer", "Bruker", "bruger", "server", "servidor", "kiszolgáló", "serveris", - "label", "etichetta", "etiķete", "rótulo", "Label", "utilizador", + "label", "etichetta", "etiķete", "rótulo", "Label", "utilizador", "etiket", "iezīme", } { oldnew = append(oldnew, "<"+el+">", "REPLACED-TAG") } From f6c442c2fee18b25e69c0ab0d0623c263b031d2e Mon Sep 17 00:00:00 2001 From: Earl Warren Date: Thu, 26 Dec 2024 09:40:21 +0100 Subject: [PATCH 0018/1157] chore(i18n): user/label translations in danish/latvian Refs: https://codeberg.org/forgejo/forgejo/pulls/6331 (cherry picked from commit 5eb08773814513bd688e714c4ee1ebf905c44f55) --- build/lint-locale.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/lint-locale.go b/build/lint-locale.go index c49e236938..44d320994c 100644 --- a/build/lint-locale.go +++ b/build/lint-locale.go @@ -59,9 +59,9 @@ func initRemoveTags() { oldnew := []string{} for _, el := range []string{ "email@example.com", "correu@example.com", "epasts@domens.lv", "email@exemplo.com", "eposta@ornek.com", "email@példa.hu", "email@esempio.it", - "user", "utente", "lietotājs", "gebruiker", "usuário", "Benutzer", "Bruker", + "user", "utente", "lietotājs", "gebruiker", "usuário", "Benutzer", "Bruker", "bruger", "server", "servidor", "kiszolgáló", "serveris", - "label", "etichetta", "etiķete", "rótulo", "Label", "utilizador", + "label", "etichetta", "etiķete", "rótulo", "Label", "utilizador", "etiket", "iezīme", } { oldnew = append(oldnew, "<"+el+">", "REPLACED-TAG") } From e94134def63ceaf6b46d5a617b11d4be0f5c1f59 Mon Sep 17 00:00:00 2001 From: Codeberg Translate Date: Thu, 26 Dec 2024 09:26:46 +0000 Subject: [PATCH 0019/1157] i18n: update of translations from Codeberg Translate (#6331) Co-authored-by: 0ko <0ko@users.noreply.translate.codeberg.org> Co-authored-by: Fjuro Co-authored-by: tacaly Co-authored-by: tacaly Co-authored-by: artnay Co-authored-by: WithLithum Co-authored-by: Wuzzy Co-authored-by: Atalanttore Co-authored-by: Juno Takano Co-authored-by: emansije Co-authored-by: earl-warren Co-authored-by: Edgarsons Co-authored-by: Benedikt Straub Co-authored-by: SomeTr Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6331 Reviewed-by: Earl Warren Co-authored-by: Codeberg Translate Co-committed-by: Codeberg Translate --- options/locale/locale_cs-CZ.ini | 3 +- options/locale/locale_da.ini | 907 ++++++++++++++++++++++++++++++++ options/locale/locale_de-DE.ini | 16 +- options/locale/locale_fi-FI.ini | 119 ++++- options/locale/locale_fr-FR.ini | 18 +- options/locale/locale_lv-LV.ini | 124 +++-- options/locale/locale_nds.ini | 1 + options/locale/locale_pt-BR.ini | 32 +- options/locale/locale_pt-PT.ini | 3 + options/locale/locale_ru-RU.ini | 83 +-- options/locale/locale_uk-UA.ini | 29 +- options/locale/locale_zh-CN.ini | 2 +- 12 files changed, 1173 insertions(+), 164 deletions(-) create mode 100644 options/locale/locale_da.ini diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index bd983164b2..375c4a710f 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1010,7 +1010,7 @@ remove_account_link=Odstranit propojený účet remove_account_link_desc=Odstraněním propojeného účtu zrušíte jeho přístup k vašemu Forgejo účtu. Pokračovat? remove_account_link_success=Propojený účet byl odstraněn. -hooks.desc=Přidat webhooky, které budou spouštěny pro všechny repozitáře vve vašem vlastnictví. +hooks.desc=Přidejte webhooky, které budou spouštěny pro všechny repozitáře ve vašem vlastnictví. orgs_none=Nejste členem žádné organizace. repos_none=Nevlastníte žádné repozitáře. @@ -2849,6 +2849,7 @@ issues.num_reviews_one = %d kontrola issues.num_reviews_few = %d kontrol issues.summary_card_alt = Souhrn problému s názvem „%s“ v repozitáři %s editor.add_tmpl.filename = nazevsouboru +settings.default_update_style_desc = Výchozí způsob aktualizací používaný pro aktualizace žádostí o sloučení, které jsou pozadu oproti základní větvi. [graphs] component_loading_info = Tohle může chvíli trvat… diff --git a/options/locale/locale_da.ini b/options/locale/locale_da.ini new file mode 100644 index 0000000000..25a3010e05 --- /dev/null +++ b/options/locale/locale_da.ini @@ -0,0 +1,907 @@ + + + +[common] +home = Hjem +dashboard = Instrumentpanel +explore = Udforsk +help = Hjælp +logo = Logo +sign_in = Login +sign_in_with_provider = Login med %s +sign_in_or = eller +sign_out = Logud +sign_up = Register +return_to_forgejo = Vend tilbage til Forgejo +new_repo.title = Ny repository +retry = Prøv igen +link_account = Link konto +register = Register +powered_by = Baseret på %s +version = Version +create_new = Opret… +user_profile_and_more = Profil og indstillinger… +page = Side +template = Skabelon +language = Sprog +notifications = Notifikationer +active_stopwatch = Aktiv tidsregistrering +enable_javascript = Dette websted kræver JavaScript. +toc = Indholdsfortegnelse +licenses = Licenser +more_items = Flere genstande +username = Brugernavn +email = E-mailadresse +password = Adgangskode +access_token = Adgangstoken +re_type = Bekræft adgangskode +captcha = CAPTCHA +twofa = To-faktor autentificering +twofa_scratch = To-faktor skrabekode +webauthn_sign_in = Tryk på knappen på din sikkerhedsnøgle. Hvis din sikkerhedsnøgle ikke har nogen knap, skal du indsætte den igen. +webauthn_use_twofa = Brug en tofaktorkode fra din telefon +webauthn_error = Kunne ikke læse din sikkerhedsnøgle. +webauthn_unsupported_browser = Din browser understøtter i øjeblikket ikke WebAuthn. +webauthn_error_unknown = Der opstod en ukendt fejl. Prøv venligst igen. +webauthn_error_unable_to_process = Serveren kunne ikke behandle din anmodning. +webauthn_error_duplicated = Sikkerhedsnøglen er ikke tilladt for denne anmodning. Sørg for, at nøglen ikke allerede er registreret. +webauthn_error_empty = Du skal angive et navn for denne nøgle. +organization = Organisation +mirror = Mirror +new_mirror = Ny mirror +new_fork = Ny repository fork +new_project = Nyt projekt +new_project_column = Ny kolonne +admin_panel = Side administration +settings = Indstillinger +your_profile = Profil +your_starred = Stjernemarkeret +your_settings = Indstillinger +passcode = Passcode +repository = Repository +new_org.title = Ny organisation +new_repo.link = Nyt repository +new_migrate.link = Ny migration +new_org.link = Ny organisation +all = Alle +sources = Kilder +mirrors = Mirrors +collaborative = Samarbejdende +forks = Forks +activities = Aktiviteter +pull_requests = Pull anmodninger +issues = Problemer +milestones = Milepæle +ok = OK +cancel = Annuller +rerun = Kør igen +rerun_all = Kør alle jobs igen +save = Gem +add = Tilføj +add_all = Tilføj alle +remove = Slet +remove_all = Slet alle +remove_label_str = Slet genstand "%s" +edit = Redigere +view = Se +test = Test +disabled = Deaktiveret +copy = Kopiér +copy_generic = Kopiér til udklipsholder +copy_url = Kopiér URL +copy_path = Kopiér sti +copy_content = Kopier indhold +copy_branch = Kopiér branch navn +copy_success = Kopieret! +copy_error = Kopiering mislykkedes +write = Skriv +preview = Forhåndsvisning +loading = Indlæser… +error = Fejl +error413 = Du har opbrugt din kvote. +go_back = Gå tilbage +never = Aldrig +unknown = Ukendt +rss_feed = RSS feed +pin = Pin +unpin = Frigør +artifacts = Artefakter +archived = Arkiveret +concept_system_global = Global +concept_user_individual = Individuel +concept_code_repository = Repository +concept_user_organization = Organisation +show_timestamps = Vis tidsstempler +show_log_seconds = Vis sekunder +tracked_time_summary = Opsummering af sporet tid baseret på filtre af problemliste +signed_in_as = Logget ind som +webauthn_error_insecure = WebAuthn understøtter kun sikre forbindelser. Til test over HTTP kan du bruge oprindelsen "localhost" eller "127.0.0.1" +invalid_data = Ugyldige data: %v +webauthn_insert_key = Indsæt din sikkerhedsnøgle +webauthn_press_button = Tryk venligst på knappen på din sikkerhedsnøgle… +webauthn_error_timeout = Timeout nået, før din nøgle kunne læses. Genindlæs denne side og prøv igen. +enabled = Aktiveret +locked = Låst +copy_hash = Kopiér hash +error404 = Den side, du forsøger at nå, enten findes ikke eller du er ikke autoriseret til at se den. +confirm_delete_artifact = Er du sikker på, at du vil slette artefakten "%s"? +new_migrate.title = Ny migration +copy_type_unsupported = Denne filtype kan ikke kopieres +toggle_menu = TIl/Fra menu +show_full_screen = Vis fuld skærm +download_logs = Download logs +confirm_delete_selected = Bekræft at slette alle valgte genstande? +name = Navn +value = Værdi +filter = Filter +filter.clear = Ryd filtre +filter.is_archived = Arkiveret +filter.not_archived = Ikke arkiveret +filter.is_fork = Forks +filter.not_fork = Ikke forks +filter.is_mirror = Mirrors +filter.not_mirror = Ikke mirrors +filter.is_template = Skabeloner +filter.not_template = Ikke skabeloner +filter.public = Offentlig +filter.private = Privat + +[search] +search = Søg... +type_tooltip = Søge type +fuzzy = Fuzzy +fuzzy_tooltip = Medtag resultater, der også matcher søgeordet tæt +union = Almindelig +union_tooltip = Inkluder resultater, der matcher et hvilket som helst af de mellemrumsadskilte søgeord +exact = Nøjagtig +exact_tooltip = Medtag kun resultater, der matcher den nøjagtige søgeterm +regexp = RegExp +regexp_tooltip = Fortolk søgetermen som et regulært udtryk +org_kind = Søg i organisationer... +team_kind = Søg efter hold... +code_kind = Søg kode... +code_search_by_git_grep = Aktuelle kodesøgeresultater leveres af "git grep". Der kan være bedre resultater, hvis webstedsadministratoren aktiverer kodeindeksering. +package_kind = Søg pakker... +project_kind = Søg efter projekter... +commit_kind = Søg commits... +branch_kind = Søg branches... +runner_kind = Søg runners... +issue_kind = Søg i problemer... +milestone_kind = Søg milepæle... +pull_kind = Søg pulls... +repo_kind = Søg depoter... +code_search_unavailable = Kodesøgning er ikke tilgængelig i øjeblikket. Kontakt venligst webstedets administrator. +no_results = Ingen matchende resultater fundet. +user_kind = Søg brugere... +keyword_search_unavailable = Søgning efter nøgleord er ikke tilgængelig i øjeblikket. Kontakt venligst webstedets administrator. + +[aria] +navbar = Navigationslinje +footer = Sidefod +footer.software = Omkring dette software +footer.links = Links + +[heatmap] +number_of_contributions_in_the_last_12_months = %s bidrag inden for de sidste 12 måneder +contributions_zero = Ingen bidrag +contributions_format = {contributions} på {month} {day}, {year} +contributions_one = bidrag +contributions_few = bidragene +less = Mindre +more = Mere + +[editor] +buttons.heading.tooltip = Tilføj overskrift +buttons.bold.tooltip = Tilføj fed tekst +buttons.italic.tooltip = Tilføj kursiv tekst +buttons.quote.tooltip = Citat tekst +buttons.code.tooltip = Tilføj kode +buttons.link.tooltip = Tilføj et link +buttons.list.unordered.tooltip = Tilføj en punktliste +buttons.list.task.tooltip = Tilføj en liste over opgaver +buttons.list.ordered.tooltip = Tilføj en nummereret liste +buttons.mention.tooltip = Nævn en bruger eller et hold +buttons.ref.tooltip = Henvis til et problem eller pull-anmodning +buttons.enable_monospace_font = Aktiver monospace-skrifttype +buttons.disable_monospace_font = Deaktiver monospace-skrifttype +buttons.new_table.tooltip = Tilføj tabel +table_modal.header = Tilføj tabel +table_modal.placeholder.header = Hoved +table_modal.placeholder.content = Indhold +table_modal.label.rows = Rækker +table_modal.label.columns = Kolonner +buttons.unindent.tooltip = Udsortere genstande med ét niveau +buttons.indent.tooltip = Indlejring af genstande med ét niveau +buttons.switch_to_legacy.tooltip = Brug den gamle editor i stedet + +[filter] +string.asc = A - Z +string.desc = Z - A + +[error] +occurred = Der opstod en fejl +not_found = Målet kunne ikke findes. +network_error = Netværksfejl +server_internal = Intern serverfejl +report_message = Hvis du mener, at dette er en Forgejo-fejl, skal du søge efter problemer på Codeberg eller åbne et nyt problem, hvis det er nødvendigt. + +[startpage] +app_desc = En smertefri, selv-hostet Git-tjeneste +install = Nem at installere +platform = På tværs af platforme +platform_desc = Det er bekræftet, at Forgejo kører på frie operativsystemer som Linux og FreeBSD, samt forskellige CPU-arkitekturer. Vælg den du elsker! +lightweight = Letvægtig +lightweight_desc = Forgejo har lave minimale krav og kan køre på en billig Raspberry Pi. Spar din maskines energi! +license = Åben kildekode +license_desc = Få Forgejo! Slut dig til os ved at bidrage til at gøre dette projekt endnu bedre. Vær ikke genert over at være en bidragyder! +install_desc = Du skal blot køre binæren for din platform, send den med Docker, eller få det pakket. + +[install] +install = Installation +title = Indledende konfiguration +docker_helper = Hvis du kører Forgejo inde i Docker, skal du læse dokumentationen, før du ændrer nogen indstillinger. +require_db_desc = Forgejo kræver MySQL, PostgreSQL, SQLite3 eller TiDB (MySQL-protokol). +db_title = Database indstillinger +db_type = Database type +host = Vært +user = Brugernavn +password = Adgangskode +db_name = Database navn +db_schema = Schema +db_schema_helper = Lad stå tom for databasestandard ("offentlig"). +ssl_mode = SSL +reinstall_error = Du forsøger at installere i en eksisterende Forgejo-database +reinstall_confirm_check_1 = Dataene krypteret af SECRET_KEY i app.ini kan gå tabt: brugere kan muligvis ikke logge ind med 2FA/OTP, og mirrors fungerer muligvis ikke korrekt. Ved at markere dette felt bekræfter du, at den aktuelle app.ini-fil indeholder den korrekte SECRET_KEY. +reinstall_confirm_check_3 = Du bekræfter, at du er helt sikker på, at denne Forgejo kører med den korrekte app.ini placering, og at du er sikker på, at du skal gen-installere. Du bekræfter, at du anerkender ovenstående risici. +err_empty_db_path = SQLite3-databasestien må ikke være tom. +no_admin_and_disable_registration = Du kan ikke deaktivere brugerens selvregistrering uden at oprette en administratorkonto. +err_empty_admin_password = Administratoradgangskoden må ikke være tom. +err_empty_admin_email = Administrator-e-mailen må ikke være tom. +err_admin_name_is_reserved = Administratorbrugernavnet er ugyldigt, brugernavnet er reserveret +err_admin_name_pattern_not_allowed = Administratorbrugernavnet er ugyldigt, brugernavnet matcher et reserveret mønster +err_admin_name_is_invalid = Administratorbrugernavnet er ugyldigt +general_title = Generelle indstillinger +app_name = Instans titel +app_slogan_helper = Indtast dit instans slogan her. Lad være tom for at deaktivere. +lfs_path_helper = Filer sporet af Git LFS vil blive gemt i denne mappe. Lad være tom for at deaktivere. +run_user = Bruger at køre som +lfs_path = Git LFS rodsti +repo_path = Depot rodsti +domain = Server domæne +domain_helper = Domæne eller værtsadresse for serveren. +ssh_port = SSH server port +ssh_port_helper = Portnummer, der vil blive brugt af SSH-serveren. Lad være tomt for at deaktivere SSH-serveren. +http_port = HTTP lytte port +http_port_helper = Portnummer, der vil blive brugt af Forgejo-webserveren. +app_url = Base URL +log_root_path = Log sti +log_root_path_helper = Logfiler vil blive skrevet til denne mappe. +optional_title = Valgfrie indstillinger +email_title = E-mail-indstillinger +smtp_addr = SMTP vært +smtp_port = SMTP port +smtp_from = Send e-mail som +smtp_from_invalid = "Send e-mail som"-adressen er ugyldig +mailer_user = SMTP brugernavn +mailer_password = SMTP adgangskode +register_confirm = Kræv e-mail-bekræftelse for at registrere +mail_notify = Aktiver e-mailmeddelelser +server_service_title = Server- og tredjeparts tjenesteindstillinger +offline_mode = Aktiver lokal tilstand +disable_gravatar = Deaktiver Gravatar +disable_gravatar.description = Deaktiver brug af Gravatar eller andre tredjeparts avatar kilder. Standardbilleder vil blive brugt til bruger avatarer, medmindre de uploader deres egen avatar til instansen. +federated_avatar_lookup = Aktiver fødererede avatarer +federated_avatar_lookup.description = Slå avatarer op ved hjælp af Libravatar. +disable_registration = Deaktiver selvregistrering +allow_only_external_registration = Tillad kun registrering via eksterne tjenester +allow_only_external_registration.description = Brugere vil kun være i stand til at oprette nye konti ved at bruge konfigurerede eksterne tjenester. +openid_signin = Aktiver OpenID-logon +openid_signin.description = Tillad brugere at logge ind via OpenID. +app_slogan = Instans slogan +repo_path_helper = Fjerne Git depoter vil blive gemt i denne mappe. +smtp_from_helper = E-mailadresse Forgejo vil bruge. Indtast en almindelig e-mailadresse, eller brug formatet "Navn" . +run_user_helper = Operativsystemets brugernavn, som Forgejo kører som. Bemærk, at denne bruger skal have adgang til depotets rodsti. +app_name_helper = Indtast dit instans-navn her. Det vil blive vist på hver side. +offline_mode.description = Deaktiver tredjeparts indholdsleverings netværk og server alle ressourcer lokalt. +disable_registration.description = Kun instans administratoren vil være i stand til at oprette nye brugerkonti. Det anbefales stærkt at holde registreringen deaktiveret, medmindre du har til hensigt at være vært for en offentlig instans for alle og klar til at håndtere store mængder spamkonti. +openid_signup = Aktiver OpenID-selvregistrering +sqlite_helper = Filsti til SQLite3-databasen.
Indtast en absolut sti, hvis du kører Forgejo som en tjeneste. +path = Sti +reinstall_confirm_message = Geninstallation med en eksisterende Forgejo-database kan forårsage flere problemer. I de fleste tilfælde bør du bruge din eksisterende "app.ini" til at køre Forgejo. Hvis du ved, hvad du laver, skal du bekræfte følgende: +reinstall_confirm_check_2 = Depoterne og indstillingerne skal muligvis synkroniseres igen. Ved at markere dette felt bekræfter du, at du vil re-synkronisere hooks for depoter og authorized_keys-filen manuelt. Du bekræfter, at du vil sikre dig, at indstillingerne for depoter og mirror er korrekte. +app_url_helper = Base adresse for HTTP(S)-klone-URL'er og e-mail-meddelelser. +enable_captcha = Aktiver registrering CAPTCHA +enable_captcha.description = Kræv brugere at bestå CAPTCHA for at oprette konti. +require_sign_in_view = Kræv at logge ind for at se instansindhold +default_keep_email_private = Skjul e-mailadresser som standard +default_keep_email_private.description = Aktiver skjulning af e-mail-adresser for nye brugere som standard, så disse oplysninger ikke lækkes umiddelbart efter tilmelding. +default_allow_create_organization = Tillad oprettelse af organisationer som standard +default_enable_timetracking = Aktiver tidsregistrering som standard +default_enable_timetracking.description = Tillad som standard brug af tidssporings-funktion for nye depoter. +admin_title = Indstillinger for administratorkonto +admin_setting.description = Det er valgfrit at oprette en administratorkonto. Den første registrerede bruger bliver automatisk administrator. +admin_name = Administrator brugernavn +admin_password = Adgangskode +confirm_password = Bekræft adgangskode +admin_email = E-mailadresse +config_location_hint = Disse konfigurationsmuligheder vil blive gemt i: +install_btn_confirm = Installer Forgejo +invalid_db_setting = Databaseindstillingerne er ugyldige: %v +invalid_db_table = Databasetabellen "%s" er ugyldig: %v +invalid_repo_path = Depotets rodsti er ugyldig: %v +invalid_app_data_path = Appens datasti er ugyldig: %v +run_user_not_match = Brugernavnet som "bruger det skal køres som" er ikke det aktuelle brugernavn: %s -> %s +internal_token_failed = Kunne ikke generere intern token: %v +secret_key_failed = Kunne ikke generere hemmelig nøgle: %v +save_config_failed = Konfigurationen kunne ikke gemmes: %v +invalid_admin_setting = Administratorkonto indstillingen er ugyldig: %v +invalid_log_root_path = Log stien er ugyldig: %v +allow_dots_in_usernames = Tillad brugere at bruge prikker i deres brugernavne. Påvirker ikke eksisterende konti. +no_reply_address = Skjult e-mail-domæne +invalid_password_algorithm = Ugyldig hash-algoritme for adgangskode +enable_update_checker = Aktiver opdateringskontrol +env_config_keys = Miljøkonfiguration +env_config_keys_prompt = Følgende miljøvariabler vil også blive anvendt på din konfigurationsfil: +test_git_failed = Kunne ikke teste "git" kommandoen: %v +sqlite3_not_available = Denne Forgejo-version understøtter ikke SQLite3. Download venligst den officielle binære version fra %s (ikke "gobuild"-versionen). +no_reply_address_helper = Domænenavn til brugere med en skjult e-mailadresse. For eksempel vil brugernavnet "joe" blive logget i Git som "joe@noreply.example.org", hvis det skjulte e-mail-domæne er sat til "noreply.example.org". +require_sign_in_view.description = Begræns indholdsadgang til ind-loggede brugere. Gæster vil kun kunne besøge autentificerings sider.. +default_allow_create_organization.description = Tillad nye brugere at oprette organisationer som standard. Når denne mulighed er deaktiveret, skal en administrator give tilladelse til at oprette organisationer til nye brugere. +password_algorithm = Adgangskode hash algoritme +enable_update_checker_helper_forgejo = Den vil med jævne mellemrum tjekke for nye Forgejo versioner ved at tjekke en TXT DNS-posten på release.forgejo.org. +password_algorithm_helper = Indstil adgangskode-hash-algoritmen. Algoritmer har forskellige krav og styrke. Argon2-algoritmen er ret sikker, men bruger meget hukommelse og kan være upassende til små systemer. +openid_signup.description = Tillad brugere at oprette konti via OpenID, hvis selvregistrering er aktiveret. + +[home] +uname_holder = Brugernavn eller e-mailadresse +switch_dashboard_context = Skift instrumentpanel-kontekst +my_repos = Depoter +my_orgs = Organisationer +view_home = Se %s +filter = Andre filtre +filter_by_team_repositories = Filtrer efter holddepoter +feed_of = Feed of "%s" +show_archived = Arkiveret +show_both_archived_unarchived = Viser både arkiveret og ikke-arkiveret +show_only_archived = Viser kun arkiverede +show_only_unarchived = Viser kun ikke-arkiveret +show_private = Privat +show_only_private = Viser kun privat +show_only_public = Viser kun offentligt +issues.in_your_repos = I dine depoter +show_both_private_public = Viser både offentlige og private + +[explore] +repos = Depoter +users = Brugere +stars_one = %d stjerne +stars_few = %d stjerner +forks_one = %d fork +forks_few = %d forks +organizations = Organisationer +code = Kode +code_last_indexed_at = Sidst indekseret %s +relevant_repositories = Kun relevante depoter vises, vis ufiltrerede resultater. +go_to = Gå til +relevant_repositories_tooltip = Depoter som er forks, eller som ikke har noget emne, intet ikon og ingen beskrivelse, er skjult. + +[auth] +create_new_account = Registrer konto +disable_register_prompt = Registrering er deaktiveret. Kontakt venligst din side-administrator. +disable_register_mail = E-mailbekræftelse for registrering er deaktiveret. +manual_activation_only = Kontakt din side-administrator for at fuldføre aktiveringen. +remember_me = Husk denne enhed +forgot_password_title = Glemt adgangskode +forgot_password = Glemt adgangskode? +hint_register = Har du brug for en konto? Registrer dig nu. +sign_up_button = Registrer nu. +sign_up_successful = Kontoen blev oprettet. Velkommen! +must_change_password = Opdater din adgangskode +allow_password_change = Kræv, at brugeren ændrer adgangskode (anbefales) +reset_password_mail_sent_prompt = En bekræftelses-e-mail er blevet sendt til %s. For at fuldføre kontogendannelses-processen skal du tjekke din indbakke og følge det medfølgende link inden for de næste %s. +active_your_account = Aktiver din konto +account_activated = Kontoen er blevet aktiveret +prohibit_login = Kontoen er suspenderet +prohibit_login_desc = Din konto er blevet suspenderet fra interaktion med instansen. Kontakt instans-administratoren for at få adgang igen. +change_unconfirmed_email_summary = Skift den e-mailadresse, aktiveringsmail sendes til. +change_unconfirmed_email_error = Kan ikke ændre e-mailadressen: %v +resend_mail = Klik her for at sende din aktiverings-e-mail igen +send_reset_mail = Send gendannelses-e-mail +reset_password = Kontogendannelse +invalid_code = Din bekræftelseskode er ugyldig eller er udløbet. +invalid_code_forgot_password = Din bekræftelseskode er ugyldig eller er udløbet. Klik her for at starte en ny session. +invalid_password = Din adgangskode stemmer ikke overens med den adgangskode, der blev brugt til at oprette kontoen. +reset_password_helper = Gendan konto +resent_limit_prompt = Du har allerede anmodet om en aktiverings-e-mail for nylig. Vent venligst 3 minutter, og prøv igen. +reset_password_wrong_user = Du er logget ind som %s, men linket til kontogendannelse er beregnet til %s +confirmation_mail_sent_prompt = En ny bekræftelses-e-mail er blevet sendt til %s. For at fuldføre registreringsprocessen skal du tjekke din indbakke og følge det medfølgende link inden for de næste %s. Hvis e-mailen er forkert, kan du logge ind og anmode om, at endnu en bekræftelses-e-mail sendes til en anden adresse. +change_unconfirmed_email = Hvis du har opgivet den forkerte e-mailadresse under tilmeldingen, kan du ændre den nedenfor, og der vil i stedet blive sendt en bekræftelse til den nye adresse. +hint_login = Har du allerede en konto? Log ind nu! +has_unconfirmed_mail = Hej %s, du har en ubekræftet e-mailadresse (%s). Hvis du ikke har modtaget en bekræftelses-e-mail eller har brug for at sende en ny, bedes du klikke på knappen nedenfor. +password_too_short = Adgangskodelængden må ikke være mindre end %d tegn. +non_local_account = Ikke-lokale brugere kan ikke opdatere deres adgangskode via Forgejo-webgrænsefladen. +verify = Verificere +unauthorized_credentials = Legitimationsoplysningerne er forkerte eller er udløbet. Prøv din kommando igen, eller se %s for at få flere oplysninger +scratch_code = Skrabekode +use_scratch_code = Brug en skrabekode +use_onetime_code = Brug en engangskode +twofa_scratch_used = Du har brugt din skrabekode. Du er blevet omdirigeret til siden med to-faktorindstillinger, så du kan fjerne din enhedstilmelding eller generere en ny skrabekode. +twofa_passcode_incorrect = Din passcode er forkert. Hvis du har forlagt din enhed, skal du bruge din skrabekode til at logge ind. +twofa_scratch_token_incorrect = Din skrabekode er forkert. +login_userpass = Login +oauth_signup_tab = Registrer ny konto +oauth_signup_title = Fuldfør ny konto +oauth_signup_submit = Gennemfør konto +oauth_signin_tab = Link til en eksisterende konto +oauth_signin_title = Log ind for at godkende linket konto +oauth_signin_submit = Link konto +oauth.signin.error = Der opstod en fejl under behandling af godkendelsesanmodningen. Hvis denne fejl fortsætter, bedes du kontakte webstedets administrator. +oauth.signin.error.access_denied = Godkendelsesanmodningen blev afvist. +oauth.signin.error.temporarily_unavailable = Godkendelse mislykkedes, fordi godkendelsesserveren midlertidigt ikke er tilgængelig. Prøv venligst igen senere. +openid_connect_submit = Forbind +openid_connect_title = Opret forbindelse til en eksisterende konto +openid_register_title = Opret ny konto +openid_register_desc = Den valgte OpenID URI er ukendt. Knyt den til en ny konto her. +disable_forgot_password_mail = Kontogendannelse er deaktiveret, fordi der ikke er konfigureret nogen e-mail. Kontakt venligst din webstedsadministrator. +email_domain_blacklisted = Du kan ikke registrere dig med din e-mailadresse. +authorize_application = Godkend Applikation +authorize_redirect_notice = Du vil blive omdirigeret til %s, hvis du godkender denne applikation. +authorize_application_created_by = Denne applikation blev oprettet af %s. +authorize_application_description = Hvis du giver adgangen, vil den være i stand til at få adgang til og skrive til alle dine kontooplysninger, inklusive private depoter og organisationer. +authorize_title = Tillad "%s" at få adgang til din konto? +authorization_failed = Godkendelse mislykkedes +password_pwned_err = Kunne ikke fuldføre anmodningen til HaveIBeenPwned +last_admin = Du kan ikke fjerne den sidste admin. Der skal være mindst én administrator. +back_to_sign_in = Tilbage til Log ind +sign_in_openid = Fortsæt med OpenID +openid_signin_desc = Indtast din OpenID URI. For eksempel: alice.openid.example.org eller https://openid.example.org/alice. +disable_forgot_password_mail_admin = Kontogendannelse er kun tilgængelig, når e-mail er konfigureret. Konfigurer venligst e-mail for at aktivere kontogendannelse. +password_pwned = Den adgangskode, du valgte, er på en liste over stjålne adgangskoder, der tidligere er blevet afsløret i forbindelse med offentlige databrud. Prøv venligst igen med en anden adgangskode, og overvej også at ændre denne adgangskode et andet sted. +openid_connect_desc = Den valgte OpenID URI er ukendt. Knyt den til en ny konto her. +authorization_failed_desc = Godkendelsen mislykkedes, fordi vi har registreret en ugyldig anmodning. Kontakt venligst vedligeholderen af den app, du har forsøgt at godkende. + +[mail] +view_it_on = Se det på %s +reply = eller svar direkte på denne e-mail +link_not_working_do_paste = Virker linket ikke? Prøv at kopiere og indsætte det i din browsers URL-linje. +hi_user_x = Hej %s, +activate_account = Aktiver venligst din konto +activate_account.text_1 = Hej %[1]s, tak, fordi du registrerede dig hos %[2]s! +activate_account.text_2 = Klik venligst på følgende link for at aktivere din konto inden for %s: +activate_email = Bekræft din e-mailadresse +admin.new_user.subject = Ny bruger %s har lige tilmeldt sig +admin.new_user.user_info = Brugeroplysninger +admin.new_user.text = Venligst klik her for at administrere denne bruger fra administrationspanelet. +register_notify = Velkommen til %s +register_notify.text_1 = dette er din registreringsbekræftelses-e-mail for %s! +register_notify.text_3 = Hvis en anden har lavet denne konto for dig, skal du først indstille din adgangskode. +reset_password = Gendan din konto +password_change.subject = Din adgangskode er blevet ændret +password_change.text_1 = Adgangskoden til din konto er lige blevet ændret. +primary_mail_change.subject = Din primære mail er blevet ændret +totp_disabled.subject = TOTP er blevet deaktiveret +totp_disabled.text_1 = Tidsbaseret engangsadgangskode (TOTP) på din konto er netop blevet deaktiveret. +totp_disabled.no_2fa = Der er ikke længere konfigureret andre 2FA-metoder, hvilket betyder, at det ikke længere er nødvendigt at logge ind på din konto hos 2FA. +removed_security_key.subject = En sikkerhedsnøgle er blevet fjernet +removed_security_key.text_1 = Sikkerhedsnøglen "%[1]s" er lige blevet fjernet fra din konto. +account_security_caution.text_1 = Hvis dette var dig, så kan du roligt ignorere denne mail. +totp_enrolled.subject = Du har aktiveret TOTP som 2FA-metode +totp_enrolled.text_1.has_webauthn = Du har lige aktiveret TOTP for din konto. Dette betyder, at du for alle fremtidige login til din konto kan bruge TOTP som en 2FA-metode eller bruge en af dine sikkerhedsnøgler. +register_success = Registreringen lykkedes +issue_assigned.pull = @%[1]s har tildelt dig at trække anmodning %[2]s i depotet %[3]s. +issue_assigned.issue = @%[1]s har tildelt dig et problem %[2]s i depotet %[3]s. +register_notify.text_2 = Du kan logge ind på din konto med dit brugernavn: %s +primary_mail_change.text_1 = Din kontos primære mail er lige blevet ændret til %[1]s. Det betyder, at denne e-mailadresse ikke længere vil modtage e-mail-meddelelser for din konto. +account_security_caution.text_2 = Hvis dette ikke var dig, er din konto kompromitteret. Kontakt venligst administratorerne af dette websted. +activate_email.text = Klik venligst på følgende link for at bekræfte din e-mailadresse inden for %s: +reset_password.text = Hvis dette var dig, skal du klikke på følgende link for at gendanne din konto inden for %s: +removed_security_key.no_2fa = Der er ikke længere konfigureret andre 2FA-metoder, hvilket betyder, at det ikke længere er nødvendigt at logge ind på din konto hos 2FA. +totp_enrolled.text_1.no_webauthn = Du har lige aktiveret TOTP for din konto. Det betyder, at du for alle fremtidige login til din konto skal bruge TOTP som 2FA-metode. +issue.x_mentioned_you = @%s nævnte dig: +issue.action.force_push = %[1]s tvangs pushed %[2]s fra %[3]s til %[4]s. +issue.action.push_1 = @%[1]s pushed %[3]d commit til %[2]s +issue.action.push_n = @%[1]s pushed %[3]d commits til %[2]s +issue.action.close = @%[1]s lukket #%[2]d. +issue.action.reopen = @%[1]s genåbnet #%[2]d. +issue.action.merge = @%[1]s merged #%[2]d ind i %[3]s. +issue.action.approve = @%[1]s godkendte denne pull-anmodning. +issue.action.reject = @%[1]s anmodede om ændringer på denne pull-anmodning. +issue.action.review = @%[1]s kommenterede denne pull-anmodning. +issue.action.review_dismissed = @%[1]s afviste den seneste kontrol fra %[2]s for denne pull-anmodning. +issue.action.ready_for_review = @%[1]s markerede denne pull-anmodning klar til gennemgang. +issue.action.new = @%[1]s oprettede #%[2]d. +issue.in_tree_path = I %s: +release.new.subject = %s i %s udgivet +release.new.text = @%[1]s udgivet %[2]s i %[3]s +release.title = Title: %s +release.note = Note: +release.downloads = Downloads: +release.download.zip = Kildekode (ZIP) +release.download.targz = Kildekode (TAR.GZ) +repo.transfer.subject_to = %s ønsker at overføre depotet "%s" til %s +repo.transfer.subject_to_you = %s ønsker at overføre depotet "%s" til dig +repo.transfer.to_you = dig +repo.transfer.body = For at acceptere eller afvise det, besøg %s eller ignorer det. +repo.collaborator.added.subject = %s føjede dig til %s som samarbejdspartner +repo.collaborator.added.text = Du er blevet tilføjet som samarbejdspartner til depotet: +team_invite.subject = %[1]s har inviteret dig til at deltage i %[2]s organisationen +team_invite.text_1 = %[1]s har inviteret dig til at deltage i teamet %[2]s i organisationen %[3]s. +team_invite.text_2 = Klik venligst på følgende link for at blive medlem af holdet: +team_invite.text_3 = Note: Denne invitation var beregnet til %[1]s. Hvis du ikke forventede denne invitation, kan du ignorere denne e-mail. + +[modal] +yes = Ja +no = Nej +confirm = Bekærft +cancel = Annuller +modify = Updatere + +[form] +UserName = Brugernavn +FullName = Fulde navn +Description = Beskrivelse +Pronouns = Stedord +Biography = Biografi +Website = Websted +Location = Lokation +RepoName = Depot navn +Email = E-mailadresse +Password = Adgangskode +Retype = Bekræft adgangskode +PayloadUrl = Payload URL +TeamName = Holdnavn +AuthName = Autorisationsnavn +AdminEmail = Admin email +To = Gren navn +AccessToken = Adgangstoken +NewBranchName = Nyt gren navn +CommitSummary = Commit oversigt +CommitMessage = Commit besked +CommitChoice = Commit valg +TreeName = Fil sti +Content = Indhold +SSPISeparatorReplacement = Separator +SSPIDefaultLanguage = Standard sprog +require_error = ` må ikke være tomt.` +alpha_dash_error = ` bør kun indeholde alfanumeriske, bindestreg ("-") og understregningstegn ("_").` +alpha_dash_dot_error = ` bør kun indeholde alfanumeriske tegn, bindestreg ("-"), understregning ("_") og prik ("".").` +git_ref_name_error = ` skal være et veludformet Git-referencenavn.` +size_error = ` skal være størrelse %s.` +min_size_error = ` skal indeholde mindst %s tegn.` +email_error = ` er ikke en gyldig e-mailadresse.` +url_error = `"%s" er ikke en gyldig URL.` +include_error = ` skal indeholde understreng "%s".` +glob_pattern_error = ` globmønster er ugyldigt: %s.` +regex_pattern_error = ` regex-mønster er ugyldigt: %s.` +invalid_group_team_map_error = ` mapping er ugyldig: %s` +unknown_error = Ukendt fejl: +captcha_incorrect = CAPTCHA-koden er forkert. +password_not_match = Adgangskoderne stemmer ikke overens. +lang_select_error = Vælg et sprog fra listen. +username_been_taken = Brugernavnet er allerede taget. +username_change_not_local_user = Ikke-lokale brugere må ikke ændre deres brugernavn. +repo_name_been_taken = Depotnavnet er allerede brugt. +repository_force_private = Tving Privat er aktiveret: private depoter kan ikke gøres offentlige. +repository_files_already_exist = Der findes allerede filer for dette depot. Kontakt systemadministratoren. +repository_files_already_exist.delete = Der findes allerede filer for dette depot. Du skal slette dem. +visit_rate_limit = Fjernbesøg adresseret takstbegrænsning. +2fa_auth_required = Fjernbesøg krævede godkendelse af to faktorer. +org_name_been_taken = Organisationsnavnet er allerede taget. +team_name_been_taken = Holdnavnet er allerede taget. +team_no_units_error = Tillad adgang til mindst én depotsektion. +email_been_used = E-mailadressen er allerede brugt. +email_invalid = E-mailadressen er ugyldig. +openid_been_used = OpenID-adressen "%s" er allerede brugt. +password_complexity = Adgangskoden opfylder ikke kompleksitetskravene: +password_lowercase_one = Mindst ét lille bogstav +password_uppercase_one = Mindst ét stort tegn +password_digit_one = Mindst ét ciffer +enterred_invalid_repo_name = Det depotnavn, du indtastede, er forkert. +enterred_invalid_org_name = Det organisationsnavn, du har indtastet, er forkert. +enterred_invalid_owner_name = Det nye ejernavn er ikke gyldigt. +enterred_invalid_password = Den adgangskode, du indtastede, er forkert. +unset_password = Login-brugeren har ikke angivet adgangskoden. +user_not_exist = Brugeren eksisterer ikke. +team_not_exist = Holdet eksisterer ikke. +last_org_owner = Du kan ikke fjerne den sidste bruger fra "ejere"-teamet. Der skal være mindst én ejer for en organisation. +duplicate_invite_to_team = Brugeren var allerede inviteret som et holdmedlem. +organization_leave_success = Du har forladt organisationen %s. +invalid_ssh_key = Kan ikke bekræfte din SSH-nøgle: %s +invalid_gpg_key = Kan ikke bekræfte din GPG-nøgle: %s +invalid_ssh_principal = Ugyldig principal: %s +unable_verify_ssh_key = Kan ikke bekræfte SSH-nøglen, dobbelttjek den for fejl. +auth_failed = Godkendelse mislykkedes: %v +still_own_repo = Din konto ejer et eller flere depoter, slet eller overfør dem først. +still_has_org = Din konto er medlem af en eller flere organisationer, forlad dem først. +still_own_packages = Din konto ejer en eller flere pakker, slet dem først. +org_still_own_packages = Denne organisation ejer stadig en eller flere pakker, slet dem først. +target_branch_not_exist = Gren målet eksisterer ikke. +admin_cannot_delete_self = Du kan ikke slette dig selv, når du er administrator. Fjern venligst dine administratorrettigheder først. +required_prefix = Input skal starte med "%s" +username_error = ` kan kun indeholde alfanumeriske tegn ("0-9","a-z","A-Z"), bindestreg ("-"), understregning ("_") og prik ("."). Det kan ikke begynde eller slutte med ikke-alfanumeriske tegn, og på hinanden følgende ikke-alfanumeriske tegn er også forbudt.` +max_size_error = ` må højst indeholde %s tegn.` +repository_files_already_exist.adopt_or_delete = Der findes allerede filer for dette depot. Enten adopter dem eller slet dem. +org_still_own_repo = Denne organisation ejer stadig et eller flere depoter, slet eller overfør dem først. +username_error_no_dots = ` kan kun indeholde alfanumeriske tegn ("0-9","a-z","A-Z"), bindestreg ("-") og understregning ("_"). Det kan ikke begynde eller slutte med ikke-alfanumeriske tegn, og på hinanden følgende ikke-alfanumeriske tegn er også forbudt.` +username_password_incorrect = Brugernavn eller adgangskode er forkert. +repository_files_already_exist.adopt = Filer findes allerede for dette depot og kan kun adopteres. +password_special_one = Mindst ét specialtegn (tegnsætning, parenteser, anførselstegn osv.) +unsupported_login_type = Login typen understøttes ikke for at slette kontoen. +cannot_add_org_to_team = En organisation kan ikke tilføjes som et holdmedlem. +must_use_public_key = Nøglen du har angivet er en privat nøgle. Lad være med at uploade din private nøgle nogen steder. Brug din offentlige nøgle i stedet. + +[user] +change_avatar = Skift din avatar… +joined_on = Tilmeldte sig den %s +repositories = Depoter +activity = Offentlig aktivitet +followers.title.one = Følger +followers.title.few = Følgere +following.title.one = Følger +following.title.few = Følger +followers_one = %d følger +followers_few = %d følgere +following_one = %d følger +following_few = %d følger +follow = Følg +unfollow = Fjern følg +block_user = Bloker bruger +block_user.detail = Bemærk venligst, at blokering af en bruger har andre effekter, såsom: +block_user.detail_1 = I vil holde op med at følge hinanden og vil ikke være i stand til at følge hinanden. +block_user.detail_2 = Denne bruger vil ikke være i stand til at interagere med de depoter, du ejer, eller de problemer og kommentarer, du har oprettet. +block_user.detail_3 = I vil ikke være i stand til at tilføje hinanden som depot-samarbejdspartnere. +follow_blocked_user = Du kan ikke følge denne bruger, fordi du har blokeret denne bruger, eller denne bruger har blokeret dig. +starred = Stjernemarkerede depoter +watched = Overvågede depoter +code = Kode +overview = Oversigt +block = Block +user_bio = Biografi +email_visibility.limited = Din e-mailadresse er synlig for alle godkendte brugere +show_on_map = Vis dette sted på et kort +settings = Brugerindstillinger +public_activity.visibility_hint.admin_public = Denne aktivitet er synlig for alle, men som administrator kan du også se interaktioner i private rum. +public_activity.visibility_hint.self_private = Din aktivitet er kun synlig for dig og instans-administratorerne. Konfigurer. +public_activity.visibility_hint.admin_private = Denne aktivitet er synlig for dig, fordi du er administrator, men brugeren ønsker, at den forbliver privat. +form.name_reserved = Brugernavnet "%s" er reserveret. +form.name_pattern_not_allowed = Mønsteret "%s" er ikke tilladt i et brugernavn. +form.name_chars_not_allowed = Brugernavnet "%s" indeholder ugyldige tegn. +unblock = Fjern blokering +projects = Projekter +disabled_public_activity = Denne bruger har deaktiveret den offentlige synlighed af aktiviteten. +public_activity.visibility_hint.self_public = Din aktivitet er synlig for alle, undtagen interaktioner i private rum. Konfigurer. +public_activity.visibility_hint.self_private_profile = Din aktivitet er kun synlig for dig og instans-administratorerne, fordi din profil er privat. Konfigurer. + +[settings] +profile = Profil +account = Konto +appearance = Udseende +password = Adgangskode +security = Sikkerhed +avatar = Avatar +ssh_gpg_keys = SSH / GPG nøgler +applications = Applikationer +orgs = Organisationer +repos = Depoter +delete = Slet konto +twofa = To-faktor-godkendelse (TOTP) +organization = Organisationer +uid = UID +webauthn = To-faktor-godkendelse (sikkerhedsnøgler) +blocked_users = Blokerede brugere +public_profile = Offentlig profil +location_placeholder = Del din omtrentlige placering med andre +password_username_disabled = Ikke-lokale brugere må ikke ændre deres brugernavn. Kontakt venligst din webstedsadministrator for flere detaljer. +full_name = Fulde navn +website = Websted +location = Lokation +pronouns = Stedord +pronouns_custom = Brugerdefineret +pronouns_unspecified = Uspecificeret +update_theme = Skift tema +update_language = Skift sprog +update_language_success = Sproget er blevet opdateret. +update_profile_success = Din profil er blevet opdateret. +change_username = Dit brugernavn er blevet ændret. +change_username_redirect_prompt = Det gamle brugernavn vil omdirigere, indtil nogen gør krav på det. +continue = Fortsæt +cancel = Annuller +language = Sprog +language.title = Standard sprog +language.description = Dette sprog gemmes på din konto og bruges som standard, når du logger ind. +ui = Tema +additional_repo_units_hint = Foreslå at aktivere yderligere depotenheder +additional_repo_units_hint_description = Vis et "Aktiver mere"-tip for depoter, der ikke har alle tilgængelige enheder aktiveret. +update_hints = Opdater tips +hints = Tips +update_hints_success = Tips er blevet opdateret. +hidden_comment_types = Skjulte kommentartyper +hidden_comment_types.ref_tooltip = Kommentarer, hvor dette problem blev refereret fra en anden problem/commit/… +hidden_comment_types.issue_ref_tooltip = Kommentarer, hvor brugeren ændrer grenen/taget, der er knyttet til problemet +comment_type_group_reference = Reference +comment_type_group_label = Etiket +comment_type_group_milestone = Milepæl +comment_type_group_assignee = Udpegningsmodtager +comment_type_group_title = Title +comment_type_group_branch = Gren +comment_type_group_time_tracking = Tidsregistrering +comment_type_group_deadline = Tidsfrist +comment_type_group_dependency = Dependency +comment_type_group_lock = Lås status +comment_type_group_review_request = Gennemgå anmodning +comment_type_group_pull_request_push = Tilføjet commits +comment_type_group_project = Projekt +comment_type_group_issue_ref = Problem henvisning +privacy = Privatliv +keep_activity_private = Skjul aktivitet fra profilsiden +lookup_avatar_by_mail = Slå avatar up efter e-mailadresse +enable_custom_avatar = Brug tilpasset avatar +delete_current_avatar = Slet nuværende avatar +uploaded_avatar_not_a_image = Den uploadede fil er ikke et billede. +update_avatar_success = Din avatar er blevet opdateret. +update_user_avatar_success = Brugerens avatar er blevet opdateret. +change_password = Skift adgangskode +update_password = Opdater adgangskode +old_password = Nuværende adgangskode +new_password = Ny adgangskode +retype_new_password = Bekræft ny adgangskode +password_incorrect = Den aktuelle adgangskode er forkert. +password_change_disabled = Ikke-lokale brugere kan ikke opdatere deres adgangskode via Forgejo-webgrænsefladen. +manage_emails = Administrer e-mailadresser +manage_themes = Standard tema +manage_openid = OpenID-adresser +theme_desc = Dette tema vil blive brugt til webgrænsefladen, når du er logget ind. +primary = Primær +activated = Aktiveret +requires_activation = Kræver aktivering +primary_email = Gør til primær +activate_email = Send aktivering +activations_pending = Aktiveringer afventer +can_not_add_email_activations_pending = Der er en afventende aktivering, prøv igen om et par minutter, hvis du vil tilføje en ny e-mail. +delete_email = Slet +email_deletion = Fjern e-mailadresse +email_deletion_success = E-mailadressen er blevet fjernet. +theme_update_success = Dit tema blev opdateret. +theme_update_error = Det valgte tema findes ikke. +openid_deletion = Fjern OpenID-adresse +openid_deletion_desc = Fjernelse af denne OpenID-adresse fra din konto vil forhindre dig i at logge ind med den. Fortsætte? +openid_deletion_success = OpenID-adressen er blevet fjernet. +add_new_email = Tilføj e-mailadresse +add_new_openid = Tilføj ny OpenID URI +language.localization_project = Hjælp os med at oversætte Forgejo til dit sprog! Få flere oplysninger. +update_language_not_found = Sprog "%s" er ikke tilgængeligt. +hidden_comment_types_description = Kommentartyper, der er markeret her, vil ikke blive vist på problemsider. Hvis du f.eks. markerer "Etiket", fjernes alle " tilføjede/fjernede " kommentarer. +update_profile = Opdater profil +change_username_prompt = Note: Ændring af dit brugernavn ændrer også din konto-URL. +biography_placeholder = Fortæl andre lidt om dig selv! (Markdown understøttes) +profile_desc = Styr, hvordan din profil vises til andre brugere. Din primære e-mailadresse vil blive brugt til meddelelser, gendannelse af adgangskode og webbaserede Git-operationer. +saved_successfully = Dine indstillinger blev gemt. +keep_activity_private.description = Din offentlige aktivitet vil kun være synlig for dig og instans-administratorerne. +email_desc = Din primære e-mailadresse vil blive brugt til meddelelser, gendannelse af adgangskode og, forudsat at den ikke er skjult, webbaserede Git-operationer. +uploaded_avatar_is_too_big = Den uploadede filstørrelse (%d KiB) overstiger den maksimale størrelse (%d KiB). +email_deletion_desc = E-mailadressen og relaterede oplysninger vil blive fjernet fra din konto. Git commits af denne e-mailadresse forbliver uændret. Fortsætte? +choose_new_avatar = Vælg ny avatar +update_avatar = Opdater avatar +change_password_success = Din adgangskode er blevet opdateret. Log ind med din nye adgangskode fra nu af. +add_email = Tilføj e-mailadresse +add_openid = Tilføj OpenID URI +add_email_confirmation_sent = En bekræftelses-e-mail er blevet sendt til "%s". For at bekræfte din e-mailadresse, tjek venligst din indbakke og følg det medfølgende link inden for de næste %s. +add_openid_success = Den nye OpenID-adresse er blevet tilføjet. +keep_email_private = Skjul e-mailadresse +openid_desc = OpenID lader dig uddelegere godkendelse til en ekstern udbyder. +manage_ssh_keys = Administrer SSH-nøgler +manage_ssh_principals = Administrer SSH-certifikatprincippere +manage_gpg_keys = Administrer GPG-nøgler +add_key = Tilføj nøgle +ssh_desc = Disse offentlige SSH-nøgler er knyttet til din konto. De tilsvarende private nøgler giver fuld adgang til dine depoter. SSH-nøgler, der er blevet bekræftet, kan bruges til at bekræfte SSH-signerede Git-commits. +principal_desc = Disse SSH-certifikatprincipper er knyttet til din konto og giver fuld adgang til dine depoter. +ssh_helper = Har du brug for hjælp? Se vejledningen til at oprette dine egne SSH-nøgler eller løse almindelige problemer a> du kan støde på når du bruger SSH. +gpg_helper = Har du brug for hjælp? Se vejledningen om GPG. +key_content_gpg_placeholder = Begynder med "-----BEGIN PGP PUBLIC KEY BLOCK-----" +add_new_principal = Tilføj principal +ssh_key_been_used = Denne SSH-nøgle er allerede blevet tilføjet til serveren. +ssh_key_name_used = En SSH-nøgle med samme navn findes allerede på din konto. +ssh_principal_been_used = Denne principal er allerede blevet tilføjet til serveren. +gpg_key_id_used = Der findes allerede en offentlig GPG-nøgle med samme id. +gpg_no_key_email_found = Denne GPG-nøgle matcher ikke nogen aktiveret e-mail-adresse tilknyttet din konto. Det kan stadig tilføjes, hvis du underskriver det medfølgende token. +gpg_key_matched_identities = Matchede identiteter: +gpg_key_verified = Verificeret nøgle +gpg_key_verified_long = Nøglen er blevet bekræftet med et token og kan bruges til at bekræfte commits, der matcher alle aktiverede e-mailadresser for denne bruger ud over eventuelle matchede identiteter for denne nøgle. +gpg_key_verify = Bekræft +gpg_invalid_token_signature = Den medfølgende GPG-nøgle, signatur og token stemmer ikke overens, eller token er forældet. +gpg_token_required = Du skal angive en underskrift for nedenstående token +gpg_token = Token +gpg_token_signature = Pansret GPG-signatur +key_signature_gpg_placeholder = Begynder med "-----BEGIN PGP SIGNATURE-----" +verify_gpg_key_success = GPG-nøglen "%s" er blevet bekræftet. +ssh_key_verified = Verificeret nøgle +ssh_token_required = Du skal angive en underskrift for nedenstående token +ssh_token = Token +ssh_token_help = Du kan generere en signatur ved at bruge: +ssh_token_signature = Pansret SSH signatur +key_signature_ssh_placeholder = Begynder med "-----BEGIN SSH SIGNATURE-----" +verify_ssh_key_success = SSH-nøglen "%s" er blevet bekræftet. +subkeys = Undernøgler +key_name = Nøglenavn +key_id = Nøgle ID +key_content = Indhold +principal_content = Indhold +add_key_success = SSH-nøglen "%s" er blevet tilføjet. +add_principal_success = SSH-certifikatprincippet "%s" er blevet tilføjet. +delete_key = Slet +ssh_key_deletion = Slet SSH-nøgle +gpg_key_deletion = Slet GPG-nøglen +ssh_principal_deletion = Slet SSH Certificate Principal +gpg_key_deletion_desc = Fjernelse af en GPG-nøgle afbekræfter commits, der er underskrevet af den. Fortsætte? +ssh_principal_deletion_desc = Fjernelse af en SSH Certificate Principal tilbagekalder dens adgang til din konto. Fortsætte? +ssh_key_deletion_success = SSH-nøglen er blevet fjernet. +ssh_principal_deletion_success = Principal er blevet fjernet. +added_on = Tilføjet den %s +valid_until_date = Gyldig indtil %s +last_used = Sidst brugt på +no_activity = Ingen nylig aktivitet +can_read_info = Læs +can_write_info = Skriv +token_state_desc = Dette token er blevet brugt inden for de sidste 7 dage +principal_state_desc = Denne principal er blevet brugt inden for de sidste 7 dage +show_openid = Vis på profil +hide_openid = Skjul fra profil +ssh_externally_managed = Denne SSH-nøgle administreres eksternt for denne bruger +manage_access_token = Adgangstoken +generate_new_token = Generer nyt token +tokens_desc = Disse tokens giver adgang til din konto ved hjælp af Forgejo API. +token_name = Token navn +generate_token = Generer token +generate_token_success = Dit nye token er blevet genereret. Kopier den nu, da den ikke vises igen. +delete_token = Slet +access_token_deletion = Slet adgangstoken +access_token_deletion_desc = Sletning af et token vil tilbagekalde adgangen til din konto for applikationer, der bruger den. Dette kan ikke fortrydes. Fortsætte? +repo_and_org_access = Depot og organisationsadgang +permissions_public_only = Kun offentlig +permissions_access_all = Alle (offentlige, private og begrænsede) +select_permissions = Vælg tilladelser +permission_no_access = Ingen adgang +permission_read = Læs +permission_write = Læs og skriv +permissions_list = Tilladelser: +manage_oauth2_applications = Administrer OAuth2-applikationer +edit_oauth2_application = Rediger OAuth2-applikation +add_email_success = Den nye e-mailadresse er blevet tilføjet. +key_content_ssh_placeholder = Begynder med "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ecdsa-sha2-nistp256@openssh.com", eller "sk-ssh-ed25519@openssh.com" +gpg_key_matched_identities_long = De indlejrede identiteter i denne nøgle matcher de følgende aktiverede e-mailadresser for denne bruger. Commits, der matcher disse e-mailadresser, kan bekræftes med denne nøgle. +gpg_key_deletion_success = GPG-nøglen er blevet fjernet. +email_preference_set_success = E-mail-præference er blevet indstillet. +keep_email_private_popup = Dette vil skjule din e-mailadresse fra din profil. Det vil ikke længere være standard for commits foretaget via webgrænsefladen, såsom filupload og redigeringer, og vil ikke blive brugt til merge commits. I stedet kan en speciel adresse %s bruges til at knytte commits til din konto. Bemærk, at ændring af denne mulighed ikke vil påvirke eksisterende commits. +gpg_desc = Disse offentlige GPG-nøgler er knyttet til din konto og bruges til at bekræfte dine commits. Opbevar dine private nøgler sikkert, da de giver dig mulighed for at underskrive commits med din identitet. +gpg_token_help = Du kan generere en signatur ved at bruge: +ssh_key_verified_long = Nøglen er blevet bekræftet med et token og kan bruges til at bekræfte commits, der matcher enhver aktiveret e-mail-adresse for denne bruger. +ssh_key_deletion_desc = Fjernelse af en SSH-nøgle tilbagekalder dens adgang til din konto. Fortsætte? +ssh_signonly = SSH er i øjeblikket deaktiveret, så disse nøgler bruges kun til bekræftelse af commit signatur. +at_least_one_permission = Du skal vælge mindst én tilladelse for at oprette et token +ssh_key_verify = Bekræft +ssh_invalid_token_signature = Den angivne SSH-nøgle, signatur eller token stemmer ikke overens, eller token er forældet. +add_gpg_key_success = GPG-nøglen "%s" er blevet tilføjet. +valid_forever = Gyldig for evigt +ssh_disabled = SSH er deaktiveret +key_state_desc = Denne nøgle er blevet brugt inden for de sidste 7 dage +generate_token_name_duplicate = %s er allerede blevet brugt som applikationsnavn. Brug venligst en ny. +delete_token_success = Tokenet er blevet slettet. Applikationer, der bruger den, har ikke længere adgang til din konto. +access_token_desc = Valgte tokentilladelser begrænser kun autorisation til de tilsvarende API-ruter. Læs dokumentationen for at få flere oplysninger. +oauth2_applications_desc = OAuth2-applikationer gør det muligt for din tredjepartsapplikation at godkende brugere sikkert i denne Forgejo-instans. +remove_oauth2_application = Slet OAuth2-applikation +remove_oauth2_application_desc = Sletning af en OAuth2-applikation vil tilbagekalde adgangen til alle signerede adgangstokens. Vil du fortsætte? +remove_oauth2_application_success = Ansøgningen er blevet slettet. +create_oauth2_application = Opret en ny OAuth2-applikation +create_oauth2_application_button = Opret applikation +create_oauth2_application_success = Du har oprettet en ny OAuth2-applikation. +update_oauth2_application_success = Du har opdateret OAuth2-applikationen. +oauth2_application_name = Applikationsnavn +oauth2_redirect_uris = Omdiriger URI'er. Brug venligst en ny linje for hver URI. +save_application = Gem +oauth2_client_id = Klient ID +oauth2_client_secret = Klient hemmelighed +oauth2_regenerate_secret = Genskab hemmeligheden +oauth2_regenerate_secret_hint = Har du mistet din hemmelighed? +oauth2_client_secret_hint = Hemmeligheden vil ikke blive vist igen, når du forlader eller opdaterer denne side. Sørg for, at du har gemt den. +oauth2_application_edit = Redigere +oauth2_confidential_client = Fortrolig klient. Vælg for apps, der holder hemmeligheden fortrolig, såsom webapps. Vælg ikke for indbyggede apps, herunder desktop- og mobilapps. \ No newline at end of file diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 12c241accc..2b28ab13af 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -814,7 +814,7 @@ manage_emails=E-Mail-Adressen verwalten manage_themes=Standard-Theme manage_openid=OpenID-Adressen email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. -theme_desc=Dies wird dein Standard-Theme auf der Seite sein. +theme_desc=Dieses Thema wird für die Weboberfläche verwendet, wenn du angemeldet bist. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -1097,7 +1097,7 @@ issue_labels=Labels issue_labels_helper=Wähle eine Label-Sammlung license=Lizenz license_helper=Wähle eine Lizenz -license_helper_desc=Eine Lizenz regelt, was andere mit deinem Code tun (oder nicht tun) können. Unsicher, welches für dein Projekt die Richtige ist? Siehe Choose a license. +license_helper_desc=Eine Lizenz regelt, was andere mit deinem Code tun (oder nicht tun) können. Unsicher, welches für dein Projekt die Richtige ist? Siehe Choose a license. readme=README readme_helper=Wähle eine README-Vorlage readme_helper_desc=Hier kannst du eine komplette Beschreibung für dein Projekt schreiben. @@ -2490,8 +2490,8 @@ settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibge settings.archive.success=Das Repo wurde erfolgreich archiviert. settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehler aufgetreten. Weitere Details finden sich im Log. settings.archive.error_ismirror=Du kannst kein gespiegeltes Repo archivieren. -settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar wenn das Repo archiviert ist. -settings.archive.tagsettings_unavailable=Tag Einstellungen sind nicht verfügbar, wenn das Repo archiviert wurde. +settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar in archivierten Repos. +settings.archive.tagsettings_unavailable=Tag-Einstellungen sind nicht verfügbar in archivierten Repos. settings.unarchive.button=Archivierung zurücksetzen settings.unarchive.header=Archivierung dieses Repositorys zurücksetzen settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen. @@ -2690,7 +2690,7 @@ error.csv.too_large=Diese Datei kann nicht gerendert werden, da sie zu groß ist error.csv.unexpected=Diese Datei kann nicht gerendert werden, da sie ein unerwartetes Zeichen in Zeile %d und Spalte %d enthält. error.csv.invalid_field_count=Diese Datei kann nicht gerendert werden, da sie eine falsche Anzahl an Feldern in Zeile %d hat. rss.must_be_on_branch = Du musst auf einem Branch sein, um einen RSS-Feed zu haben. -new_repo_helper = Ein Repository enthält alle Projektdateien inklusive der Revisionshistorie. Bereits woanders gehostet? Repository migrieren. +new_repo_helper = Ein Repository enthält alle Projektdateien inklusive der Revisionshistorie. Bereits woanders gehostet? Repository migrieren. issues.comment.blocked_by_user = Du kannst kein Kommentar für dieses Issue erstellen, weil du vom Repository-Besitzer oder dem Autoren des Issues blockiert wurdest. clone_in_vscodium = In VSCodium klonen settings.units.add_more = Mehr aktivieren @@ -2704,7 +2704,7 @@ settings.add_collaborator_blocked_them = Der Mitarbeiter konnte nicht hinzugefü settings.wiki_rename_branch_main = Den Wiki-Branch-Namen normalisieren settings.enter_repo_name = Gib den Besitzer- und den Repository-Namen genau wie angezeigt ein: settings.wiki_branch_rename_success = Der Branch-Name des Repository-Wikis wurde erfolgreich normalisiert. -settings.archive.mirrors_unavailable = Spiegel sind nicht verfügbar, wenn das Repo archiviert ist. +settings.archive.mirrors_unavailable = Spiegel sind nicht verfügbar in archivierten Repos. pulls.blocked_by_user = Du kannst keinen Pull-Request in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. settings.add_collaborator_blocked_our = Der Mitarbeiter konnte nicht hinzugefügt werden, weil der Repository-Besitzer ihn blockiert hat. issues.blocked_by_user = Du kannst kein Issue in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. @@ -3686,12 +3686,12 @@ conan.registry=Diese Registry über die Kommandozeile einrichten: conan.install=Um das Paket mit Conan zu installieren, führe den folgenden Befehl aus: conda.registry=Richte diese Registry als Conda-Repository in deiner .condarc-Datei ein: conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befehl aus: -container.details.type=Container-Image Typ +container.details.type=Abbildtyp container.details.platform=Plattform container.pull=Downloade das Container-Image aus der Kommandozeile: container.digest=Digest container.multi_arch=Betriebsystem / Architektur -container.layers=Container-Image Ebenen +container.layers=Abbildebenen container.labels=Labels container.labels.key=Schlüssel container.labels.value=Wert diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b3864d4e83..12f88a5847 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -319,7 +319,7 @@ default_keep_email_private=Piilota sähköpostiosoitteet oletuksena default_keep_email_private.description=Piilota oletusarvoisesti uusien käyttäjätilien sähköpostiosoitteet estääksesi tietojen vuotamisen rekisteröinnin yhteydessä. default_enable_timetracking=Ota ajanseuranta oletusarvoisesti käyttöön default_enable_timetracking.description=Salli uusien repositorioiden aikaseurannan käyttöönotto oletusarvoisesti. -no_reply_address=Piilotettu sähköpostin verkkotunnus +no_reply_address=Piilotetun sähköpostin verkkotunnus no_reply_address_helper=Verkkotunnuksen nimi käyttäjille, joilla on piilotettu sähköpostiosoite. Esimerkiksi käyttäjätunnus 'joe' kirjataan Git-palveluun nimellä 'joe@noreply.example.org' jos piilotetun sähköpostiosoitteen arvoksi on asetettu 'noreply.example.org'. password_algorithm=Salasanan hajautusalgoritmi enable_update_checker_helper_forgejo = Se tarkistaa tietyin väliajoin uusia Forgejo-versioita tutkimalla sen TXT DNS record -tietoja osoitteesta release.forgejo.org . @@ -461,6 +461,9 @@ change_unconfirmed_email = Jos annoit väärän sähköpostiosoitteen rekisterö invalid_code_forgot_password = Vahvistuskoodisi on virheellinen tai vanhentunut. Napsauta tästä aloittaaksesi uuden istunnon. openid_signin_desc = Kirjoita OpenID-URI:si. Esimerkki: alice.openid.example.org tai https://openid.example.org/alice. change_unconfirmed_email_summary = Vaihda sähköpostiosoite, johon aktivointisähköposti lähetetään. +reset_password_wrong_user = Olet kirjautuneena tilillä %s, mutta tilin palautuslinkki on tarkoitettu tilille %s +last_admin = Et voi poistaa viimeistä ylläpitäjää. Ylläpitäjiä tulee olla vähintään yksi. +password_pwned = Valitsemasi salasana on varastettujen salasanojen listalla, eli se on paljastanut jossain julkisessa tietovuodossa. Kokeile asettaa eri salasana, ja jos käytät samaa salasanaa muissa palveluissa, vaihda kyseinen salasana. [mail] view_it_on=Näytä %s @@ -629,6 +632,8 @@ following_one = %d seurataan block_user.detail = Huomaa, että käyttäjän estämisellä on muita vaikutuksia, kuten: show_on_map = Näytä paikka kartalla form.name_chars_not_allowed = Käyttäjätunnus "%s" sisältää virheellisiä merkkejä. +follow_blocked_user = Et voi seurata tätä käyttäjää, koska olet estänyt kyseisen käyttäjän tai kyseinen käyttäjä on estänyt sinut. +disabled_public_activity = Käyttäjä on poistanut käytöstä toiminnan julkisen näkyvyyden. [settings] @@ -684,7 +689,7 @@ keep_activity_private_popup=Tekee toiminnon näkyvän vain sinulle ja ylläpitä lookup_avatar_by_mail=Hae profiilikuva sähköpostin perusteella federated_avatar_lookup=Ulkopuolinen profiilikuvan haku -enable_custom_avatar=Ota käyttöön mukautettu profiilikuva +enable_custom_avatar=Käytä mukautettua profiilikuvaa choose_new_avatar=Valitse uusi profiilikuva update_avatar=Päivitä profiilikuva delete_current_avatar=Poista nykyinen profiilikuva @@ -699,9 +704,9 @@ password_change_disabled=Ei-lokaalit käyttäjät eivät voi päivittää salasa emails=Sähköposti osoitteet manage_emails=Hallitse sähköpostiosoitteita -manage_themes=Valitse oletusteema -manage_openid=Hallitse OpenID osoitteita -theme_desc=Tämä on sivuston oletusteemasi. +manage_themes=Oletusteema +manage_openid=OpenID-osoitteet +theme_desc=Tätä teemaa käytetään verkkosivuston käyttöliittymässä, kun olet sisäänkirjautuneena. primary=Ensisijainen activated=Aktivoitu requires_activation=Vaatii aktivoinnin @@ -717,7 +722,7 @@ theme_update_error=Valittua teemaa ei löydy. openid_deletion=Poista OpenID-osoite openid_deletion_success=OpenID-osoite on poistettu. add_new_email=Lisää uusi sähköpostiosoite -add_new_openid=Lisää uusi OpenID URI +add_new_openid=Lisää uusi OpenID-URI add_email=Lisää sähköpostiosoite add_openid=Lisää OpenID URI add_email_success=Uusi sähköpostiosoite on lisätty. @@ -729,14 +734,14 @@ openid_desc=OpenID mahdollistaa todentamisen delegoinnin ulkopuoliselle palvelun manage_ssh_keys=Hallitse SSH-avaimia manage_gpg_keys=Hallitse GPG-avaimia add_key=Lisää avain -ssh_desc=Nämä julkiset SSH-avaimet on liitetty tiliisi. Vastaavat yksityiset avaimet antavat täyden pääsyn repoihisi. -gpg_desc=Nämä julkiset GPG-avaimet on liitetty tiliisi. Pidä yksityiset avaimet turvassa, koska ne mahdollistavat committien todentamisen. +ssh_desc=Nämä julkiset SSH-avaimet on liitetty tiliisi. Vastaavat yksityiset avaimet antavat täyden pääsyn repoihisi. Vahvistettuja SSH-avaimia voi käyttää SSH-allekirjoitettujen Git-kommittien vahvistamiseen. +gpg_desc=Nämä julkiset GPG-avaimet on liitetty tiliisi, ja niitä käytetään kommittien vahvistamiseen. Pidä yksityiset avaimet turvassa, koska ne mahdollistavat kommittien allekirjoittamisen sinun nimissä. ssh_helper=Tarvitsetko apua? Tutustu GitHubin oppaaseen omien SSH-avainten luonnista tai yleisistä ongelmista, joita voit kohdata SSH:n kanssa. gpg_helper=Tarvitsetko apua? Katso GitHubin opas GPG:stä. add_new_key=Lisää SSH avain add_new_gpg_key=Lisää GPG-avain -key_content_ssh_placeholder=Alkaa sanoilla 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', tai 'sk-ssh-ed25519@openssh.com' -key_content_gpg_placeholder=Alkaa sanoilla '-----BEGIN PGP PUBLIC KEY BLOCK-----' +key_content_ssh_placeholder=Alkaa sanoilla "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ecdsa-sha2-nistp256@openssh.com" tai "sk-ssh-ed25519@openssh.com" +key_content_gpg_placeholder=Alkaa sanoilla "-----BEGIN PGP PUBLIC KEY BLOCK-----" ssh_key_name_used=Samanniminen SSH avain on jo olemassa tililläsi. gpg_key_id_used=Julkinen GPG-avain samalla tunnuksella on jo olemassa. gpg_no_key_email_found=Tämä GPG-avain ei vastaa mitään tiliisi liitettyä aktivoitua sähköpostiosoitetta. Se voidaan silti lisätä, jos allekirjoitat annetun pääsymerkin. @@ -748,7 +753,7 @@ gpg_token=Pääsymerkki gpg_token_help=Voit luoda allekirjoituksen käyttäen: gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig gpg_token_signature=Panssaroitu GPG-allekirjoitus -key_signature_gpg_placeholder=Alkaa sanoilla '-----BEGIN PGP SIGNATURE-----' +key_signature_gpg_placeholder=Alkaa sanoilla "-----BEGIN PGP SIGNATURE-----" ssh_key_verified=Vahvistettu avain ssh_key_verified_long=Avain on vahvistettu pääsymerkillä ja sitä voidaan käyttää todentamaan commitit, jotka vastaavat tämän käyttäjän aktivoituja sähköpostiosoitteita. ssh_key_verify=Vahvista @@ -756,7 +761,7 @@ ssh_token_required=Sinun täytyy antaa allekirjoitus alla olevalle pääsymerkil ssh_token=Pääsymerkki ssh_token_help=Voit luoda allekirjoituksen käyttäen: ssh_token_signature=Panssaroitu SSH-allekirjoitus -key_signature_ssh_placeholder=Alkaa sanoilla '-----BEGIN SSH SIGNATURE-----' +key_signature_ssh_placeholder=Alkaa sanoilla "-----BEGIN SSH SIGNATURE-----" subkeys=Aliavaimet key_id=Avain ID key_name=Avaimen nimi @@ -774,7 +779,7 @@ can_read_info=Luku can_write_info=Kirjoitus show_openid=Näytä profiilissa hide_openid=Piilota profiilista -ssh_disabled=SSH pois käytöstä +ssh_disabled=SSH on pois käytöstä manage_social=Hallitse liitettyjä sosiaalisia tilejä manage_access_token=Hallitse pääsymerkkejä @@ -795,7 +800,7 @@ create_oauth2_application=Luo uusi OAuth2-sovellus create_oauth2_application_button=Luo sovellus oauth2_application_name=Sovelluksen nimi save_application=Tallenna -oauth2_regenerate_secret=Luo secret uudelleen +oauth2_regenerate_secret=Luo salaisuus uudelleen oauth2_regenerate_secret_hint=Kadotitko secretin? oauth2_application_edit=Muokkaa @@ -811,10 +816,10 @@ twofa_enrolled=Tiliisi on otettu käyttöön kaksivaiheinen vahvistus. Ota palau webauthn_nickname=Nimimerkki -manage_account_links=Hallitse linkitettyjä tilejä +manage_account_links=Yhdistetyt tilit manage_account_links_desc=Nämä ulkoiset tilit on linkitetty Forgejo tiliisi. link_account=Yhdistä tili -remove_account_link=Poista linkitetty tili +remove_account_link=Poista yhdistetty tili remove_account_link_desc=Linkitetyn tilin poistaminen peruuttaa pääsyn Forgejo-tiliisi linkitetyn tili kautta. Jatketaanko? remove_account_link_success=Linkitetty tili on poistettu. @@ -854,7 +859,7 @@ location_placeholder = Jaa likimääräinen sijaintisi muiden kanssa retype_new_password = Vahvista uusi salasana create_oauth2_application_success = Loit uuden OAuth2-sovelluksen. repos_none = Et omista yhtäkään repositoriota. -visibility.limited_tooltip = Näkyvissä vain tunnistautuneille käyttäjille +visibility.limited_tooltip = Näkyvissä vain kirjautuneille käyttäjille email_notifications.disable = Poista sähköposti-ilmoitukset käytöstä webauthn_register_key = Lisää turva-avain blocked_users = Estetyt käyttäjät @@ -892,6 +897,28 @@ twofa_disable = Poista kaksivaiheinen todennus käytöstä twofa_disable_desc = Kaksivaiheisen todennuksen poistaminen asettaa tilisi aiempaa suurempaan uhkaan. Jatketaanko? update_language_not_found = Kieli "%s" ei ole käytettävissä. change_username_prompt = Huomio: Käyttäjätunnuksen vaihtaminen muuttaa myös tilisi URL-osoitteen. +oauth2_client_secret_hint = Tätä salaisuutta ei näytetä uudelleen, kun olet poistunut sivulta tai päivittänyt sivun. Varmista, että olet ottanut salaisuuden talteen. +blocked_since = Estetty %s lähtien +user_unblock_success = Käyttäjän esto on poistettu. +oauth2_redirect_uris = Uudelleenohjaus-URI:t. Käytä uutta riviä (newline) jokaista URI:a kohden. +oauth2_client_secret = Asiakkaan salaisuus +verify_ssh_key_success = SSH-avain "%s" on vahvistettu. +change_username_redirect_prompt = Vanha käyttäjätunnus uudelleenohjaa, kunnes joku muu ottaa käyttäjätunnuksen käyttönsä. +uploaded_avatar_is_too_big = Lähetetyn tiedoston koko (%d KiB) ylittää enimmäiskoon (%d KiB). +ssh_key_been_used = Tämä SSH-avain on jo lisätty palvelimelle. +verify_gpg_key_success = GPG-avain "%s" on vahvistettu. +add_key_success = SSH-avain "%s" on lisätty. +add_gpg_key_success = GPG-avain "%s" on lisätty. +ssh_key_deletion_success = SSH-avain on poistettu. +valid_until_date = Kelvollinen %s asti +oauth2_client_id = Asiakkaan tunniste +email_notifications.onmention = Ilmoitus vain maininnasta +email_notifications.submit = Aseta valinta +email_notifications.andyourown = Ja omat ilmoitukset +key_state_desc = Tätä avainta on käytetty viimeisen 7 päivän aikana +oauth2_application_create_description = OAuth2-sovellukset mahdollistavat kolmannen osapuolen sovelluksen pääsyn tilillesi tässä instanssissa. +oauth2_confidential_client = Luottamuksellinen sovellus. Valitse sovelluksille, jotka pitävät salaisuuden luottamuksellisena, kuten web-sovelluksille. Älä valitse natiiveille sovelluksille mukaan lukien työpöytä- ja mobiilisovellukset. +ssh_key_deletion_desc = SSH-avaimen poistaminen kumoaa pääsyn tilillesi kyseistä avainta käyttäen. Jatketaanko? [repo] owner=Omistaja @@ -917,7 +944,7 @@ repo_gitignore_helper=Valitse .gitignore-mallit issue_labels=Ongelmien tunnisteet issue_labels_helper=Valitse pohja ongelmien nimilapuille. license=Lisenssi -license_helper=Valitse lisenssitiedosto. +license_helper=Valitse lisenssitiedosto readme=README auto_init=Alusta repo (Luo .gitignore, License ja README) create_repo=Luo repo @@ -1316,7 +1343,7 @@ activity.new_issues_count_1=Uusi ongelma activity.new_issues_count_n=uutta ongelmaa activity.new_issue_label=Avoinna activity.unresolved_conv_label=Auki -activity.published_release_label=Julkaistu +activity.published_release_label=Julkaisu activity.git_stats_pushed_1=on työntänyt activity.git_stats_file_1=%d tiedosto activity.git_stats_file_n=%d tiedostoa @@ -1361,9 +1388,9 @@ settings.transfer.title=Siirrä omistajuus settings.transfer_form_title=Syötä repon nimi vahvistuksena: settings.transfer_notices_3=- Jos arkisto on yksityinen ja se siirretään yksittäiselle käyttäjälle, tämä toiminto varmistaa, että käyttäjällä on ainakin lukuoikeudet (ja muuttaa käyttöoikeuksia tarvittaessa). settings.transfer_owner=Uusi omistaja -settings.wiki_delete=Poista Wiki data +settings.wiki_delete=Poista wikidata settings.wiki_delete_desc=Repon wikin data poistaminen on pysyvä eikä voi peruuttaa. -settings.confirm_wiki_delete=Wiki datan poistaminen +settings.confirm_wiki_delete=Poista wikidata settings.wiki_deletion_success=Repon wiki data on poistettu. settings.delete=Poista tämä repo settings.delete_desc=Repon poistaminen on pysyvä eikä voi peruuttaa. @@ -1384,7 +1411,7 @@ settings.webhook.body=Sisältö settings.githook_edit_desc=Jos koukku ei ole käytössä, esitellään esimerkkisisältö. Sisällön jättäminen tyhjäksi arvoksi poistaa tämän koukun käytöstä. settings.githook_name=Koukun nimi settings.githook_content=Koukun sisältö -settings.update_githook=Päivitys koukku +settings.update_githook=Päivitä koukku settings.payload_url=Kohde URL settings.http_method=HTTP-menetelmä settings.secret=Salaus @@ -1406,7 +1433,7 @@ settings.event_push_desc=Git push repoon. settings.event_repository=Repo settings.event_repository_desc=Repo luotu tai poistettu. settings.event_header_issue=Ongelmien tapahtumat -settings.event_issues=Ongelmat +settings.event_issues=Muokkaus settings.event_issues_desc=Ongelma avattu, suljettu, avattu uudelleen tai muokattu. settings.event_issue_assign=Ongelma määritetty settings.event_issue_assign_desc=Ongelma osoitettu tai osoitus poistettu. @@ -1414,7 +1441,7 @@ settings.event_issue_label_desc=Ongelman tunnisteet päivitetty tai tyhjennetty. settings.event_issue_milestone_desc=Merkkipaalu lisätty, poistettu tai muokattu. settings.event_issue_comment_desc=Ongelman kommentti luotu, muokattu tai poistettu. settings.event_header_pull_request=Vetopyyntöjen tapahtumat -settings.event_pull_request=Vetopyyntö +settings.event_pull_request=Muokkaus settings.event_package_desc=Paketti on luotu tai poistettu repossa. settings.active_helper=Tiedot käynnistetyistä tapahtumista lähetetään tähän webkoukun URL-osoitteeseen. settings.add_hook_success=Uusi webkoukku on lisätty. @@ -1883,7 +1910,7 @@ migrate.gitlab.description = Tee migraatio gitlab.comista tai muista GitLab-inst migrate.gitea.description = Tee migraatio gitea.comista tai muista Gitea-instansseista. repo_gitignore_helper_desc = Valitse mitä tiedostoja ei seurata yleisimpien kielten mallipohjista. Tyypilliset artefaktit, joita eri kielten koostamistyökalut tuottavat, lisätään .gitignore-tiedostoon oletusarvoisesti. milestones.filter_sort.latest_due_date = Kaukaisin määräpäivä -license_helper_desc = Lisenssi määrää, mitä muut voivat ja eivät voi tehdä koodillasi. Etkö ole varma, mikä lisenssi soveltuu projektillesi? Lue ohje lisenssin valinnasta. +license_helper_desc = Lisenssi määrää, mitä muut voivat ja eivät voi tehdä koodillasi. Etkö ole varma, mikä lisenssi soveltuu projektillesi? Lue ohje lisenssin valinnasta. milestones.filter_sort.earliest_due_data = Lähin määräpäivä issues.filter_type.reviewed_by_you = Katselmoitu toimestasi settings.units.overview = Yleisnäkymä @@ -1959,6 +1986,31 @@ editor.must_have_write_access = Sinulla täytyy olla kirjoitusoikeus tehdäksesi issues.re_request_review = Pyydä katselmointia uudelleen pulls.status_checks_details = Yksityiskohdat release.title_empty = Nimi ei voi olla tyhjä. +archive.title = Tämä repo on arkistoitu. Voit katsella sen sisältämiä tiedostoja ja kloonata repon, mutta et voi pushata, avata ongelmia tai luoda vetopyyntöjä. +reactions_more = ja %d lisää +mirror_address = Kloonaa URL-osoitteesta +migrate_items_merge_requests = Yhdistämispyynnöt +stars_remove_warning = Tämä poistaa kaikki tähdet tästä reposta. +archive.issue.nocomment = Tämä repo on arkistoitu. Et voi kommentoida ongelmia. +archive.pull.nocomment = Tämä repo on arkistoitu. Et voi kommentoida vetopyyntöjä. +settings.webhook_deletion_desc = Webkoukun poistaminen poistaa sen asetukset ja toimitushistorian. Jatketaanko? +settings.discord_icon_url.exceeds_max_length = Kuvakkeen URL-osoite voi sisältää enintään 2048 merkkiä +settings.event_wiki_desc = Wiki-sivu luotu, nimetty uudelleen, muokattu tai poistettu. +settings.event_pull_request_desc = Vetopyyntö avattu, suljettu, avattu uudelleen tai muokattu. +settings.protect_branch_name_pattern = Suojatun haaran nimen kaava +issues.dependency.add_error_dep_not_same_repo = Molempien ongelmien tulee olla samassa repossa. +settings.event_release = Julkaisu +pulls.merge_pull_request = Luo yhdistämiskommitti +settings.pull_mirror_sync_quota_exceeded = Kiintiö ylitetty, ei vedetä muutoksia. +settings.wiki_rename_branch_main_notices_1 = Tätä toimintoa EI VOI perua. +settings.webhook.test_delivery_desc_disabled = Aktivoi webkoukku testataksesi sitä tekaistulla tapahtumalla. +settings.discord_icon_url = Kuvakkeen URL-osoite +settings.archive.branchsettings_unavailable = Haaran asetukset eivät ole saatavilla arkistoiduissa repoissa. +pulls.ready_for_review = Valmiina katselmointiin? +issues.time_spent_total = Käytetty kokonaisaika +settings.webhook.test_delivery_desc = Testaa tätä webkoukkua tekaistulla tapahtumalla. +pulls.switch_comparison_type = Vaihda vertailutyyppiä +settings.hooks_desc = Webkoukut tekevät automaattisesti HTTP POST -pyyntöjä palvelimelle, kun jotkin Forgejo-tapahtumat käynnistyvät. Lue lisää webkoukkujen oppaasta. @@ -2102,8 +2154,8 @@ dashboard.operation_switch=Vaihda dashboard.operation_run=Suorita dashboard.delete_inactive_accounts=Poista kaikki aktivoimattomat käyttäjät dashboard.delete_repo_archives=Poista kaikki repojen arkistot (ZIP, TAR.GZ, jne..) -dashboard.server_uptime=Palvelimen Uptime -dashboard.current_goroutine=Nykyiset Goroutinet +dashboard.server_uptime=Palvelimen uptime +dashboard.current_goroutine=Nykyiset goroutinet dashboard.current_memory_usage=Nykyinen muistinkäyttö dashboard.total_memory_allocated=Yhteensä muistia varattu dashboard.memory_obtained=Muistia saatu @@ -2279,7 +2331,7 @@ config.default_visibility_organization=Uuden organisaation oletusnäkyvyys config.webhook_config=Webkoukkujen asetukset config.queue_length=Jonon pituus -config.deliver_timeout=Toimitus aikakatkaisu +config.deliver_timeout=Toimituksen aikakatkaisu config.mailer_enabled=Käytössä config.mailer_name=Nimi @@ -2426,6 +2478,8 @@ dashboard.task.unknown = Tuntematon tehtävä: %[1]s dashboard.cron.error = Virhe Cronissa: %s: %[3]s dashboard.task.started = Käynnistetty tehtävä: %[1]s dashboard.cron.finished = Cron: %[1]s on valmistunut +dashboard.resync_all_sshkeys = Päivitä ".ssh/authorized_keys"-tiedosto Forgejo:n SSH-avaimilla. +dashboard.cleanup_packages = Siivoa vanhentuneet paketit [action] @@ -2601,6 +2655,15 @@ settings.link = Linkitä tämä paketti repositorioon maven.download = Lataa riippuvuus suorittamalla komentorivillä: registry.documentation = Lisätietoja %s-rekisteristä on dokumentaatiossa. owner.settings.chef.keypair.description = Avainpari vaaditaan Chef-rekisteriin tunnistautumista varten. Jos olet luonut avainparin aiemmin, uuden avainparin luominen hylkää aiemman avainparin. +owner.settings.cleanuprules.keep.pattern = Säilytä kaavaa vastaavat versiot +owner.settings.cleanuprules.pattern_full_match = Toteuta kaavio paketin koko nimeen +owner.settings.cleanuprules.keep.title = Näitä sääntöjä vastaavat versiot säilytetään, vaikka ne vastaisivat alla olevaa poistosääntöä. +owner.settings.cleanuprules.keep.count = Säilytä viimeisimmät +owner.settings.cleanuprules.remove.pattern = Poista kaavaa vastaavat versiot +owner.settings.cleanuprules.keep.pattern.container = Viimeisin (latest) versio säilytetään aina Container-paketeista. +owner.settings.cleanuprules.remove.title = Näitä sääntöjä vastaavat versiot poistetaan, ellei sääntö yläpuolella käske säilyttää niitä. +owner.settings.cleanuprules.remove.days = Poista versiot, jotka ovat vanhempia kuin +arch.pacman.helper.gpg = Lisää luottamusvarmenne pacmanille: [secrets] creation.failed = Salaisuuden lisääminen epäonnistui. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index e45e27e7f0..8285607b1f 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -165,6 +165,7 @@ error413 = Votre quota est épuisé. new_repo.title = Nouveau dépôt new_migrate.link = Nouvelle migration new_org.link = Nouvelle organisation +copy_path = Copier le chemin [aria] navbar=Barre de navigation @@ -811,7 +812,7 @@ manage_emails=Gérer les adresses courriels manage_themes=Thème par défaut manage_openid=Adresses OpenID email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web. -theme_desc=Ce sera votre thème par défaut sur le site. +theme_desc=Ce thème sera utilisé pour l'interface web lorsque vous êtes authentifié. primary=Principale activated=Activé requires_activation=Nécessite une activation @@ -1055,7 +1056,7 @@ language.localization_project = Aidez-nous à traduire Forgejo dans votre langue 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. +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.. owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt @@ -1095,7 +1096,7 @@ issue_labels=Étiquettes issue_labels_helper=Sélectionner un jeu d'étiquettes license=Licence license_helper=Sélectionner une licence -license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment choisir une licence. +license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment choisir une licence.. readme=LISEZMOI readme_helper=Choisissez un modèle de fichier LISEZMOI readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et accueillir des contributeurs. @@ -2508,7 +2509,7 @@ settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt. settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir. settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé. settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé. -settings.archive.mirrors_unavailable = Les mirroirs ne sont pas disponibles si le dépôt a été archivé. +settings.archive.mirrors_unavailable = Les miroirs ne sont pas disponibles si le dépôt a été archivé. settings.unarchive.button=Désarchiver ce dépôt settings.unarchive.header=Réhabiliter ce dépôt settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts. @@ -2844,6 +2845,10 @@ diff.git-notes.remove-body = Cette note sera supprimée. diff.git-notes.add = Ajouter une note diff.git-notes.remove-header = Supprimer la note issues.summary_card_alt = Fiche de synthèse d'un ticket nommé "%s" dans le dépôt %s +editor.add_tmpl.filename = fichier +issues.num_reviews_one = %d revue +issues.num_reviews_few = %d revues +settings.default_update_style_desc = Style de mise à jour des demandes de fusion qui sont en retard par rapport à la branche de base. [graphs] component_loading = Chargement %s... @@ -3662,7 +3667,7 @@ alpine.registry=Configurez ce registre en ajoutant l’URL dans votre fichier /etc/apk/keys/ pour vérifier la signature de l'index : alpine.registry.info=Choisissez $branch et $repository dans la liste ci-dessous. alpine.install=Pour installer le paquet, exécutez la commande suivante : -alpine.repository=Informations sur le Dépôt +alpine.repository=Informations sur le dépôt alpine.repository.branches=Branches alpine.repository.repositories=Dépôts alpine.repository.architectures=Architectures @@ -3682,7 +3687,7 @@ conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande container.details.type=Type d'image container.details.platform=Plateforme container.pull=Tirez l'image depuis un terminal : -container.digest=Empreinte : +container.digest=Empreinte container.multi_arch=SE / Arch container.layers=Calques d'image container.labels=Labels @@ -3797,6 +3802,7 @@ arch.version.conflicts = Conflits arch.version.replaces = Remplace arch.version.backup = Sauvegarde arch.version.makedepends = Faire des dépendances +container.images.title = Images [secrets] secrets=Secrets diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 397370c883..b44f3f384d 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -765,7 +765,7 @@ cancel=Atcelt language=Valoda ui=Motīvs hidden_comment_types=Slēpjamo piebilžu veidi -hidden_comment_types_description=Šeit atzīmētie piebilžu veidi netiks attēloti pieteikumu lapās. "Iezīme" atzīmēšana, piemēram, noņems visas " pievienoja/noņēma
{{end}} {{if .NoteRendered}} -
+
{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "repo.diff.git-notes"}}: {{if .NoteAuthor}} @@ -277,11 +277,11 @@ {{else}} {{.NoteCommit.Author.Name}} {{end}} - {{DateUtils.TimeSince .NoteCommit.Author.When}} + {{DateUtils.TimeSince .NoteCommit.Author.When}} {{if and ($.Permission.CanWrite $.UnitTypeCode) (not $.Repository.IsArchived) (not .IsDeleted)}} -
- - +
+ +