diff options
86 files changed, 777 insertions, 435 deletions
diff --git a/cmd/serv.go b/cmd/serv.go index b18508459f..26a3af50f3 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strconv" "strings" "time" @@ -20,7 +19,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfstransfer" @@ -37,14 +36,6 @@ import ( "github.com/urfave/cli/v2" ) -const ( - verbUploadPack = "git-upload-pack" - verbUploadArchive = "git-upload-archive" - verbReceivePack = "git-receive-pack" - verbLfsAuthenticate = "git-lfs-authenticate" - verbLfsTransfer = "git-lfs-transfer" -) - // CmdServ represents the available serv sub-command. var CmdServ = &cli.Command{ Name: "serv", @@ -78,22 +69,6 @@ func setup(ctx context.Context, debug bool) { } } -var ( - // keep getAccessMode() in sync - allowedCommands = container.SetOf( - verbUploadPack, - verbUploadArchive, - verbReceivePack, - verbLfsAuthenticate, - verbLfsTransfer, - ) - allowedCommandsLfs = container.SetOf( - verbLfsAuthenticate, - verbLfsTransfer, - ) - alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) - // fail prints message to stdout, it's mainly used for git serv and git hook commands. // The output will be passed to git client and shown to user. func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error { @@ -139,19 +114,20 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { func getAccessMode(verb, lfsVerb string) perm.AccessMode { switch verb { - case verbUploadPack, verbUploadArchive: + case git.CmdVerbUploadPack, git.CmdVerbUploadArchive: return perm.AccessModeRead - case verbReceivePack: + case git.CmdVerbReceivePack: return perm.AccessModeWrite - case verbLfsAuthenticate, verbLfsTransfer: + case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer: switch lfsVerb { - case "upload": + case git.CmdSubVerbLfsUpload: return perm.AccessModeWrite - case "download": + case git.CmdSubVerbLfsDownload: return perm.AccessModeRead } } // should be unreachable + setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb) return perm.AccessModeNone } @@ -230,12 +206,12 @@ func runServ(c *cli.Context) error { log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND")) } - words, err := shellquote.Split(cmd) + sshCmdArgs, err := shellquote.Split(cmd) if err != nil { return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) } - if len(words) < 2 { + if len(sshCmdArgs) < 2 { if git.DefaultFeatures().SupportProcReceive { // for AGit Flow if cmd == "ssh_info" { @@ -246,25 +222,21 @@ func runServ(c *cli.Context) error { return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } - verb := words[0] - repoPath := strings.TrimPrefix(words[1], "/") - - var lfsVerb string - - rr := strings.SplitN(repoPath, "/", 2) - if len(rr) != 2 { + repoPath := strings.TrimPrefix(sshCmdArgs[1], "/") + repoPathFields := strings.SplitN(repoPath, "/", 2) + if len(repoPathFields) != 2 { return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } - username := rr[0] - reponame := strings.TrimSuffix(rr[1], ".git") + username := repoPathFields[0] + reponame := strings.TrimSuffix(repoPathFields[1], ".git") // βthe-repo-name" or "the-repo-name.wiki" // LowerCase and trim the repoPath as that's how they are stored. // This should be done after splitting the repoPath into username and reponame // so that username and reponame are not affected. repoPath = strings.ToLower(strings.TrimSpace(repoPath)) - if alphaDashDotPattern.MatchString(reponame) { + if !repo.IsValidSSHAccessRepoName(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } @@ -286,22 +258,23 @@ func runServ(c *cli.Context) error { }() } - if allowedCommands.Contains(verb) { - if allowedCommandsLfs.Contains(verb) { - if !setting.LFS.StartServer { - return fail(ctx, "LFS Server is not enabled", "") - } - if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH { - return fail(ctx, "LFS SSH transfer is not enabled", "") - } - if len(words) > 2 { - lfsVerb = words[2] - } - } - } else { + verb, lfsVerb := sshCmdArgs[0], "" + if !git.IsAllowedVerbForServe(verb) { return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } + if git.IsAllowedVerbForServeLfs(verb) { + if !setting.LFS.StartServer { + return fail(ctx, "LFS Server is not enabled", "") + } + if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH { + return fail(ctx, "LFS SSH transfer is not enabled", "") + } + if len(sshCmdArgs) > 2 { + lfsVerb = sshCmdArgs[2] + } + } + requestedMode := getAccessMode(verb, lfsVerb) results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) @@ -310,7 +283,7 @@ func runServ(c *cli.Context) error { } // LFS SSH protocol - if verb == verbLfsTransfer { + if verb == git.CmdVerbLfsTransfer { token, err := getLFSAuthToken(ctx, lfsVerb, results) if err != nil { return err @@ -319,7 +292,7 @@ func runServ(c *cli.Context) error { } // LFS token authentication - if verb == verbLfsAuthenticate { + if verb == git.CmdVerbLfsAuthenticate { url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) token, err := getLFSAuthToken(ctx, lfsVerb, results) diff --git a/models/activities/statistic.go b/models/activities/statistic.go index ff81ad78a1..983a124550 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -17,13 +17,15 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" ) // Statistic contains the database statistics type Statistic struct { Counter struct { - User, Org, PublicKey, + UsersActive, UsersNotActive, + Org, PublicKey, Repo, Watch, Star, Access, Issue, IssueClosed, IssueOpen, Comment, Oauth, Follow, @@ -53,7 +55,19 @@ type IssueByRepositoryCount struct { // GetStatistic returns the database statistics func GetStatistic(ctx context.Context) (stats Statistic) { e := db.GetEngine(ctx) - stats.Counter.User = user_model.CountUsers(ctx, nil) + + // Number of active users + usersActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(true), + } + stats.Counter.UsersActive = user_model.CountUsers(ctx, &usersActiveOpts) + + // Number of inactive users + usersNotActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(false), + } + stats.Counter.UsersNotActive = user_model.CountUsers(ctx, &usersNotActiveOpts) + stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludePrivate: true}) stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) stats.Counter.Repo, _ = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{}) diff --git a/models/renderhelper/commit_checker.go b/models/renderhelper/commit_checker.go index 4815643e67..407e45fb54 100644 --- a/models/renderhelper/commit_checker.go +++ b/models/renderhelper/commit_checker.go @@ -47,7 +47,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool { c.gitRepo, c.gitRepoCloser = r, closer } - exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition. + exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. c.commitCache[commitID] = exist return exist } diff --git a/models/repo/repo.go b/models/repo/repo.go index 2977dfb9f1..5aae02c6d8 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -64,18 +64,18 @@ func (err ErrRepoIsArchived) Error() string { } type globalVarsStruct struct { - validRepoNamePattern *regexp.Regexp - invalidRepoNamePattern *regexp.Regexp - reservedRepoNames []string - reservedRepoPatterns []string + validRepoNamePattern *regexp.Regexp + invalidRepoNamePattern *regexp.Regexp + reservedRepoNames []string + reservedRepoNamePatterns []string } var globalVars = sync.OnceValue(func() *globalVarsStruct { return &globalVarsStruct{ - validRepoNamePattern: regexp.MustCompile(`[-.\w]+`), - invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`), - reservedRepoNames: []string{".", "..", "-"}, - reservedRepoPatterns: []string{"*.git", "*.wiki", "*.rss", "*.atom"}, + validRepoNamePattern: regexp.MustCompile(`^[-.\w]+$`), + invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`), + reservedRepoNames: []string{".", "..", "-"}, + reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"}, } }) @@ -86,7 +86,16 @@ func IsUsableRepoName(name string) error { // Note: usually this error is normally caught up earlier in the UI return db.ErrNameCharsNotAllowed{Name: name} } - return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name) + return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name) +} + +// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code +func IsValidSSHAccessRepoName(name string) bool { + vars := globalVars() + if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) { + return false + } + return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil } // TrustModelType defines the types of trust model for this repository diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index b2604ab575..66abe864fc 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -216,8 +216,23 @@ func TestIsUsableRepoName(t *testing.T) { assert.Error(t, IsUsableRepoName("-")) assert.Error(t, IsUsableRepoName("π")) + assert.Error(t, IsUsableRepoName("the/repo")) assert.Error(t, IsUsableRepoName("the..repo")) assert.Error(t, IsUsableRepoName("foo.wiki")) assert.Error(t, IsUsableRepoName("foo.git")) assert.Error(t, IsUsableRepoName("foo.RSS")) } + +func TestIsValidSSHAccessRepoName(t *testing.T) { + assert.True(t, IsValidSSHAccessRepoName("a")) + assert.True(t, IsValidSSHAccessRepoName("-1_.")) + assert.True(t, IsValidSSHAccessRepoName(".profile")) + assert.True(t, IsValidSSHAccessRepoName("foo.wiki")) + + assert.False(t, IsValidSSHAccessRepoName("-")) + assert.False(t, IsValidSSHAccessRepoName("π")) + assert.False(t, IsValidSSHAccessRepoName("the/repo")) + assert.False(t, IsValidSSHAccessRepoName("the..repo")) + assert.False(t, IsValidSSHAccessRepoName("foo.git")) + assert.False(t, IsValidSSHAccessRepoName("foo.RSS")) +} diff --git a/models/user/user.go b/models/user/user.go index 100f924cc6..d7331d79f0 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -828,6 +828,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool { type CountUserFilter struct { LastLoginSince *int64 IsAdmin optional.Option[bool] + IsActive optional.Option[bool] } // CountUsers returns number of users. @@ -848,6 +849,10 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 { if opts.IsAdmin.Has() { cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } + + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()}) + } } count, err := sess.Where(cond).Count(new(User)) @@ -1198,7 +1203,8 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e for _, email := range emailAddresses { user := users[email.UID] if user != nil { - results[user.GetEmail()] = user + results[user.Email] = user + results[user.GetPlaceholderEmail()] = user } } } @@ -1208,6 +1214,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e return nil, err } for _, user := range users { + results[user.Email] = user results[user.GetPlaceholderEmail()] = user } return results, nil diff --git a/models/user/user_test.go b/models/user/user_test.go index 90e8bf13a8..dd232abe2e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsUsableUsername(t *testing.T) { @@ -48,14 +49,23 @@ func TestOAuth2Application_LoadUser(t *testing.T) { assert.NotNil(t, user) } -func TestGetUserEmailsByNames(t *testing.T) { +func TestUserEmails(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - - // ignore none active user email - assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) - assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) - - assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) + t.Run("GetUserEmailsByNames", func(t *testing.T) { + // ignore none active user email + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) + assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) + }) + t.Run("GetUsersByEmails", func(t *testing.T) { + m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{"user1@example.com", "user2@" + setting.Service.NoReplyAddress}) + require.NoError(t, err) + require.Len(t, m, 4) + assert.EqualValues(t, 1, m["user1@example.com"].ID) + assert.EqualValues(t, 1, m["user1@"+setting.Service.NoReplyAddress].ID) + assert.EqualValues(t, 2, m["user2@example.com"].ID) + assert.EqualValues(t, 2, m["user2@"+setting.Service.NoReplyAddress].ID) + }) } func TestCanCreateOrganization(t *testing.T) { diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go index c17006a154..167b31416e 100644 --- a/modules/git/attribute/checker.go +++ b/modules/git/attribute/checker.go @@ -39,7 +39,12 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib ) cancel = deleteTemporaryFile } - } // else: no treeish, assume it is a not a bare repo, read from working directory + } else { + // Read from existing index, in cases where the repo is bare and has an index, + // or the work tree contains unstaged changes that shouldn't affect the attribute check. + // It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes. + cmd.AddArguments("--cached") + } cmd.AddDynamicArguments(attributes...) if len(filenames) > 0 { diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go index 97db43460b..67fbda8918 100644 --- a/modules/git/attribute/checker_test.go +++ b/modules/git/attribute/checker_test.go @@ -57,8 +57,18 @@ func Test_Checker(t *testing.T) { assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) }) + t.Run("Run git check-attr in bare repository using index", func(t *testing.T) { + attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + if !git.DefaultFeatures().SupportCheckAttrOnBare { - t.Skip("git version 2.40 is required to support run check-attr on bare repo") + t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index") return } diff --git a/modules/git/cmdverb.go b/modules/git/cmdverb.go new file mode 100644 index 0000000000..3d6f4ae0c6 --- /dev/null +++ b/modules/git/cmdverb.go @@ -0,0 +1,36 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +const ( + CmdVerbUploadPack = "git-upload-pack" + CmdVerbUploadArchive = "git-upload-archive" + CmdVerbReceivePack = "git-receive-pack" + CmdVerbLfsAuthenticate = "git-lfs-authenticate" + CmdVerbLfsTransfer = "git-lfs-transfer" + + CmdSubVerbLfsUpload = "upload" + CmdSubVerbLfsDownload = "download" +) + +func IsAllowedVerbForServe(verb string) bool { + switch verb { + case CmdVerbUploadPack, + CmdVerbUploadArchive, + CmdVerbReceivePack, + CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} + +func IsAllowedVerbForServeLfs(verb string) bool { + switch verb { + case CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 230260ff94..4d2ec287a9 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -184,7 +184,7 @@ func NewCollector() Collector { Users: prometheus.NewDesc( namespace+"users", "Number of Users", - nil, nil, + []string{"state"}, nil, ), Watches: prometheus.NewDesc( namespace+"watches", @@ -373,7 +373,14 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( c.Users, prometheus.GaugeValue, - float64(stats.Counter.User), + float64(stats.Counter.UsersActive), + "active", // state label + ) + ch <- prometheus.MustNewConstMetric( + c.Users, + prometheus.GaugeValue, + float64(stats.Counter.UsersNotActive), + "inactive", // state label ) ch <- prometheus.MustNewConstMetric( c.Watches, diff --git a/modules/private/serv.go b/modules/private/serv.go index 10e9f7995c..b1dafbd81b 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -46,18 +46,16 @@ type ServCommandResults struct { } // ServCommand preps for a serv call -func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { +func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verb, lfsVerb string) (*ServCommandResults, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", keyID, url.PathEscape(ownerName), url.PathEscape(repoName), mode, ) - for _, verb := range verbs { - if verb != "" { - reqURL += "&verb=" + url.QueryEscape(verb) - } - } + reqURL += "&verb=" + url.QueryEscape(verb) + // reqURL += "&lfs_verb=" + url.QueryEscape(lfsVerb) // TODO: actually there is no use of this parameter. In the future, the URL construction should be more flexible + _ = lfsVerb req := newInternalRequestAPI(ctx, reqURL, "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/templates/util_date_test.go b/modules/templates/util_date_test.go index f3a2409a9f..9015462bbb 100644 --- a/modules/templates/util_date_test.go +++ b/modules/templates/util_date_test.go @@ -17,6 +17,7 @@ import ( func TestDateTime(t *testing.T) { testTz, _ := time.LoadLocation("America/New_York") defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + defer test.MockVariableValue(&setting.IsProd, true)() defer test.MockVariableValue(&setting.IsInTesting, false)() du := NewDateUtils() @@ -53,6 +54,7 @@ func TestDateTime(t *testing.T) { func TestTimeSince(t *testing.T) { testTz, _ := time.LoadLocation("America/New_York") defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + defer test.MockVariableValue(&setting.IsProd, true)() defer test.MockVariableValue(&setting.IsInTesting, false)() du := NewDateUtils() diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 521233db40..8d9ba1000c 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -14,6 +14,8 @@ import ( "unicode" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/renderhelper" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" @@ -34,11 +36,11 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { } // RenderCommitMessage renders commit message with XSS-safe and special links. -func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { cleanMsg := template.HTMLEscapeString(msg) // we can safely assume that it will not return any error, since there // shouldn't be any special HTML. - fullMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg) + fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" @@ -52,7 +54,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string) // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. -func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) template.HTML { msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -63,9 +65,8 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me return "" } - // we can safely assume that it will not return any error, since there - // shouldn't be any special HTML. - renderedMessage, err := markup.PostProcessCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine)) + // we can safely assume that it will not return any error, since there shouldn't be any special HTML. + renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine)) if err != nil { log.Error("PostProcessCommitMessageSubject: %v", err) return "" @@ -74,7 +75,7 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me } // RenderCommitBody extracts the body of a commit message without its title. -func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -87,7 +88,7 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem return "" } - renderedMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine)) + renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" @@ -105,8 +106,8 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { } // RenderIssueTitle renders issue/pull title with defined post processors -func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { - renderedText, err := markup.PostProcessIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text)) +func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { + renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(text)) if err != nil { log.Error("PostProcessIssueTitle: %v", err) return "" diff --git a/modules/templates/util_render_legacy.go b/modules/templates/util_render_legacy.go index 8f7b84c83d..df8f5e64de 100644 --- a/modules/templates/util_render_legacy.go +++ b/modules/templates/util_render_legacy.go @@ -32,22 +32,22 @@ func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML return NewRenderUtils(reqctx.FromContext(ctx)).MarkdownToHtml(input) } -func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { +func renderCommitMessageLegacy(ctx context.Context, msg string, _ map[string]string) template.HTML { panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, metas) + return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, nil) } -func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { +func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, _ map[string]string) template.HTML { panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, metas) + return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, nil) } -func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML { +func renderIssueTitleLegacy(ctx context.Context, text string, _ map[string]string) template.HTML { panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, metas) + return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, nil) } -func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { +func renderCommitBodyLegacy(ctx context.Context, msg string, _ map[string]string) template.HTML { panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, metas) + return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, nil) } diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 460b9dc190..9b51d0cd57 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -11,11 +11,11 @@ import ( "testing" "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -47,19 +47,8 @@ mail@domain.com return strings.ReplaceAll(s, "<SPACE>", " ") } -var testMetas = map[string]string{ - "user": "user13", - "repo": "repo11", - "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", - "markdownNewLineHardBreak": "true", - "markupAllowShortIssuePattern": "true", -} - func TestMain(m *testing.M) { - unittest.InitSettingsForTesting() - if err := git.InitSimple(context.Background()); err != nil { - log.Fatal("git init failed, err: %v", err) - } + setting.Markdown.RenderOptionsComment.ShortIssuePattern = true markup.Init(&markup.RenderHelperFuncs{ IsUsernameMentionable: func(ctx context.Context, username string) bool { return username == "mention-user" @@ -74,46 +63,52 @@ func newTestRenderUtils(t *testing.T) *RenderUtils { return NewRenderUtils(ctx) } -func TestRenderCommitBody(t *testing.T) { - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - type args struct { - msg string +func TestRenderRepoComment(t *testing.T) { + mockRepo := &repo.Repository{ + ID: 1, OwnerName: "user13", Name: "repo11", + Owner: &user_model.User{ID: 13, Name: "user13"}, + Units: []*repo.RepoUnit{}, } - tests := []struct { - name string - args args - want template.HTML - }{ - { - name: "multiple lines", - args: args{ - msg: "first line\nsecond line", + t.Run("RenderCommitBody", func(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + type args struct { + msg string + } + tests := []struct { + name string + args args + want template.HTML + }{ + { + name: "multiple lines", + args: args{ + msg: "first line\nsecond line", + }, + want: "second line", }, - want: "second line", - }, - { - name: "multiple lines with leading newlines", - args: args{ - msg: "\n\n\n\nfirst line\nsecond line", + { + name: "multiple lines with leading newlines", + args: args{ + msg: "\n\n\n\nfirst line\nsecond line", + }, + want: "second line", }, - want: "second line", - }, - { - name: "multiple lines with trailing newlines", - args: args{ - msg: "first line\nsecond line\n\n\n", + { + name: "multiple lines with trailing newlines", + args: args{ + msg: "first line\nsecond line\n\n\n", + }, + want: "second line", }, - want: "second line", - }, - } - ut := newTestRenderUtils(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil) - }) - } - - expected := `/just/a/path.bin + } + ut := newTestRenderUtils(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, mockRepo), "RenderCommitBody(%v, %v)", tt.args.msg, nil) + }) + } + + expected := `/just/a/path.bin <a href="https://example.com/file.bin">https://example.com/file.bin</a> [local link](file.bin) [remote link](<a href="https://example.com">https://example.com</a>) @@ -132,22 +127,22 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit <a href="/mention-user">@mention-user</a> test <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> space` - assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), testMetas))) -} + assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), mockRepo))) + }) -func TestRenderCommitMessage(t *testing.T) { - expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a> ` - assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), testMetas)) -} + t.Run("RenderCommitMessage", func(t *testing.T) { + expected := `space <a href="/mention-user" data-markdown-generated-content="">@mention-user</a> ` + assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo)) + }) -func TestRenderCommitMessageLinkSubject(t *testing.T) { - expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>` - assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) -} + t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) { + expected := `<a href="https://example.com/link" class="muted">space </a><a href="/mention-user" data-markdown-generated-content="">@mention-user</a>` + assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo)) + }) -func TestRenderIssueTitle(t *testing.T) { - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - expected := ` space @mention-user<SPACE><SPACE> + t.Run("RenderIssueTitle", func(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + expected := ` space @mention-user<SPACE><SPACE> /just/a/path.bin https://example.com/file.bin [local link](file.bin) @@ -168,8 +163,9 @@ mail@domain.com <a href="/user13/repo11/issues/123" class="ref-issue">#123</a> space<SPACE><SPACE> ` - expected = strings.ReplaceAll(expected, "<SPACE>", " ") - assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), testMetas))) + expected = strings.ReplaceAll(expected, "<SPACE>", " ") + assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), mockRepo))) + }) } func TestRenderMarkdownToHtml(t *testing.T) { diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 63ab9f9d3a..2a3bd3e743 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -3667,12 +3667,13 @@ owner.settings.chef.keypair.description=Pro autentizaci do registru Chef je zapo secrets=TajnΓ© klΓΔe description=TejnΓ© klΓΔe budou pΕedΓ‘ny urΔitΓ½m akcΓm a nelze je pΕeΔΓst jinak. none=ZatΓm zde nejsou ΕΎΓ‘dnΓ© tajnΓ© klΓΔe. -creation=PΕidat tajnΓ½ klΓΔ + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Popis creation.name_placeholder=nerozliΕ‘ovat velkΓ‘ a malΓ‘ pΓsmena, pouze alfanumerickΓ© znaky nebo podtrΕΎΓtka, nemohou zaΔΓnat na GITEA_ nebo GITHUB_ creation.value_placeholder=VloΕΎte jakΓ½koliv obsah. Mezery na zaΔΓ‘tku a konci budou vynechΓ‘ny. -creation.success=TajnΓ½ klΓΔ β%sβ byl pΕidΓ‘n. -creation.failed=NepodaΕilo se pΕidat tajnΓ½ klΓΔ. + + deletion=Odstranit tajnΓ½ klΓΔ deletion.description=OdstranΔnΓ tajnΓ©ho klΓΔe je trvalΓ© a nelze ho vrΓ‘tit zpΔt. PokraΔovat? deletion.success=TajnΓ½ klΓΔ byl odstranΔn. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 43333f8ac6..f115dee247 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -3659,12 +3659,13 @@ owner.settings.chef.keypair.description=Ein SchlΓΌsselpaar ist notwendig, um mit secrets=Secrets description=Secrets werden an bestimmte Aktionen weitergegeben und kΓΆnnen nicht anderweitig ausgelesen werden. none=Noch keine Secrets vorhanden. -creation=Secret hinzufΓΌgen + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Beschreibung creation.name_placeholder=GroΓ-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen. -creation.success=Das Secret "%s" wurde hinzugefΓΌgt. -creation.failed=Secret konnte nicht hinzugefΓΌgt werden. + + deletion=Secret entfernen deletion.description=Das Entfernen eines Secrets kann nicht rΓΌckgΓ€ngig gemacht werden. Fortfahren? deletion.success=Das Secret wurde entfernt. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index c2479bf342..444fbd26c9 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -3329,12 +3329,13 @@ owner.settings.chef.keypair.description=ΞΞ½Ξ± ΞΆΞ΅ΟΞ³ΞΏΟ ΞΊΞ»Ξ΅ΞΉΞ΄ΞΉΟΞ½ ΡΠsecrets=ΞΟ
ΟΟΞΉΞΊΞ¬ description=΀α ΞΌΟ
ΟΟΞΉΞΊΞ¬ ΞΈΞ± ΟΞ΅ΟΞ¬ΟΞΏΟ
Ξ½ ΟΞ΅ ΞΏΟΞΉΟΞΌΞΞ½Ξ΅Ο Ξ΄ΟΞ¬ΟΞ΅ΞΉΟ ΞΊΞ±ΞΉ δΡν ΞΌΟΞΏΟΞΏΟΞ½ Ξ½Ξ± Ξ±Ξ½Ξ±Ξ³Ξ½ΟΟΟΞΏΟΞ½ αλλοΟ. none=ΞΡν Ο
ΟΞ¬ΟΟΞΏΟ
Ξ½ Ξ±ΞΊΟΞΌΞ± ΞΌΟ
ΟΟΞΉΞΊΞ¬. -creation=Ξ ΟΞΏΟΞΈΞΞΊΞ· ΞΟ
ΟΟΞΉΞΊΞΏΟ + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Ξ Ξ΅ΟΞΉΞ³ΟΞ±ΟΞ creation.name_placeholder=Ξ±Ξ»ΟΞ±ΟΞΉΞΈΞΌΞ·ΟΞΉΞΊΞΏΞ― ΟΞ±ΟΞ±ΞΊΟΞΟΞ΅Ο Ξ ΞΊΞ¬ΟΟ ΟΞ±ΟΞ»Ξ΅Ο ΞΌΟΞ½ΞΏ, δΡν ΞΌΟΞΏΟΞΏΟΞ½ Ξ½Ξ± ξΡκινοΟΞ½ ΞΌΞ΅ GITEA_ Ξ GITHUB_ creation.value_placeholder=ΞΞΉΟάγΡΟΞ΅ ΞΏΟΞΏΞΉΞΏΞ΄ΞΟΞΏΟΞ΅ ΟΞ΅ΟΞΉΞ΅ΟΟμΡνο. ΀α κΡνά ΟΟΞ·Ξ½ Ξ±ΟΟΞ ΟΞ±ΟαλΡίΟΞΏΞ½ΟΞ±ΞΉ. -creation.success=΀ο ΞΌΟ
ΟΟΞΉΞΊΟ "%s" ΟΟΞΏΟΟΞΞΈΞ·ΞΊΞ΅. -creation.failed=ΞΟΞΏΟΟ
ΟΞ―Ξ± δημιοΟ
ΟΞ³Ξ―Ξ±Ο ΞΌΟ
ΟΟΞΉΞΊΞΏΟ. + + deletion=ΞΟΞ±Ξ―ΟΞ΅ΟΞ· ΞΌΟ
ΟΟΞΉΞΊΞΏΟ deletion.description=Ξ Ξ±ΟΞ±Ξ―ΟΞ΅ΟΞ· ΡνΟΟ ΞΌΟ
ΟΟΞΉΞΊΞΏΟ Ξ΅Ξ―Ξ½Ξ±ΞΉ ΞΌΟΞ½ΞΉΞΌΞ· ΞΊΞ±ΞΉ δΡν ΞΌΟΞΏΟΡί Ξ½Ξ± Ξ±Ξ½Ξ±ΞΉΟΡθΡί. Ξ£Ο
Ξ½ΞΟΡια; deletion.success=΀ο ΞΌΟ
ΟΟΞΉΞΊΟ ΞΟΡι Ξ±ΟΞ±ΞΉΟΡθΡί. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9091b6bc4b..af3b948a88 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3722,13 +3722,18 @@ owner.settings.chef.keypair.description = A key pair is necessary to authenticat secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. -creation = Add Secret + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description = Description creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. creation.description_placeholder = Enter short description (optional). -creation.success = The secret "%s" has been added. -creation.failed = Failed to add secret. + +save_success = The secret "%s" has been saved. +save_failed = Failed to save secret. + +add_secret = Add secret +edit_secret = Edit secret deletion = Remove secret deletion.description = Removing a secret is permanent and cannot be undone. Continue? deletion.success = The secret has been removed. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 5f989f6acf..521583395e 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -3309,12 +3309,13 @@ owner.settings.chef.keypair.description=Un par de claves es necesario para auten secrets=Secretos description=Los secretos pasarΓ‘n a ciertas acciones y no se podrΓ‘n leer de otro modo. none=TodavΓa no hay secretos. -creation=AΓ±adir secreto + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=DescripciΓ³n creation.name_placeholder=sin distinciΓ³n de mayΓΊsculas, solo carΓ‘cteres alfanumΓ©ricos o guiones bajos, no puede empezar por GITEA_ o GITHUB_ creation.value_placeholder=Introduce cualquier contenido. Se omitirΓ‘ el espacio en blanco en el inicio y el final. -creation.success=El secreto "%s" ha sido aΓ±adido. -creation.failed=Error al aΓ±adir secreto. + + deletion=Eliminar secreto deletion.description=Eliminar un secreto es permanente y no se puede deshacer. ΒΏContinuar? deletion.success=El secreto ha sido eliminado. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 5d67f03bac..18abc0f401 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -2506,8 +2506,12 @@ conan.details.repository=Ω
ΨΨ²Ω owner.settings.cleanuprules.enabled=ΩΨΉΨ§Ω Ψ΄Ψ―Ω [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Ψ΄Ψ±Ψ + + [actions] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 69cee090fe..b925d6f43a 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1692,8 +1692,12 @@ conan.details.repository=Repo owner.settings.cleanuprules.enabled=KΓ€ytΓΆssΓ€ [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Kuvaus + + [actions] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index b9d550eee5..eeb5e31965 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -130,6 +130,7 @@ pin=Γpingler unpin=DΓ©sΓ©pingler artifacts=Artefacts +expired=ExpirΓ© confirm_delete_artifact=Γtes-vous sΓ»r de vouloir supprimer lβartefact « %sΒ Β» ? archived=ArchivΓ© @@ -450,6 +451,7 @@ use_scratch_code=Utiliser un code de secours twofa_scratch_used=Vous avez utilisΓ© votre code de secours. Vous avez Γ©tΓ© redirigΓ© vers cette page de configuration afin de supprimer l'authentification Γ deux facteurs de votre appareil ou afin de gΓ©nΓ©rer un nouveau code de secours. twofa_passcode_incorrect=Votre code dβaccΓ¨s nβest pas correct. Si vous avez Γ©garΓ© votre appareil, utilisez votre code de secours pour vous connecter. twofa_scratch_token_incorrect=Votre code de secours est incorrect. +twofa_required=Vous devez configurer lβauthentification Γ deux facteurs pour avoir accΓ¨s aux dΓ©pΓ΄ts, ou essayer de vous reconnecter. login_userpass=Connexion login_openid=OpenID oauth_signup_tab=CrΓ©er un compte @@ -1878,6 +1880,7 @@ pulls.add_prefix=Ajouter le prΓ©fixe <strong>%s</strong> pulls.remove_prefix=Enlever le prΓ©fixe <strong>%s</strong> pulls.data_broken=Cette demande dβajout est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblΓ©e. +pulls.is_checking=Recherche de conflits de fusionβ¦ pulls.is_ancestor=Cette branche est dΓ©jΓ prΓ©sente dans la branche ciblΓ©e. Il n'y a rien Γ fusionner. pulls.is_empty=Les changements sur cette branche sont dΓ©jΓ sur la branche cible. Cette rΓ©vision sera vide. pulls.required_status_check_failed=Certains contrΓ΄les requis n'ont pas rΓ©ussi. @@ -3718,13 +3721,14 @@ owner.settings.chef.keypair.description=Une paire de clΓ©s est nΓ©cessaire pour secrets=Secrets description=Les secrets seront transmis Γ certaines actions et ne pourront pas Γͺtre lus autrement. none=Il n'y a pas encore de secrets. -creation=Ajouter un secret + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Description creation.name_placeholder=CaractΓ¨res alphanumΓ©riques ou tirets bas uniquement, insensibles Γ la casse, ne peut commencer par GITEA_ ou GITHUB_. creation.value_placeholder=Entrez nβimporte quoi. Les blancs cernant seront taillΓ©s. creation.description_placeholder=DΓ©crire briΓ¨vement votre dΓ©pΓ΄t (optionnel). -creation.success=Le secret "%s" a Γ©tΓ© ajoutΓ©. -creation.failed=Impossible d'ajouter le secret. + + deletion=Supprimer le secret deletion.description=La suppression d'un secret est permanente et irrΓ©versible. Continuer ? deletion.success=Le secret a Γ©tΓ© supprimΓ©. diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index b3a4a43f9d..cdde7e015d 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -130,6 +130,7 @@ pin=BiorΓ‘in unpin=DΓphorΓ‘il artifacts=DΓ©antΓ‘in +expired=Imithe in Γ©ag confirm_delete_artifact=An bhfuil tΓΊ cinnte gur mhaith leat an dΓ©antΓ‘n '%s' a scriosadh? archived=Cartlann @@ -3720,13 +3721,14 @@ owner.settings.chef.keypair.description=TΓ‘ eochairphΓ©ire riachtanach le fΓord secrets=RΓΊin description=Cuirfear rΓΊin ar aghaidh chuig gnΓomhartha Γ‘irithe agus nΓ fΓ©idir iad a lΓ©amh ar mhalairt. none=NΓl aon rΓΊin ann fΓ³s. -creation=Cuir RΓΊnda leis + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Cur sΓos creation.name_placeholder=carachtair alfanumair nΓ³ Γoslaghda amhΓ‘in nach fΓ©idir a thosΓΊ le GITEA_ nΓ³ GITHUB_ creation.value_placeholder=Ionchur Γ‘bhar ar bith. FΓ‘gfar spΓ‘s bΓ‘n ag tΓΊs agus ag deireadh ar lΓ‘r. creation.description_placeholder=Cuir isteach cur sΓos gairid (roghnach). -creation.success=TΓ‘ an rΓΊn "%s" curtha leis. -creation.failed=Theip ar an rΓΊn a chur leis. + + deletion=Bain rΓΊn deletion.description=Is buan rΓΊn a bhaint agus nΓ fΓ©idir Γ© a chealΓΊ. Lean ort? deletion.success=TΓ‘ an rΓΊn bainte. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 0dae5505aa..ebc6d5c801 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1592,8 +1592,12 @@ conan.details.repository=TΓ‘rolΓ³ owner.settings.cleanuprules.enabled=EngedΓ©lyezett [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=LeΓrΓ‘s + + [actions] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 808ebaa9ec..54b0499d96 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -1394,8 +1394,12 @@ conan.details.repository=Repositori owner.settings.cleanuprules.enabled=Aktif [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Deskripsi + + [actions] diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 999b21c608..42ecfabe22 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1325,8 +1325,12 @@ npm.details.tag=Merki pypi.requires=Γarfnast Python [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=LΓ½sing + + [actions] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index f4a6767ea4..569d3f54e1 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -2782,8 +2782,12 @@ settings.delete.error=Impossibile eliminare il pacchetto. owner.settings.cleanuprules.enabled=Attivo [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Descrizione + + [actions] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index a6366565b2..7790dccd6b 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -3718,13 +3718,14 @@ owner.settings.chef.keypair.description=Chefγ¬γΈγΉγγͺγθͺθ¨Όγ«γ―γγΌ secrets=γ·γΌγ―γ¬γγ description=γ·γΌγ―γ¬γγγ―ηΉεγActionsγ«ζΈ‘γγγΎγγ γγδ»₯ε€γ§θͺγΏεΊγγγγγ¨γ―γγγΎγγγ none=γ·γΌγ―γ¬γγγ―γΎγ γγγΎγγγ -creation=γ·γΌγ―γ¬γγγθΏ½ε + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=θͺ¬ζ creation.name_placeholder=ε€§ζεε°ζεγεΊε₯γͺγγθ±ζ°εγ¨γ’γ³γγΌγΉγ³γ’γγΏγGITEA_ γ GITHUB_ γ§ε§γΎγγγγ―δΈε― creation.value_placeholder=ε
εΉγε
₯εγγ¦γγ γγγεεΎγη©Ίη½γ―ι€ε»γγγΎγγ creation.description_placeholder=η°‘εγͺθͺ¬ζγε
₯εγγ¦γγ γγγ (γͺγγ·γ§γ³) -creation.success=γ·γΌγ―γ¬γγ "%s" γθΏ½ε γγΎγγγ -creation.failed=γ·γΌγ―γ¬γγγθΏ½ε γ«ε€±ζγγΎγγγ + + deletion=γ·γΌγ―γ¬γγγει€ deletion.description=γ·γΌγ―γ¬γγγει€γ―ζδΉ
ηγ§ε
γ«ζ»γγγ¨γ―γ§γγΎγγγ ηΆθ‘γγΎγγοΌ deletion.success=γ·γΌγ―γ¬γγγει€γγΎγγγ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 08f6d723de..22bf3e1641 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1542,8 +1542,12 @@ conan.details.repository=μ μ₯μ owner.settings.cleanuprules.enabled=νμ±νλ¨ [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=μ€λͺ
+ + [actions] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 718ca0594e..a746f8738c 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -3332,12 +3332,13 @@ owner.settings.chef.keypair.description=AtslΔgu pΔris ir nepiecieΕ‘ams, lai au secrets=NoslΔpumi description=NoslΔpumi tiks padoti atseviΕ‘Δ·Δm darbΔ«bΔm un citΔdi nevar tikt nolasΔ«ti. none=PagaidΔm nav neviena noslΔpuma. -creation=Pievienot noslΔpumu + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Apraksts creation.name_placeholder=reΔ£istr-nejΕ«tΔ«gs, tikai burti, cipari un apakΕ‘svΔ«tras, nevar sΔkties ar GITEA_ vai GITHUB_ creation.value_placeholder=Ievadiet jebkΔdu saturu. Atstarpes sΔkumΔ un beigΔ tiks noΕemtas. -creation.success=NoslΔpums "%s" tika pievienots. -creation.failed=NeizdevΔs pievienot noslΔpumu. + + deletion=DzΔst noslΔpumu deletion.description=NoslΔpuma dzΔΕ‘ana ir neatgriezeniska. Vai turpinΔt? deletion.success=NoslΔpums tika izdzΔsts. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index eff4c1f85f..b6887ee9e0 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -2515,8 +2515,12 @@ settings.link.button=Repository link bijwerken owner.settings.cleanuprules.enabled=Ingeschakeld [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Omschrijving + + [actions] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index b45f0fc8e0..42a33f9ce4 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2405,8 +2405,12 @@ conan.details.repository=Repozytorium owner.settings.cleanuprules.enabled=WΕΔ
czone [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Opis + + [actions] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 75d425417c..8ee675e6e0 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -3269,12 +3269,13 @@ owner.settings.chef.keypair=Gerar par de chaves secrets=Segredos description=Os segredos serΓ£o passados a certas aΓ§Γ΅es e nΓ£o poderΓ£o ser lidos de outra forma. none=NΓ£o hΓ‘ segredos ainda. -creation=Adicionar Segredo + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=DescriΓ§Γ£o creation.name_placeholder=apenas caracteres alfanumΓ©ricos ou underline (_), nΓ£o pode comeΓ§ar com GITEA_ ou GITHUB_ creation.value_placeholder=Insira qualquer conteΓΊdo. EspaΓ§os em branco no inΓcio e no fim serΓ£o omitidos. -creation.success=O segredo "%s" foi adicionado. -creation.failed=Falha ao adicionar segredo. + + deletion=Excluir segredo deletion.description=A exclusΓ£o de um segredo Γ© permanente e nΓ£o pode ser desfeita. Continuar? deletion.success=O segredo foi excluΓdo. diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 7d00fb81c7..b47b61f6bd 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -130,6 +130,7 @@ pin=Fixar unpin=Desafixar artifacts=Artefactos +expired=Expirado confirm_delete_artifact=Tem a certeza que quer eliminar este artefacto "%s"? archived=Arquivado @@ -1879,6 +1880,7 @@ pulls.add_prefix=Adicione o prefixo <strong>%s</strong> pulls.remove_prefix=Remover o prefixo <strong>%s</strong> pulls.data_broken=Este pedido de integraΓ§Γ£o estΓ‘ danificado devido Γ falta de informaΓ§Γ£o da derivaΓ§Γ£o. pulls.files_conflicted=Este pedido de integraΓ§Γ£o contΓ©m modificaΓ§Γ΅es que entram em conflito com o ramo de destino. +pulls.is_checking=Verificando se existem conflitos na integraΓ§Γ£o... pulls.is_ancestor=Este ramo jΓ‘ estΓ‘ incluΓdo no ramo de destino. NΓ£o hΓ‘ nada a integrar. pulls.is_empty=As modificaΓ§Γ΅es feitas neste ramo jΓ‘ existem no ramo de destino. Este cometimento ficarΓ‘ vazio. pulls.required_status_check_failed=Algumas das verificaΓ§Γ΅es obrigatΓ³rias nΓ£o foram bem sucedidas. @@ -3719,13 +3721,18 @@ owner.settings.chef.keypair.description=Γ necessΓ‘rio um par de chaves para aut secrets=Segredos description=Os segredos serΓ£o transmitidos a certas operaΓ§Γ΅es e nΓ£o poderΓ£o ser lidos de outra forma. none=Ainda nΓ£o hΓ‘ segredos. -creation=Adicionar segredo + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=DescriΓ§Γ£o creation.name_placeholder=SΓ³ sublinhados ou alfanumΓ©ricos sem distinguir maiΓΊsculas, sem comeΓ§ar com GITEA_ nem GITHUB_ creation.value_placeholder=Insira um conteΓΊdo qualquer. EspaΓ§os em branco no inΓcio ou no fim serΓ£o omitidos. creation.description_placeholder=Escreva uma descriΓ§Γ£o curta (opcional). -creation.success=O segredo "%s" foi adicionado. -creation.failed=Falhou ao adicionar o segredo. + +save_success=O segredo "%s" foi guardado. +save_failed=Falhou ao guardar o segredo. + +add_secret=Adicionar segredo +edit_secret=Editar segredo deletion=Remover segredo deletion.description=Remover um segredo Γ© permanente e nΓ£o pode ser revertido. Continuar? deletion.success=O segredo foi removido. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 879d7c6145..c65d08a4cf 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -3266,12 +3266,13 @@ owner.settings.chef.keypair=Π‘ΠΎΠ·Π΄Π°ΡΡ ΠΏΠ°ΡΡ ΠΊΠ»ΡΡΠ΅ΠΉ secrets=Π‘Π΅ΠΊΡΠ΅ΡΡ description=Π‘Π΅ΠΊΡΠ΅ΡΡ Π±ΡΠ΄ΡΡ ΠΏΠ΅ΡΠ΅Π΄Π°Π²Π°ΡΡΡΡ ΠΎΠΏΡΠ΅Π΄Π΅Π»Π΅Π½Π½ΡΠΌ Π΄Π΅ΠΉΡΡΠ²ΠΈΡΠΌ ΠΈ Π½Π΅ ΠΌΠΎΠ³ΡΡ Π±ΡΡΡ ΠΏΡΠΎΡΠΈΡΠ°Π½Ρ ΠΈΠ½Π°ΡΠ΅. none=Π‘Π΅ΠΊΡΠ΅ΡΠΎΠ² ΠΏΠΎΠΊΠ° Π½Π΅Ρ. -creation=ΠΠΎΠ±Π°Π²ΠΈΡΡ ΡΠ΅ΠΊΡΠ΅Ρ + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ creation.name_placeholder=ΡΠ΅Π³ΠΈΡΡΡ Π½Π΅ Π²Π°ΠΆΠ΅Π½, ΡΠΎΠ»ΡΠΊΠΎ Π°Π»ΡΠ°Π²ΠΈΡΠ½ΠΎ-ΡΠΈΡΡΠΎΠ²ΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ ΠΈ ΠΏΠΎΠ΄ΡΡΡΠΊΠΈΠ²Π°Π½ΠΈΡ, Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ Π½Π°ΡΠΈΠ½Π°ΡΡΡΡ Ρ GITEA_ ΠΈΠ»ΠΈ GITHUB_ creation.value_placeholder=ΠΠ²Π΅Π΄ΠΈΡΠ΅ Π»ΡΠ±ΠΎΠ΅ ΡΠΎΠ΄Π΅ΡΠΆΠΈΠΌΠΎΠ΅. ΠΡΠΎΠ±Π΅Π»ΡΠ½ΡΠ΅ ΡΠΈΠΌΠ²ΠΎΠ»Ρ Π² Π½Π°ΡΠ°Π»Π΅ ΠΈ ΠΊΠΎΠ½ΡΠ΅ Π±ΡΠ΄ΡΡ ΠΎΠΏΡΡΠ΅Π½Ρ. -creation.success=Π‘Π΅ΠΊΡΠ΅Ρ Β«%sΒ» Π΄ΠΎΠ±Π°Π²Π»Π΅Π½. -creation.failed=ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ ΡΠ΅ΠΊΡΠ΅Ρ. + + deletion=Π£Π΄Π°Π»ΠΈΡΡ ΡΠ΅ΠΊΡΠ΅Ρ deletion.description=Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ ΡΠ΅ΠΊΡΠ΅ΡΠ° Π½Π΅ΠΎΠ±ΡΠ°ΡΠΈΠΌΠΎ, Π΅Π³ΠΎ Π½Π΅Π»ΡΠ·Ρ ΠΎΡΠΌΠ΅Π½ΠΈΡΡ. ΠΡΠΎΠ΄ΠΎΠ»ΠΆΠΈΡΡ? deletion.success=Π‘Π΅ΠΊΡΠ΅Ρ ΡΠ΄Π°Π»ΡΠ½. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 042e8ad21b..a209187aff 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -2447,8 +2447,12 @@ conan.details.repository=ΰΆΰ·ΰ·ΰ·ΰΆ¨ΰΆΊ owner.settings.cleanuprules.enabled=ΰ·ΰΆΆΰΆ½ ΰΆΰΆ» ΰΆΰΆ [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=ΰ·ΰ·ΰ·ΰ·ΰ·ΰΆΰΆ»ΰΆΊ + + [actions] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index b1dae7c490..e461075e53 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1321,6 +1321,10 @@ owner.settings.cleanuprules.enabled=PovolenΓ© [secrets] +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation + + + [actions] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 6fb5a9c4cb..04428aeab2 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1982,8 +1982,12 @@ conan.details.repository=Utvecklingskatalog owner.settings.cleanuprules.enabled=Aktiv [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Beskrivning + + [actions] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index acd0892eba..d617598057 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -3525,12 +3525,13 @@ owner.settings.chef.keypair.description=Chef kΓΌtΓΌΔΓΌnde kimlik doΔrulamasΔ± secrets=Gizlilikler description=Gizlilikler belirli iΕlemlere aktarΔ±lacaktΔ±r, bunun dΔ±ΕΔ±nda okunamaz. none=HenΓΌz gizlilik yok. -creation=Gizlilik Ekle + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=AΓ§Δ±klama creation.name_placeholder=kΓΌΓ§ΓΌk-bΓΌyΓΌk harfe duyarlΔ± deΔil, alfanΓΌmerik karakterler veya sadece alt tire, GITEA_ veya GITHUB_ ile baΕlayamaz creation.value_placeholder=Herhangi bir iΓ§erik girin. BaΕtaki ve sondaki boΕluklar ihmal edilecektir. -creation.success=Gizlilik "%s" eklendi. -creation.failed=Gizlilik eklenemedi. + + deletion=GizliliΔi kaldΔ±r deletion.description=Bir gizliliΔi kaldΔ±rma kalΔ±cΔ±dΔ±r ve geri alΔ±namaz. Devam edilsin mi? deletion.success=Gizlilik kaldΔ±rΔ±ldΔ±. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 3a6d1539fa..6aed70491b 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2517,8 +2517,12 @@ conan.details.repository=Π Π΅ΠΏΠΎΠ·ΠΈΡΠΎΡΡΠΉ owner.settings.cleanuprules.enabled=Π£Π²ΡΠΌΠΊΠ½Π΅Π½ΠΎ [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=ΠΠΏΠΈΡ + + [actions] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 0e7db6350c..f6d6183e52 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -3717,13 +3717,14 @@ owner.settings.chef.keypair.description=ιθ¦ε―ι₯ε―Ήζθ½ε Chef 注εδΈε secrets=ε―ι₯ description=Secrets ε°θ’«δΌ η»ηΉεη ActionsοΌε
Άεζ
ε΅ε°δΈθ½θ―»ε none=θΏζ²‘ζε―ι₯γ -creation=ζ·»ε ε―ι₯ + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=η»η»ζθΏ° creation.name_placeholder=δΈεΊεε€§ε°εοΌεζ―ζ°εζδΈεηΊΏδΈθ½δ»₯GITEA_ ζ GITHUB_ εΌε€΄γ creation.value_placeholder=θΎε
₯δ»»δ½ε
εΉοΌεΌε€΄εη»ε°Ύηη©Ίη½ι½δΌθ’«ηη₯ creation.description_placeholder=θΎε
₯ηηζθΏ°(ε―ι)γ -creation.success=ζ¨ηε―ι₯ '%s' ζ·»ε ζεγ -creation.failed=ζ·»ε ε―ι₯ε€±θ΄₯γ + + deletion=ε ι€ε―ι₯ deletion.description=ε ι€ε―ι₯ζ―ζ°ΈδΉ
ζ§ηοΌζ ζ³ζ€ζΆγη»§η»εοΌ deletion.success=ζ€Secretε·²θ’«ε ι€γ diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index b157a44c69..2874da3170 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -959,8 +959,12 @@ conan.details.repository=ε²εεΊ« owner.settings.cleanuprules.enabled=ε·²εη¨ [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=η΅ηΉζθΏ° + + [actions] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 3b25c81be3..a52e147415 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -3635,12 +3635,13 @@ owner.settings.chef.keypair.description=ι©θ Chef 註εδΈεΏιθ¦δΈεε―ι secrets=Secret description=Secret ζθ’«ε³η΅¦ηΉεη ActionοΌε
Άδ»ζ
ζ³η‘ζ³θεγ none=ιζ²ζ Secretγ -creation=ε ε
₯ Secret + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=ζθΏ° creation.name_placeholder=δΈεεε€§ε°ε―«οΌεͺθ½ε
ε«θ±ζεζ―γζΈεγεΊη· ('_')οΌδΈθ½δ»₯ GITEA_ ζ GITHUB_ ιι γ creation.value_placeholder=θΌΈε
₯δ»»δ½ε
§εΉοΌι ε°Ύηη©Ίη½ι½ζθ’«εΏ½η₯γ -creation.success=ε·²ζ°ε’ Secretγ%sγγ -creation.failed=ε ε
₯ Secret ε€±ζγ + + deletion=η§»ι€ Secret deletion.description=η§»ι€ Secret ζ―ζ°ΈδΉ
ηδΈδΈε―ιεοΌζ―ε¦ηΉΌηΊοΌ deletion.success=ε·²η§»ι€ζ€ Secretγ diff --git a/package-lock.json b/package-lock.json index 4c5963d0c8..e61fe3472d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@citation-js/plugin-csl": "0.7.18", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.7", + "@github/relative-time-element": "4.4.8", "@github/text-expander-element": "2.9.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.15.1", @@ -1146,9 +1146,9 @@ "license": "MIT" }, "node_modules/@github/relative-time-element": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.7.tgz", - "integrity": "sha512-NZCePEFYtV7qAUI/pHYuqZ8vRhcsfH/dziUZTY9YR5+JwzDCWtEokYSDbDLZjrRl+SAFr02YHUK+UdtP6hPcbQ==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.8.tgz", + "integrity": "sha512-FSLYm6F3TSQnqHE1EMQUVVgi2XjbCvsESwwXfugHFpBnhyF1uhJOtu0Psp/BB/qqazfdkk7f5fVcu7WuXl3t8Q==", "license": "MIT" }, "node_modules/@github/text-expander-element": { diff --git a/package.json b/package.json index 0202b92ff4..bc2c0c87f3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@citation-js/plugin-csl": "0.7.18", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.7", + "@github/relative-time-element": "4.4.8", "@github/text-expander-element": "2.9.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.15.1", diff --git a/routers/private/serv.go b/routers/private/serv.go index 37fbc0730c..b879be0dc2 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -81,6 +81,7 @@ func ServCommand(ctx *context.PrivateContext) { ownerName := ctx.PathParam("owner") repoName := ctx.PathParam("repo") mode := perm.AccessMode(ctx.FormInt("mode")) + verb := ctx.FormString("verb") // Set the basic parts of the results to return results := private.ServCommandResults{ @@ -295,8 +296,11 @@ func ServCommand(ctx *context.PrivateContext) { return } } else { - // Because of the special ref "refs/for" we will need to delay write permission check - if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode { + // Because of the special ref "refs/for" (AGit) we will need to delay write permission check, + // AGit flow needs to write its own ref when the doer has "reader" permission (allowing to create PR). + // The real permission check is done in HookPreReceive (routers/private/hook_pre_receive.go). + // Here it should relax the permission check for "git push (git-receive-pack)", but not for others like LFS operations. + if git.DefaultFeatures().SupportProcReceive && unitType == unit.TypeCode && verb == git.CmdVerbReceivePack { mode = perm.AccessModeRead } diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index b04855fa6a..7c59132841 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -201,7 +201,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio switch act.OpType { case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush: push := templates.ActionContent2Commits(act) - + _ = act.LoadRepo(ctx) for _, commit := range push.Commits { if len(desc) != 0 { desc += "\n\n" @@ -209,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s", html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)), commit.Sha1, - renderUtils.RenderCommitMessage(commit.Message, nil), + renderUtils.RenderCommitMessage(commit.Message, act.Repo), ) } diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 2ec6389263..dd18c8380d 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -200,13 +200,9 @@ func ViewPost(ctx *context_module.Context) { } } - // TODO: "ComposeCommentMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does. - // need to be refactored together in the future - metas := ctx.Repo.Repository.ComposeCommentMetas(ctx) - // the title for the "run" is from the commit message resp.State.Run.Title = run.Title - resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas) + resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository) resp.State.Run.Link = run.Link() resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index c8b80ebb26..29f4e9520d 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -32,11 +32,11 @@ func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description) if err != nil { log.Error("CreateOrUpdateSecret failed: %v", err) - ctx.JSONError(ctx.Tr("secrets.creation.failed")) + ctx.JSONError(ctx.Tr("secrets.save_failed")) return } - ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name)) + ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name)) ctx.JSONRedirect(redirectURL) } diff --git a/services/repository/branch.go b/services/repository/branch.go index 94c47ffdc4..dd00ca7dcd 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -663,6 +663,11 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, newB } } + // clear divergence cache + if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil { + log.Error("DelRepoDivergenceFromCache: %v", err) + } + notify_service.ChangeDefaultBranch(ctx, repo) return nil diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index f348cb68ab..68a071cd28 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -107,6 +107,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } var attributesMap map[string]*attribute.Attributes + // when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist if setting.LFS.StartServer { attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ Attributes: []string{attribute.Filter}, @@ -118,6 +119,12 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Copy uploaded files into repository. + // TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop, + // so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future: + // 1. Add ".gitattributes" to git index + // 2. Run "check-attr" (the previous attribute.CheckAttributes call) + // 3. Add files to git index (this loop) + // This problem is trivial so maybe no need to spend too much time on it at the moment. for i := range infos { if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil { return err diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 15683307ed..91b84e13b6 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -15,7 +15,7 @@ </div> <div class="required inline field {{if .Err_Name}}error{{end}}"> <label for="auth_name">{{ctx.Locale.Tr "admin.auths.auth_name"}}</label> - <input id="auth_name" name="name" value="{{.Source.Name}}" autofocus required> + <input id="auth_name" name="name" value="{{.Source.Name}}" required> </div> <div class="inline field"> <div class="ui checkbox"> diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index c04d332660..879b5cb550 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -9,7 +9,7 @@ {{.CsrfTokenHtml}} <div class="field {{if .Err_UserName}}error{{end}}"> <label for="user_name">{{ctx.Locale.Tr "username"}}</label> - <input id="user_name" name="user_name" value="{{.User.Name}}" autofocus {{if not .User.IsLocal}}disabled{{end}} maxlength="40"> + <input id="user_name" name="user_name" value="{{.User.Name}}" {{if not .User.IsLocal}}disabled{{end}} maxlength="40"> </div> <!-- Types and name --> <div class="inline required field {{if .Err_LoginType}}error{{end}}"> @@ -55,7 +55,7 @@ <div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}tw-hidden{{end}}"> <label for="login_name">{{ctx.Locale.Tr "admin.users.auth_login_name"}}</label> - <input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus> + <input id="login_name" name="login_name" value="{{.User.LoginName}}"> </div> <div class="field {{if .Err_FullName}}error{{end}}"> <label for="full_name">{{ctx.Locale.Tr "settings.full_name"}}</label> @@ -63,7 +63,7 @@ </div> <div class="required field {{if .Err_Email}}error{{end}}"> <label for="email">{{ctx.Locale.Tr "email"}}</label> - <input id="email" name="email" type="email" value="{{.User.Email}}" autofocus required> + <input id="email" name="email" type="email" value="{{.User.Email}}" required> </div> <div class="local field {{if .Err_Password}}error{{end}} {{if not (or (.User.IsLocal) (.User.IsOAuth2))}}tw-hidden{{end}}"> <label for="password">{{ctx.Locale.Tr "password"}}</label> diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 76315f3eac..f4583bbe36 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -12,7 +12,7 @@ <br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}} </span> </label> - <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required maxlength="40"> + <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40"> </div> <div class="field {{if .Err_FullName}}error{{end}}"> <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 19797229bf..fffe3a08cc 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -20,14 +20,14 @@ <tr> <td> <div class="flex-text-block"> - <a class="gt-ellipsis" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a> + <a class="gt-ellipsis branch-name" href="{{.RepoLink}}/src/branch/{{PathEscapeSegments .DefaultBranchBranch.DBBranch.Name}}">{{.DefaultBranchBranch.DBBranch.Name}}</a> {{if .DefaultBranchBranch.IsProtected}} <span data-tooltip-content="{{ctx.Locale.Tr "repo.settings.protected_branch"}}">{{svg "octicon-shield-lock"}}</span> {{end}} <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}} </div> - <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> Β· <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeCommentMetas ctx)}}</span> Β· {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> + <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> Β· <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage .Repository}}</span> Β· {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p> </td> {{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}} <td class="tw-text-right tw-overflow-visible"> @@ -90,20 +90,20 @@ <td class="eight wide"> {{if .DBBranch.IsDeleted}} <div class="flex-text-block"> - <span class="gt-ellipsis">{{.DBBranch.Name}}</span> + <span class="gt-ellipsis branch-name">{{.DBBranch.Name}}</span> <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> </div> <p class="info">{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{DateUtils.TimeSince .DBBranch.DeletedUnix}}</p> {{else}} <div class="flex-text-block"> - <a class="gt-ellipsis" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> + <a class="gt-ellipsis branch-name" href="{{$.RepoLink}}/src/branch/{{PathEscapeSegments .DBBranch.Name}}">{{.DBBranch.Name}}</a> {{if .IsProtected}} <span data-tooltip-content="{{ctx.Locale.Tr "repo.settings.protected_branch"}}">{{svg "octicon-shield-lock"}}</span> {{end}} <button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button> {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}} </div> - <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> Β· <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeCommentMetas ctx)}}</span> Β· {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> + <p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> Β· <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage $.Repository}}</span> Β· {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p> {{end}} </td> <td class="two wide ui"> diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 5639c87a82..7abd377108 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -5,7 +5,7 @@ <div class="ui container fluid padded"> <div class="ui top attached header clearing segment tw-relative commit-header"> <div class="tw-flex tw-mb-4 tw-gap-1"> - <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> + <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message $.Repository}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> {{if not $.PageIsWiki}} <div class="commit-header-buttons"> <a class="ui primary tiny button" href="{{.SourcePath}}"> @@ -122,7 +122,7 @@ {{end}} </div> {{if IsMultilineCommitMessage .Commit.Message}} - <pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre> + <pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message $.Repository}}</pre> {{end}} {{template "repo/commit_load_branches_and_tags" .}} </div> diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 17c7240ee4..8a268a5d14 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -44,7 +44,7 @@ <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span> {{else}} {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}} - <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span> + <span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink $.Repository}}</span> {{end}} </span> {{if IsMultilineCommitMessage .Message}} @@ -52,7 +52,7 @@ {{end}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} {{if IsMultilineCommitMessage .Message}} - <pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}</pre> + <pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message $.Repository}}</pre> {{end}} {{if $.CommitsTagsMap}} {{range (index $.CommitsTagsMap .ID.String)}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index b054ce19a5..ee94ad7e58 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -15,7 +15,7 @@ {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} <span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}"> - {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}} + {{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink $.comment.Issue.PullRequest.BaseRepo -}} </span> {{if IsMultilineCommitMessage .Message}} @@ -29,7 +29,7 @@ </div> {{if IsMultilineCommitMessage .Message}} <pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}"> - {{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}} + {{- ctx.RenderUtils.RenderCommitBody .Message $.comment.Issue.PullRequest.BaseRepo -}} </pre> {{end}} {{end}} diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 6f16ce3bd8..4e8ad1326c 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -189,7 +189,7 @@ <div class="ui segment flex-text-block tw-gap-4"> {{template "shared/issueicon" .}} <div class="issue-title tw-break-anywhere"> - {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeCommentMetas ctx)}} + {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title $.Repository}} <span class="index">#{{.PullRequest.Issue.Index}}</span> </div> <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index 630c4579ea..34167cadc0 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -8,7 +8,7 @@ {{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}} <span class="message tw-inline-block gt-ellipsis"> - <span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeCommentMetas ctx)}}</span> + <span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject $.Repository}}</span> </span> <span class="commit-refs flex-text-inline"> diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index a4be598540..b8f28dfd9b 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -13,7 +13,7 @@ {{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} <div class="issue-title" id="issue-title-display"> <h1 class="tw-break-anywhere"> - {{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeCommentMetas ctx)}} + {{ctx.RenderUtils.RenderIssueTitle .Issue.Title $.Repository}} <span class="index">#{{.Issue.Index}}</span> </h1> <div class="issue-title-buttons"> diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index da457e423a..cff338949f 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -21,10 +21,10 @@ {{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}} {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} - <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span> + <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink $.Repository}}</span> {{if IsMultilineCommitMessage .LatestCommit.Message}} <button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button> - <pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre> + <pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message $.Repository}}</pre> {{end}} </span> {{end}} diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index 4461398258..7064b4c7ba 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -90,7 +90,7 @@ <form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post"> {{.CsrfTokenHtml}} <div id="search-team-box" class="ui search input tw-align-middle" data-org-name="{{.OrgName}}"> - <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" autofocus required> + <input class="prompt" name="team" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" autocomplete="off" required> </div> <button class="ui primary button">{{ctx.Locale.Tr "repo.settings.add_team"}}</button> </form> diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 4d61604612..fc42056e0a 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -10,7 +10,7 @@ <input type="hidden" name="action" value="update"> <div class="required field {{if .Err_RepoName}}error{{end}}"> <label>{{ctx.Locale.Tr "repo.repo_name"}}</label> - <input name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" autofocus required> + <input name="repo_name" value="{{.Repository.Name}}" data-repo-name="{{.Repository.Name}}" required> </div> <div class="inline field"> <label>{{ctx.Locale.Tr "repo.repo_size"}}</label> diff --git a/templates/repo/view.tmpl b/templates/repo/view.tmpl index c3d562003d..85d09d03a1 100644 --- a/templates/repo/view.tmpl +++ b/templates/repo/view.tmpl @@ -17,7 +17,7 @@ {{template "repo/code/recently_pushed_new_branches" .}} <div class="repo-view-container"> - <div class="repo-view-file-tree-container not-mobile {{if not .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" {{if .IsSigned}}data-user-is-signed-in{{end}}> + <div class="tw-flex tw-flex-col repo-view-file-tree-container not-mobile {{if not .UserSettingCodeViewShowFileTree}}tw-hidden{{end}}" {{if .IsSigned}}data-user-is-signed-in{{end}}> {{template "repo/view_file_tree" .}} </div> <div class="repo-view-content"> diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index cd832498b4..c8ee059e89 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -47,7 +47,7 @@ <div class="repo-file-cell message loading-icon-2px"> {{if $commit}} {{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink $.Repository}} {{else}} β¦ {{/* will be loaded again by LastCommitLoaderURL */}} {{end}} diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl index 977f308b71..a4ef2e5384 100644 --- a/templates/shared/secrets/add_list.tmpl +++ b/templates/shared/secrets/add_list.tmpl @@ -4,9 +4,13 @@ <button class="ui primary tiny button show-modal" data-modal="#add-secret-modal" data-modal-form.action="{{.Link}}" - data-modal-header="{{ctx.Locale.Tr "secrets.creation"}}" + data-modal-header="{{ctx.Locale.Tr "secrets.add_secret"}}" + data-modal-secret-name.value="" + data-modal-secret-name.read-only="false" + data-modal-secret-data="" + data-modal-secret-description="" > - {{ctx.Locale.Tr "secrets.creation"}} + {{ctx.Locale.Tr "secrets.add_secret"}} </button> </div> </h4> @@ -33,6 +37,18 @@ <span class="color-text-light-2"> {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} </span> + <button class="ui btn interact-bg show-modal tw-p-2" + data-modal="#add-secret-modal" + data-modal-form.action="{{$.Link}}" + data-modal-header="{{ctx.Locale.Tr "secrets.edit_secret"}}" + data-tooltip-content="{{ctx.Locale.Tr "secrets.edit_secret"}}" + data-modal-secret-name.value="{{.Name}}" + data-modal-secret-name.read-only="true" + data-modal-secret-data="" + data-modal-secret-description="{{if .Description}}{{.Description}}{{end}}" + > + {{svg "octicon-pencil"}} + </button> <button class="ui btn interact-bg link-action tw-p-2" data-url="{{$.Link}}/delete?id={{.ID}}" data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}" @@ -51,9 +67,7 @@ {{/* Add secret dialog */}} <div class="ui small modal" id="add-secret-modal"> - <div class="header"> - <span id="actions-modal-header"></span> - </div> + <div class="header"></div> <form class="ui form form-fetch-action" method="post"> <div class="content"> {{.CsrfTokenHtml}} diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 97291fc42d..4ee12fa783 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -94,7 +94,7 @@ <img alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> <a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> <span class="text truncate"> - {{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeCommentMetas ctx)}} + {{ctx.RenderUtils.RenderCommitMessage .Message $repo}} </span> </div> {{end}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 03c3c18f28..d8e5e27b89 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -12,7 +12,7 @@ <span class="text red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span> <span class="text red tw-hidden" id="name-change-redirect-prompt"> {{ctx.Locale.Tr "settings.change_username_redirect_prompt"}}</span> </label> - <input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}disabled{{end}} maxlength="40"> + <input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" required {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}}disabled{{end}} maxlength="40"> {{if or (not .SignedUser.IsLocal) ($.UserDisabledFeatures.Contains "change_username") .IsReverseProxy}} <p class="help text blue">{{ctx.Locale.Tr "settings.password_username_disabled"}}</p> {{end}} diff --git a/tests/integration/change_default_branch_test.go b/tests/integration/change_default_branch_test.go index 729eb1e4ce..9b61cff9fd 100644 --- a/tests/integration/change_default_branch_test.go +++ b/tests/integration/change_default_branch_test.go @@ -6,12 +6,16 @@ package integration import ( "fmt" "net/http" + "strconv" "testing" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestChangeDefaultBranch(t *testing.T) { @@ -38,3 +42,96 @@ func TestChangeDefaultBranch(t *testing.T) { }) session.MakeRequest(t, req, http.StatusNotFound) } + +func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]git.DivergeObject) { + req := NewRequest(t, "GET", branchesURL) + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + + branchNodes := htmlDoc.doc.Find(".branch-name").Nodes + branchNames := []string{} + for _, node := range branchNodes { + branchNames = append(branchNames, node.FirstChild.Data) + } + + expectBranchCount := len(expectedBranchToDivergence) + + assert.Len(t, branchNames, expectBranchCount+1) + assert.Equal(t, expectedDefaultBranch, branchNames[0]) + + allCountBehindNodes := htmlDoc.doc.Find(".count-behind").Nodes + allCountAheadNodes := htmlDoc.doc.Find(".count-ahead").Nodes + + assert.Len(t, allCountAheadNodes, expectBranchCount) + assert.Len(t, allCountBehindNodes, expectBranchCount) + + for i := range expectBranchCount { + branchName := branchNames[i+1] + assert.Contains(t, expectedBranchToDivergence, branchName) + + expectedCountAhead := expectedBranchToDivergence[branchName].Ahead + expectedCountBehind := expectedBranchToDivergence[branchName].Behind + countAhead, err := strconv.Atoi(allCountAheadNodes[i].FirstChild.Data) + assert.NoError(t, err) + countBehind, err := strconv.Atoi(allCountBehindNodes[i].FirstChild.Data) + assert.NoError(t, err) + + assert.Equal(t, expectedCountAhead, countAhead) + assert.Equal(t, expectedCountBehind, countBehind) + } +} + +func TestChangeDefaultBranchDivergence(t *testing.T) { + defer tests.PrepareTestEnv(t)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + session := loginUser(t, owner.Name) + branchesURL := fmt.Sprintf("/%s/%s/branches", owner.Name, repo.Name) + settingsBranchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name) + + // check branch divergence before switching default branch + expectedBranchToDivergenceBefore := map[string]git.DivergeObject{ + "not-signed": { + Ahead: 0, + Behind: 0, + }, + "good-sign-not-yet-validated": { + Ahead: 0, + Behind: 1, + }, + "good-sign": { + Ahead: 1, + Behind: 3, + }, + } + checkDivergence(t, session, branchesURL, "master", expectedBranchToDivergenceBefore) + + // switch default branch + newDefaultBranch := "good-sign-not-yet-validated" + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", settingsBranchesURL, map[string]string{ + "_csrf": csrf, + "action": "default_branch", + "branch": newDefaultBranch, + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // check branch divergence after switching default branch + expectedBranchToDivergenceAfter := map[string]git.DivergeObject{ + "master": { + Ahead: 1, + Behind: 0, + }, + "not-signed": { + Ahead: 1, + Behind: 0, + }, + "good-sign": { + Ahead: 1, + Behind: 2, + }, + } + checkDivergence(t, session, branchesURL, newDefaultBranch, expectedBranchToDivergenceAfter) +} diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index 34fe212d50..ed60bdb58a 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -11,8 +11,10 @@ import ( "net/http" "net/url" "os" + "os/exec" "path" "path/filepath" + "slices" "strconv" "testing" "time" @@ -30,6 +32,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" + "github.com/kballard/go-shellquote" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -105,7 +108,12 @@ func testGitGeneral(t *testing.T, u *url.URL) { // Setup key the user ssh key withKeyFile(t, keyname, func(keyFile string) { - t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) + var keyID int64 + t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile, func(t *testing.T, key api.PublicKey) { + keyID = key.ID + })) + assert.NotZero(t, keyID) + t.Run("LFSAccessTest", doSSHLFSAccessTest(sshContext, keyID)) // Setup remote link // TODO: get url from api @@ -136,6 +144,36 @@ func testGitGeneral(t *testing.T, u *url.URL) { }) } +func doSSHLFSAccessTest(_ APITestContext, keyID int64) func(*testing.T) { + return func(t *testing.T) { + sshCommand := os.Getenv("GIT_SSH_COMMAND") // it is set in withKeyFile + sshCmdParts, err := shellquote.Split(sshCommand) // and parse the ssh command to construct some mocked arguments + require.NoError(t, err) + + t.Run("User2AccessOwned", func(t *testing.T) { + sshCmdUser2Self := append(slices.Clone(sshCmdParts), + "-p", strconv.Itoa(setting.SSH.ListenPort), "git@"+setting.SSH.ListenHost, + "git-lfs-authenticate", "user2/repo1.git", "upload", // accessible to own repo + ) + cmd := exec.CommandContext(t.Context(), sshCmdUser2Self[0], sshCmdUser2Self[1:]...) + _, err := cmd.Output() + assert.NoError(t, err) // accessible, no error + }) + + t.Run("User2AccessOther", func(t *testing.T) { + sshCmdUser2Other := append(slices.Clone(sshCmdParts), + "-p", strconv.Itoa(setting.SSH.ListenPort), "git@"+setting.SSH.ListenHost, + "git-lfs-authenticate", "user5/repo4.git", "upload", // inaccessible to other's (user5/repo4) + ) + cmd := exec.CommandContext(t.Context(), sshCmdUser2Other[0], sshCmdUser2Other[1:]...) + _, err := cmd.Output() + var errExit *exec.ExitError + require.ErrorAs(t, err, &errExit) // inaccessible, error + assert.Contains(t, string(errExit.Stderr), fmt.Sprintf("User: 2:user2 with Key: %d:test-key is not authorized to write to user5/repo4.", keyID)) + }) + } +} + func ensureAnonymousClone(t *testing.T, u *url.URL) { dstLocalPath := t.TempDir() t.Run("CloneAnonymous", doGitClone(dstLocalPath, u)) diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index dee0aa6176..504d2adacc 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -12,8 +12,6 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -40,40 +38,24 @@ func TestRepoCommits(t *testing.T) { func Test_ReposGitCommitListNotMaster(t *testing.T) { defer tests.PrepareTestEnv(t)() - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - // Login as User2. - session := loginUser(t, user.Name) - - // Test getting commits (Page 1) - req := NewRequestf(t, "GET", "/%s/repo16/commits/branch/master", user.Name) + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - commits := []string{} + var commits []string doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { - commitURL, exists := s.Attr("href") - assert.True(t, exists) - assert.NotEmpty(t, commitURL) + commitURL, _ := s.Attr("href") commits = append(commits, path.Base(commitURL)) }) + assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) - assert.Len(t, commits, 3) - assert.Equal(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", commits[0]) - assert.Equal(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", commits[1]) - assert.Equal(t, "5099b81332712fe655e34e8dd63574f503f61811", commits[2]) - - userNames := []string{} + var userHrefs []string doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) { - userPath, exists := s.Attr("href") - assert.True(t, exists) - assert.NotEmpty(t, userPath) - userNames = append(userNames, path.Base(userPath)) + userHref, _ := s.Attr("href") + userHrefs = append(userHrefs, userHref) }) - - assert.Len(t, userNames, 3) - assert.Equal(t, "User2", userNames[0]) - assert.Equal(t, "user21", userNames[1]) - assert.Equal(t, "User2", userNames[2]) + assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) } func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css index 835286b795..046010c6c8 100644 --- a/web_src/css/editor/combomarkdowneditor.css +++ b/web_src/css/editor/combomarkdowneditor.css @@ -100,67 +100,3 @@ border-bottom: 1px solid var(--color-secondary); padding-bottom: 1rem; } - -text-expander { - display: block; - position: relative; -} - -text-expander .suggestions { - position: absolute; - min-width: 180px; - padding: 0; - margin-top: 24px; - list-style: none; - background: var(--color-box-body); - border-radius: var(--border-radius); - border: 1px solid var(--color-secondary); - box-shadow: 0 .5rem 1rem var(--color-shadow); - z-index: 100; /* needs to be > 20 to be on top of dropzone's .dz-details */ -} - -text-expander .suggestions li { - display: flex; - align-items: center; - cursor: pointer; - padding: 4px 8px; - font-weight: var(--font-weight-medium); -} - -text-expander .suggestions li + li { - border-top: 1px solid var(--color-secondary-alpha-40); -} - -text-expander .suggestions li:first-child { - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -text-expander .suggestions li:last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); -} - -text-expander .suggestions li:only-child { - border-radius: var(--border-radius); -} - -text-expander .suggestions li:hover { - background: var(--color-hover); -} - -text-expander .suggestions .fullname { - font-weight: var(--font-weight-normal); - margin-left: 4px; - color: var(--color-text-light-1); -} - -text-expander .suggestions li[aria-selected="true"], -text-expander .suggestions li[aria-selected="true"] span { - background: var(--color-primary); - color: var(--color-primary-contrast); -} - -text-expander .suggestions img { - width: 24px; - height: 24px; - margin-right: 8px; -} diff --git a/web_src/css/features/expander.css b/web_src/css/features/expander.css new file mode 100644 index 0000000000..f560b2a9fd --- /dev/null +++ b/web_src/css/features/expander.css @@ -0,0 +1,96 @@ +text-expander .suggestions, +.tribute-container { + position: absolute; + max-height: min(300px, 95vh); + max-width: min(500px, 95vw); + overflow-x: hidden; + overflow-y: auto; + white-space: nowrap; + background: var(--color-menu); + box-shadow: 0 6px 18px var(--color-shadow); + border-radius: var(--border-radius); + border: 1px solid var(--color-secondary); + z-index: 100; /* needs to be > 20 to be on top of dropzone's .dz-details */ +} + +text-expander { + display: block; + position: relative; +} + +text-expander .suggestions { + padding: 0; + margin-top: 24px; + list-style: none; +} + +text-expander .suggestions li, +.tribute-item { + display: flex; + align-items: center; + cursor: pointer; + gap: 6px; + font-weight: var(--font-weight-medium); +} + +text-expander .suggestions li, +.tribute-container li { + padding: 3px 6px; +} + +text-expander .suggestions li + li, +.tribute-container li + li { + border-top: 1px solid var(--color-secondary); +} + +text-expander .suggestions li:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +text-expander .suggestions li:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +text-expander .suggestions li:only-child { + border-radius: var(--border-radius); +} + +text-expander .suggestions .fullname, +.tribute-container li .fullname { + font-weight: var(--font-weight-normal); + color: var(--color-text-light-1); + overflow: hidden; + text-overflow: ellipsis; +} + +text-expander .suggestions li:hover, +text-expander .suggestions li:hover *, +text-expander .suggestions li[aria-selected="true"], +text-expander .suggestions li[aria-selected="true"] *, +.tribute-container li.highlight, +.tribute-container li.highlight * { + background: var(--color-primary); + color: var(--color-primary-contrast); +} + +text-expander .suggestions img, +.tribute-item img { + width: 21px; + height: 21px; + object-fit: contain; + aspect-ratio: 1; +} + +.tribute-container { + display: block; +} + +.tribute-container ul { + margin: 0; + padding: 0; + list-style: none; +} + +.tribute-container li.no-match { + cursor: default; +} diff --git a/web_src/css/features/tribute.css b/web_src/css/features/tribute.css deleted file mode 100644 index 99a026b9bc..0000000000 --- a/web_src/css/features/tribute.css +++ /dev/null @@ -1,32 +0,0 @@ -@import "tributejs/dist/tribute.css"; - -.tribute-container { - box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.25); - border-radius: var(--border-radius); -} - -.tribute-container ul { - margin-top: 0 !important; - background: var(--color-body) !important; -} - -.tribute-container li { - padding: 3px 0.5rem !important; -} - -.tribute-container li span.fullname { - font-weight: var(--font-weight-normal); - font-size: 0.8rem; -} - -.tribute-container li.highlight, -.tribute-container li:hover { - background: var(--color-primary) !important; - color: var(--color-primary-contrast) !important; -} - -.tribute-item { - display: flex; - align-items: center; - gap: 6px; -} diff --git a/web_src/css/index.css b/web_src/css/index.css index 84795d6d27..c20aa028e4 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -39,7 +39,7 @@ @import "./features/imagediff.css"; @import "./features/codeeditor.css"; @import "./features/projects.css"; -@import "./features/tribute.css"; +@import "./features/expander.css"; @import "./features/cropper.css"; @import "./features/console.css"; diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index 69c454d611..61b0a1f962 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -58,6 +58,11 @@ flex: 0 0 15%; min-width: 0; max-height: 100vh; + position: sticky; + top: 0; + bottom: 0; + height: 100%; + overflow-y: hidden; } .repo-view-content { diff --git a/web_src/js/features/common-button.test.ts b/web_src/js/features/common-button.test.ts new file mode 100644 index 0000000000..f41bafbc79 --- /dev/null +++ b/web_src/js/features/common-button.test.ts @@ -0,0 +1,14 @@ +import {assignElementProperty} from './common-button.ts'; + +test('assignElementProperty', () => { + const elForm = document.createElement('form'); + assignElementProperty(elForm, 'action', '/test-link'); + expect(elForm.action).contains('/test-link'); // the DOM always returns absolute URL + assignElementProperty(elForm, 'text-content', 'dummy'); + expect(elForm.textContent).toBe('dummy'); + + const elInput = document.createElement('input'); + expect(elInput.readOnly).toBe(false); + assignElementProperty(elInput, 'read-only', 'true'); + expect(elInput.readOnly).toBe(true); +}); diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index 003bfbce5d..ae399e48b3 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -1,5 +1,5 @@ import {POST} from '../modules/fetch.ts'; -import {addDelegatedEventListener, hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {camelize} from 'vue'; @@ -79,10 +79,11 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) { // if it has "toggle" class, it toggles the panel e.preventDefault(); const sel = el.getAttribute('data-panel'); - if (el.classList.contains('toggle')) { - toggleElem(sel); - } else { - showElem(sel); + const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel); + for (const elem of elems) { + if (isElemVisible(elem as HTMLElement)) { + elem.querySelector<HTMLElement>('[autofocus]')?.focus(); + } } } @@ -102,6 +103,21 @@ function onHidePanelClick(el: HTMLElement, e: MouseEvent) { throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code } +export function assignElementProperty(el: any, name: string, val: string) { + name = camelize(name); + const old = el[name]; + if (typeof old === 'boolean') { + el[name] = val === 'true'; + } else if (typeof old === 'number') { + el[name] = parseFloat(val); + } else if (typeof old === 'string') { + el[name] = val; + } else { + // in the future, we could introduce a better typing system like `data-modal-form.action:string="..."` + throw new Error(`cannot assign element property ${name} by value ${val}`); + } +} + function onShowModalClick(el: HTMLElement, e: MouseEvent) { // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute. // Each "data-modal-{target}" attribute will be filled to target element's value or text-content. @@ -109,7 +125,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) { // * Then, try to query '[name=target]' // * Then, try to query '.target' // * Then, try to query 'target' as HTML tag - // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set. + // If there is a ".{prop-name}" part like "data-modal-form.action", the "form" element's "action" property will be set, the "prop-name" will be camel-cased to "propName". e.preventDefault(); const modalSelector = el.getAttribute('data-modal'); const elModal = document.querySelector(modalSelector); @@ -122,7 +138,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) { } const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length); - const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.'); + const [attrTargetName, attrTargetProp] = attrTargetCombo.split('.'); // try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag" const attrTarget = elModal.querySelector(`#${attrTargetName}`) || elModal.querySelector(`[name=${attrTargetName}]`) || @@ -133,8 +149,8 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) { continue; } - if (attrTargetAttr) { - (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value; + if (attrTargetProp) { + assignElementProperty(attrTarget, attrTargetProp, attrib.value); } else if (attrTarget.matches('input, textarea')) { (attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox } else { diff --git a/web_src/js/features/common-issue-list.ts b/web_src/js/features/common-issue-list.ts index e207364794..037529bd10 100644 --- a/web_src/js/features/common-issue-list.ts +++ b/web_src/js/features/common-issue-list.ts @@ -1,4 +1,4 @@ -import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts'; +import {isElemVisible, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.ts'; import {GET} from '../modules/fetch.ts'; const {appSubUrl} = window.config; @@ -28,7 +28,7 @@ export function parseIssueListQuickGotoLink(repoLink: string, searchText: string } export function initCommonIssueListQuickGoto() { - const goto = document.querySelector('#issue-list-quick-goto'); + const goto = document.querySelector<HTMLElement>('#issue-list-quick-goto'); if (!goto) return; const form = goto.closest('form'); @@ -37,7 +37,7 @@ export function initCommonIssueListQuickGoto() { form.addEventListener('submit', (e) => { // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly - let doQuickGoto = !isElemHidden(goto); + let doQuickGoto = isElemVisible(goto); const submitter = submitEventSubmitter(e); if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false; if (!doQuickGoto) return; diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts index 5be234629d..2d79fe5029 100644 --- a/web_src/js/features/comp/TextExpander.ts +++ b/web_src/js/features/comp/TextExpander.ts @@ -97,6 +97,7 @@ export function initTextExpander(expander: TextExpanderElement) { li.append(img); const nameSpan = document.createElement('span'); + nameSpan.classList.add('name'); nameSpan.textContent = name; li.append(nameSpan); diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 8cd4483357..3ea5fb70c0 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -1,5 +1,5 @@ import {updateIssuesMeta} from './repo-common.ts'; -import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts'; +import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {confirmModal} from './comp/ConfirmModal.ts'; import {showErrorToast} from '../modules/toast.ts'; @@ -33,8 +33,8 @@ function initRepoIssueListCheckboxes() { toggleElem('#issue-filters', !anyChecked); toggleElem('#issue-actions', anyChecked); // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel - const panels = document.querySelectorAll('#issue-filters, #issue-actions'); - const visiblePanel = Array.from(panels).find((el) => !isElemHidden(el)); + const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions'); + const visiblePanel = Array.from(panels).find((el) => isElemVisible(el)); const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left'); toolbarLeft.prepend(issueSelectAll); }; diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index 6a3af91556..057ea9808c 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -25,10 +25,14 @@ test('createElementFromAttrs', () => { }); test('querySingleVisibleElem', () => { - let el = createElementFromHTML('<div><span>foo</span></div>'); + let el = createElementFromHTML('<div></div>'); + expect(querySingleVisibleElem(el, 'span')).toBeNull(); + el = createElementFromHTML('<div><span>foo</span></div>'); expect(querySingleVisibleElem(el, 'span').textContent).toEqual('foo'); el = createElementFromHTML('<div><span style="display: none;">foo</span><span>bar</span></div>'); expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar'); + el = createElementFromHTML('<div><span class="some-class tw-hidden">foo</span><span>bar</span></div>'); + expect(querySingleVisibleElem(el, 'span').textContent).toEqual('bar'); el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>'); expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); }); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 4386d38632..83a0d9c8df 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -9,24 +9,24 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>; type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>; export type DOMEvent<E extends Event, T extends Element = HTMLElement> = E & { target: Partial<T>; }; -function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) { +function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> { if (typeof el === 'string' || el instanceof String) { el = document.querySelectorAll(el as string); } if (el instanceof Node) { func(el, ...args); + return [el]; } else if (el.length !== undefined) { // this works for: NodeList, HTMLCollection, Array, jQuery - for (const e of (el as ArrayLikeIterable<Element>)) { - func(e, ...args); - } - } else { - throw new Error('invalid argument to be shown/hidden'); + const elems = el as ArrayLikeIterable<Element>; + for (const elem of elems) func(elem, ...args); + return elems; } + throw new Error('invalid argument to be shown/hidden'); } -export function toggleClass(el: ElementArg, className: string, force?: boolean) { - elementsCall(el, (e: Element) => { +export function toggleClass(el: ElementArg, className: string, force?: boolean): ArrayLikeIterable<Element> { + return elementsCall(el, (e: Element) => { if (force === true) { e.classList.add(className); } else if (force === false) { @@ -43,23 +43,16 @@ export function toggleClass(el: ElementArg, className: string, force?: boolean) * @param el ElementArg * @param force force=true to show or force=false to hide, undefined to toggle */ -export function toggleElem(el: ElementArg, force?: boolean) { - toggleClass(el, 'tw-hidden', force === undefined ? force : !force); -} - -export function showElem(el: ElementArg) { - toggleElem(el, true); +export function toggleElem(el: ElementArg, force?: boolean): ArrayLikeIterable<Element> { + return toggleClass(el, 'tw-hidden', force === undefined ? force : !force); } -export function hideElem(el: ElementArg) { - toggleElem(el, false); +export function showElem(el: ElementArg): ArrayLikeIterable<Element> { + return toggleElem(el, true); } -export function isElemHidden(el: ElementArg) { - const res: boolean[] = []; - elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); - if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); - return res[0]; +export function hideElem(el: ElementArg): ArrayLikeIterable<Element> { + return toggleElem(el, false); } function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> { @@ -275,14 +268,12 @@ export function initSubmitEventPolyfill() { document.body.addEventListener('focus', submitEventPolyfillListener); } -/** - * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. - * Note: This function doesn't account for all possible visibility scenarios. - */ -export function isElemVisible(element: HTMLElement): boolean { - if (!element) return false; - // checking element.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout - return Boolean((element.offsetWidth || element.offsetHeight || element.getClientRects().length) && element.style.display !== 'none'); +export function isElemVisible(el: HTMLElement): boolean { + // Check if an element is visible, equivalent to jQuery's `:visible` pseudo. + // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem" + if (!el) return false; + // checking el.style.display is not necessary for browsers, but it is required by some tests with happy-dom because happy-dom doesn't really do layout + return !el.classList.contains('tw-hidden') && Boolean((el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none'); } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this |