diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index fd6ba7ecad..1eab5efcce 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("runner token %q: %w", token, util.ErrNotExist) + return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist) } return &runnerToken, nil } @@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string return err } -// NewRunnerToken creates a new active runner token and invalidate all old tokens +// NewRunnerTokenWithValue creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { +func NewRunnerTokenWithValue(ctx context.Context, ownerID, repoID int64, token string) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - token, err := util.CryptoRandomString(40) - if err != nil { - return nil, err - } runnerToken := &ActionRunnerToken{ OwnerID: ownerID, RepoID: repoID, @@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo return err } - _, err = db.GetEngine(ctx).Insert(runnerToken) + _, err := db.GetEngine(ctx).Insert(runnerToken) return err }) } +func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { + token, err := util.CryptoRandomString(40) + if err != nil { + return nil, err + } + return NewRunnerTokenWithValue(ctx, ownerID, repoID, token) +} + // GetLatestRunnerToken returns the latest runner token func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { if ownerID != 0 && repoID != 0 { diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 5794d5109e..12f3c25676 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) } return err } diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9ffadb833d..647894bb21 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct { // CommitsByFileAndRange return the commits according revision file and the page func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - skip := (opts.Page - 1) * setting.Git.CommitsRangeSize - stdoutReader, stdoutWriter := io.Pipe() defer func() { _ = stdoutReader.Close() @@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) go func() { stderr := strings.Builder{} gitCmd := NewCommand(repo.Ctx, "rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). - AddOptionFormat("--skip=%d", skip) + AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) gitCmd.AddDynamicArguments(opts.Revision) if opts.Not != "" { diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 4c26fa2a48..e9f469accd 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -8,7 +8,11 @@ import ( "path/filepath" "testing" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_GetCommitBranches(t *testing.T) { @@ -126,3 +130,21 @@ func TestGetRefCommitID(t *testing.T) { } } } + +func TestCommitsByFileAndRange(t *testing.T) { + defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)() + + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + defer bareRepo1.Close() + + // "foo" has 3 commits in "master" branch + commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + require.NoError(t, err) + assert.Len(t, commits, 2) + + commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + require.NoError(t, err) + assert.Len(t, commits, 1) +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 14d809aedb..831b9d7bb7 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -38,63 +39,32 @@ func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, // contextKey is a value for use with context.WithValue. type contextKey struct { - name string -} - -// RepositoryContextKey is a context key. It is used with context.Value() to get the current Repository for the context -var RepositoryContextKey = &contextKey{"repository"} - -// RepositoryFromContext attempts to get the repository from the context -func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository { - value := ctx.Value(RepositoryContextKey) - if value == nil { - return nil - } - - if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil { - if gitRepo.Path == repoPath(repo) { - return gitRepo - } - } - - return nil + repoPath string } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) { - gitRepo := repositoryFromContext(ctx, repo) - if gitRepo != nil { - return gitRepo, util.NopCloser{}, nil + ds := reqctx.GetRequestDataStore(ctx) + if ds != nil { + gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo) + return gitRepo, util.NopCloser{}, err } - gitRepo, err := OpenRepository(ctx, repo) return gitRepo, gitRepo, err } -// repositoryFromContextPath attempts to get the repository from the context -func repositoryFromContextPath(ctx context.Context, path string) *git.Repository { - value := ctx.Value(RepositoryContextKey) - if value == nil { - return nil +// RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context +// The repo will be automatically closed when the request context is done +func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) { + ck := contextKey{repoPath: repoPath(repo)} + if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok { + return gitRepo, nil } - - if repo, ok := value.(*git.Repository); ok && repo != nil { - if repo.Path == path { - return repo - } + gitRepo, err := git.OpenRepository(ctx, ck.repoPath) + if err != nil { + return nil, err } - - return nil -} - -// RepositoryFromContextOrOpenPath attempts to get the repository from the context or just opens it -// Deprecated: Use RepositoryFromContextOrOpen instead -func RepositoryFromContextOrOpenPath(ctx context.Context, path string) (*git.Repository, io.Closer, error) { - gitRepo := repositoryFromContextPath(ctx, path) - if gitRepo != nil { - return gitRepo, util.NopCloser{}, nil - } - - gitRepo, err := git.OpenRepository(ctx, path) - return gitRepo, gitRepo, err + ds.AddCloser(gitRepo) + ds.SetContextValue(ck, gitRepo) + return gitRepo, nil } diff --git a/modules/gitrepo/walk_gogit.go b/modules/gitrepo/walk_gogit.go index 6370faf08e..709897ba0c 100644 --- a/modules/gitrepo/walk_gogit.go +++ b/modules/gitrepo/walk_gogit.go @@ -14,15 +14,11 @@ import ( // WalkReferences walks all the references from the repository // refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - gitRepo := repositoryFromContext(ctx, repo) - if gitRepo == nil { - var err error - gitRepo, err = OpenRepository(ctx, repo) - if err != nil { - return 0, err - } - defer gitRepo.Close() + gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return 0, err } + defer closer.Close() i := 0 iter, err := gitRepo.GoGitRepo().References() diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 358e7b06ba..aa1b7d034a 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" @@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling } } + +func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { + next := node.NextSibling + + for node != nil && node != next { + found, ref := references.FindRenderizableCommitCrossReference(node.Data) + if !found { + return + } + + reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) + link := createLink(ctx, linkHref, reftext, "commit") + + replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) + node = node.NextSibling.NextSibling + } +} diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go index e64ec76c3d..7a6f33011a 100644 --- a/modules/markup/html_issue.go +++ b/modules/markup/html_issue.go @@ -4,9 +4,9 @@ package markup import ( + "strconv" "strings" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" @@ -16,8 +16,16 @@ import ( "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" + "golang.org/x/net/html/atom" ) +type RenderIssueIconTitleOptions struct { + OwnerName string + RepoName string + LinkHref string + IssueIndex int64 +} + func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -66,6 +74,27 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { } } +func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node { + if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil { + return nil + } + issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64) + h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{ + OwnerName: ref.Owner, + RepoName: ref.Name, + LinkHref: linkHref, + IssueIndex: issueIndex, + }) + if err != nil { + log.Error("RenderRepoIssueIconTitle failed: %v", err) + return nil + } + if h == "" { + return nil + } + return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))} +} + func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.RenderOptions.Metas == nil { return @@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true" - var ( - found bool - ref *references.RenderizableReference - ) + var ref *references.RenderizableReference next := node.NextSibling - for node != nil && node != next { _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] // Repos with external issue trackers might still need to reference local PRs // We need to concern with the first one that shows up in the text, whichever it is isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric - foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) + refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly) switch ctx.RenderOptions.Metas["style"] { case "", IssueNameStyleNumeric: - found, ref = foundNumeric, refNumeric + ref = refNumeric case IssueNameStyleAlphanumeric: - found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) + ref = references.FindRenderizableReferenceAlphanumeric(node.Data) case IssueNameStyleRegexp: pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) if err != nil { return } - found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) + ref = references.FindRenderizableReferenceRegexp(node.Data, pattern) } // Repos with external issue trackers might still need to reference local PRs @@ -109,17 +134,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { if hasExtTrackFormat && !isNumericStyle && refNumeric != nil { // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // Allow a free-pass when non-numeric pattern wasn't found. - if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { - found = foundNumeric + if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start { ref = refNumeric } } - if !found { + + if ref == nil { return } var link *html.Node - reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] + refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End] if hasExtTrackFormat && !ref.IsPull { ctx.RenderOptions.Metas["index"] = ref.Issue @@ -129,18 +154,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err) } - link = createLink(ctx, res, reftext, "ref-issue ref-external-issue") + link = createLink(ctx, res, refText, "ref-issue ref-external-issue") } else { // Path determines the type of link that will be rendered. It's unknown at this point whether // the linked item is actually a PR or an issue. Luckily it's of no real consequence because // Gitea will redirect on click as appropriate. + issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) + issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - if ref.Owner == "" { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") - } else { - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp) - link = createLink(ctx, linkHref, reftext, "ref-issue") + linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp) + + // at the moment, only render the issue index in a full line (or simple line) as icon+title + // otherwise it would be too noisy for "take #1 as an example" in a sentence + if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) { + link = createIssueLinkContentWithSummary(ctx, linkHref, ref) + } + if link == nil { + link = createLink(ctx, linkHref, refText, "ref-issue") } } @@ -168,21 +198,3 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { node = node.NextSibling.NextSibling.NextSibling.NextSibling } } - -func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { - next := node.NextSibling - - for node != nil && node != next { - found, ref := references.FindRenderizableCommitCrossReference(node.Data) - if !found { - return - } - - reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp) - link := createLink(ctx, linkHref, reftext, "commit") - - replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) - node = node.NextSibling.NextSibling - } -} diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go new file mode 100644 index 0000000000..8d189fbdf6 --- /dev/null +++ b/modules/markup/html_issue_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "context" + "html/template" + "strings" + "testing" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + testModule "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRender_IssueList(t *testing.T) { + defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + markup.Init(&markup.RenderHelperFuncs{ + RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) { + return htmlutil.HTMLFormat("
issue #%d
", opts.IssueIndex), nil + }, + }) + + test := func(input, expected string) { + rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ + "user": "test-user", "repo": "test-repo", + "markupAllowShortIssuePattern": "true", + }) + out, err := markdown.RenderString(rctx, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) + } + + t.Run("NormalIssueRef", func(t *testing.T) { + test( + "#12345", + `

#12345

`, + ) + }) + + t.Run("ListIssueRef", func(t *testing.T) { + test( + "* #12345", + ``, + ) + }) + + t.Run("ListIssueRefNormal", func(t *testing.T) { + test( + "* foo #12345 bar", + ``, + ) + }) + + t.Run("ListTodoIssueRef", func(t *testing.T) { + test( + "* [ ] #12345", + ``, + ) + }) +} diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go index 82796ef274..8ff0e7d6fb 100644 --- a/modules/markup/render_helper.go +++ b/modules/markup/render_helper.go @@ -38,6 +38,7 @@ type RenderHelper interface { type RenderHelperFuncs struct { IsUsernameMentionable func(ctx context.Context, username string) bool RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) + RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error) } var DefaultRenderHelperFuncs *RenderHelperFuncs diff --git a/modules/references/references.go b/modules/references/references.go index dcb70a33d0..a5b102b7f2 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference { } // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. -func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) { +func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference { var match []int if !crossLinkOnly { match = issueNumericPattern.FindStringSubmatchIndex(content) } if match == nil { if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { - return false, nil + return nil } } r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly) if r == nil { - return false, nil + return nil } - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: r.issue, Owner: r.owner, Name: r.name, @@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe } // FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. -func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) { +func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference { match := pattern.FindStringSubmatchIndex(content) if len(match) < 4 { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[0], End: match[1]}, Action: action, @@ -390,15 +389,14 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bo } // FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. -func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { +func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference { match := issueAlphanumericPattern.FindStringSubmatchIndex(content) if match == nil { - return false, nil + return nil } action, location := findActionKeywords([]byte(content), match[2]) - - return true, &RenderizableReference{ + return &RenderizableReference{ Issue: content[match[2]:match[3]], RefLocation: &RefSpan{Start: match[2], End: match[3]}, Action: action, diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 27803083c0..1b6a968d6a 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) { } for _, fixture := range alnumFixtures { - found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) + ref := FindRenderizableReferenceAlphanumeric(fixture.input) if fixture.issue == "" { - assert.False(t, found, "Failed to parse: {%s}", fixture.input) + assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input) } else { - assert.True(t, found, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go new file mode 100644 index 0000000000..66361a4587 --- /dev/null +++ b/modules/reqctx/datastore.go @@ -0,0 +1,123 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package reqctx + +import ( + "context" + "io" + "sync" + + "code.gitea.io/gitea/modules/process" +) + +type ContextDataProvider interface { + GetData() ContextData +} + +type ContextData map[string]any + +func (ds ContextData) GetData() ContextData { + return ds +} + +func (ds ContextData) MergeFrom(other ContextData) ContextData { + for k, v := range other { + ds[k] = v + } + return ds +} + +// RequestDataStore is a short-lived context-related object that is used to store request-specific data. +type RequestDataStore interface { + GetData() ContextData + SetContextValue(k, v any) + GetContextValue(key any) any + AddCleanUp(f func()) + AddCloser(c io.Closer) +} + +type requestDataStoreKeyType struct{} + +var RequestDataStoreKey requestDataStoreKeyType + +type requestDataStore struct { + data ContextData + + mu sync.RWMutex + values map[any]any + cleanUpFuncs []func() +} + +func (r *requestDataStore) GetContextValue(key any) any { + if key == RequestDataStoreKey { + return r + } + r.mu.RLock() + defer r.mu.RUnlock() + return r.values[key] +} + +func (r *requestDataStore) SetContextValue(k, v any) { + r.mu.Lock() + r.values[k] = v + r.mu.Unlock() +} + +// GetData and the underlying ContextData are not thread-safe, callers should ensure thread-safety. +func (r *requestDataStore) GetData() ContextData { + if r.data == nil { + r.data = make(ContextData) + } + return r.data +} + +func (r *requestDataStore) AddCleanUp(f func()) { + r.mu.Lock() + r.cleanUpFuncs = append(r.cleanUpFuncs, f) + r.mu.Unlock() +} + +func (r *requestDataStore) AddCloser(c io.Closer) { + r.AddCleanUp(func() { _ = c.Close() }) +} + +func (r *requestDataStore) cleanUp() { + for _, f := range r.cleanUpFuncs { + f() + } +} + +func GetRequestDataStore(ctx context.Context) RequestDataStore { + if req, ok := ctx.Value(RequestDataStoreKey).(*requestDataStore); ok { + return req + } + return nil +} + +type requestContext struct { + context.Context + dataStore *requestDataStore +} + +func (c *requestContext) Value(key any) any { + if v := c.dataStore.GetContextValue(key); v != nil { + return v + } + return c.Context.Value(key) +} + +func NewRequestContext(parentCtx context.Context, profDesc string) (_ context.Context, finished func()) { + ctx, _, processFinished := process.GetManager().AddTypedContext(parentCtx, profDesc, process.RequestProcessType, true) + reqCtx := &requestContext{Context: ctx, dataStore: &requestDataStore{values: make(map[any]any)}} + return reqCtx, func() { + reqCtx.dataStore.cleanUp() + processFinished() + } +} + +// NewRequestContextForTest creates a new RequestContext for testing purposes +// It doesn't add the context to the process manager, nor do cleanup +func NewRequestContextForTest(parentCtx context.Context) context.Context { + return &requestContext{Context: parentCtx, dataStore: &requestDataStore{values: make(map[any]any)}} +} diff --git a/modules/svg/processor.go b/modules/svg/processor.go index 82248fb0c1..4fcb11a57d 100644 --- a/modules/svg/processor.go +++ b/modules/svg/processor.go @@ -10,7 +10,7 @@ import ( "sync" ) -type normalizeVarsStruct struct { +type globalVarsStruct struct { reXMLDoc, reComment, reAttrXMLNs, @@ -18,26 +18,23 @@ type normalizeVarsStruct struct { reAttrClassPrefix *regexp.Regexp } -var ( - normalizeVars *normalizeVarsStruct - normalizeVarsOnce sync.Once -) +var globalVars = sync.OnceValue(func() *globalVarsStruct { + return &globalVarsStruct{ + reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), + reComment: regexp.MustCompile(`(?s)`), + + reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), + reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), + reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), + } +}) // Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes // It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed. func Normalize(data []byte, size int) []byte { - normalizeVarsOnce.Do(func() { - normalizeVars = &normalizeVarsStruct{ - reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), - reComment: regexp.MustCompile(`(?s)`), - - reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`), - reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`), - reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`), - } - }) - data = normalizeVars.reXMLDoc.ReplaceAll(data, nil) - data = normalizeVars.reComment.ReplaceAll(data, nil) + vars := globalVars() + data = vars.reXMLDoc.ReplaceAll(data, nil) + data = vars.reComment.ReplaceAll(data, nil) data = bytes.TrimSpace(data) svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) @@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte { return data } normalized := bytes.Clone(svgTag) - normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil) - normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) + normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil) + normalized = vars.reAttrSize.ReplaceAll(normalized, nil) + normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) normalized = bytes.TrimSpace(normalized) normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size) if !bytes.Contains(normalized, []byte(` class="`)) { diff --git a/modules/web/handler.go b/modules/web/handler.go index 1812c664b3..9a3e4a7f17 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -4,7 +4,6 @@ package web import ( - goctx "context" "fmt" "net/http" "reflect" @@ -51,7 +50,6 @@ func (r *responseWriter) WriteHeader(statusCode int) { var ( httpReqType = reflect.TypeOf((*http.Request)(nil)) respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() - cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() ) // preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup @@ -65,11 +63,8 @@ func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { if !hasStatusProvider { panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) } - if fn.Type().NumOut() != 0 && fn.Type().NumIn() != 1 { - panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) - } - if fn.Type().NumOut() == 1 && fn.Type().Out(0) != cancelFuncType { - panic(fmt.Sprintf("handler should return a cancel function, but got %s", fn.Type())) + if fn.Type().NumOut() != 0 { + panic(fmt.Sprintf("handler should have no return value other than registered ones, but got %s", fn.Type())) } } @@ -105,16 +100,10 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect return argsIn } -func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { - if len(ret) == 1 { - if cancelFunc, ok := ret[0].Interface().(goctx.CancelFunc); ok { - return cancelFunc - } - panic(fmt.Sprintf("unsupported return type: %s", ret[0].Type())) - } else if len(ret) > 1 { +func handleResponse(fn reflect.Value, ret []reflect.Value) { + if len(ret) != 0 { panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) } - return nil } func hasResponseBeenWritten(argsIn []reflect.Value) bool { @@ -171,11 +160,8 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { routing.UpdateFuncInfo(req.Context(), funcInfo) ret := fn.Call(argsIn) - // handle the return value, and defer the cancel function if there is one - cancelFunc := handleResponse(fn, ret) - if cancelFunc != nil { - defer cancelFunc() - } + // handle the return value (no-op at the moment) + handleResponse(fn, ret) // if the response has not been written, call the next handler if next != nil && !hasResponseBeenWritten(argsIn) { diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 08d83f94be..a47da0f836 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -7,46 +7,21 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" ) -// ContextDataStore represents a data store -type ContextDataStore interface { - GetData() ContextData -} - -type ContextData map[string]any - -func (ds ContextData) GetData() ContextData { - return ds -} - -func (ds ContextData) MergeFrom(other ContextData) ContextData { - for k, v := range other { - ds[k] = v - } - return ds -} - const ContextDataKeySignedUser = "SignedUser" -type contextDataKeyType struct{} - -var contextDataKey contextDataKeyType - -func WithContextData(c context.Context) context.Context { - return context.WithValue(c, contextDataKey, make(ContextData, 10)) -} - -func GetContextData(c context.Context) ContextData { - if ds, ok := c.Value(contextDataKey).(ContextData); ok { - return ds +func GetContextData(c context.Context) reqctx.ContextData { + if rc := reqctx.GetRequestDataStore(c); rc != nil { + return rc.GetData() } return nil } -func CommonTemplateContextData() ContextData { - return ContextData{ +func CommonTemplateContextData() reqctx.ContextData { + return reqctx.ContextData{ "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, "ShowRegistrationButton": setting.Service.ShowRegistrationButton, diff --git a/modules/web/middleware/flash.go b/modules/web/middleware/flash.go index 88da2049a4..0caaa8c036 100644 --- a/modules/web/middleware/flash.go +++ b/modules/web/middleware/flash.go @@ -7,11 +7,13 @@ import ( "fmt" "html/template" "net/url" + + "code.gitea.io/gitea/modules/reqctx" ) // Flash represents a one time data transfer between two requests. type Flash struct { - DataStore ContextDataStore + DataStore reqctx.RequestDataStore url.Values ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string } diff --git a/modules/web/route.go b/modules/web/route.go index 787521dfb0..533ac3eaf1 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" @@ -29,12 +30,12 @@ func Bind[T any](_ T) http.HandlerFunc { } // SetForm set the form object -func SetForm(dataStore middleware.ContextDataStore, obj any) { +func SetForm(dataStore reqctx.ContextDataProvider, obj any) { dataStore.GetData()["__form"] = obj } // GetForm returns the validate form information -func GetForm(dataStore middleware.ContextDataStore) any { +func GetForm(dataStore reqctx.RequestDataStore) any { return dataStore.GetData()["__form"] } diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2abe3672cd..3c7e247d13 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -3742,6 +3742,7 @@ variables.creation.success=Proměnná „%s“ byla přidána. variables.update.failed=Úprava proměnné se nezdařila. variables.update.success=Proměnná byla upravena. + [projects] deleted.display_name=Odstraněný projekt type-1.display_name=Samostatný projekt diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c8dd3f71a2..78bfb26d73 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -3556,6 +3556,7 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt. variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.success=Die Variable wurde bearbeitet. + [projects] type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 193441828a..db7b3ffae3 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -3439,6 +3439,7 @@ variables.creation.success=Η μεταβλητή "%s" έχει προστεθε variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής. variables.update.success=Η μεταβλητή έχει τροποποιηθεί. + [projects] type-1.display_name=Ατομικό Έργο type-2.display_name=Έργο Αποθετηρίου diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index aa01e8699a..ea4d36fb1c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3722,6 +3722,7 @@ runners.status.active = Active runners.status.offline = Offline runners.version = Version runners.reset_registration_token = Reset registration token +runners.reset_registration_token_confirm = Would you like to invalidate the current token and generate a new one? runners.reset_registration_token_success = Runner registration token reset successfully runs.all_workflows = All Workflows diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index e95513766b..010bee186d 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -3415,6 +3415,7 @@ variables.creation.success=La variable "%s" ha sido añadida. variables.update.failed=Error al editar la variable. variables.update.success=La variable ha sido editada. + [projects] type-1.display_name=Proyecto individual type-2.display_name=Proyecto repositorio diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 640592f2bf..b92361db34 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -2529,6 +2529,7 @@ runs.commit=کامیت + [projects] [git.filemode] diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 375c7b11bf..b4f3869db4 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1707,6 +1707,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index d522abbd11..0898d0fd29 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1945,6 +1945,8 @@ pulls.delete.title=Supprimer cette demande d'ajout ? pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.recently_pushed_new_branches=Vous avez soumis sur la branche %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s +pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements pulls.upstream_diverging_merge=Synchroniser la bifurcation @@ -3770,6 +3772,7 @@ variables.creation.success=La variable « %s » a été ajoutée. variables.update.failed=Impossible d’éditer la variable. variables.update.success=La variable a bien été modifiée. + [projects] deleted.display_name=Projet supprimé type-1.display_name=Projet personnel diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 6d989d3347..20672569ac 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -3770,6 +3770,7 @@ variables.creation.success=Tá an athróg "%s" curtha leis. variables.update.failed=Theip ar athróg a chur in eagar. variables.update.success=Tá an t-athróg curtha in eagar. + [projects] deleted.display_name=Tionscadal scriosta type-1.display_name=Tionscadal Aonair diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 88ccc9fac2..8e11f07d33 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1615,6 +1615,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 00a406aae4..7c8271b98b 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -1444,6 +1444,7 @@ variables.creation.success=Variabel "%s" telah ditambahkan. variables.update.failed=Gagal mengedit variabel. variables.update.success=Variabel telah diedit. + [projects] type-1.display_name=Proyek Individu type-2.display_name=Proyek Repositori diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 0564d49b1c..d3824f83a2 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -1342,6 +1342,7 @@ runs.commit=Framlag + [projects] [git.filemode] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 567e6acdce..26b4f9e27b 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -2807,6 +2807,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index bbd48989f7..da07d77825 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -3762,6 +3762,7 @@ variables.creation.success=変数 "%s" を追加しました。 variables.update.failed=変数を更新できませんでした。 variables.update.success=変数を更新しました。 + [projects] deleted.display_name=削除されたプロジェクト type-1.display_name=個人プロジェクト diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 48220d5c99..361ab23e4c 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1563,6 +1563,7 @@ runs.commit=커밋 + [projects] [git.filemode] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index fd412b95b4..27a061d722 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -3443,6 +3443,7 @@ variables.creation.success=Mainīgais "%s" tika pievienots. variables.update.failed=Neizdevās labot mainīgo. variables.update.success=Mainīgais tika labots. + [projects] type-1.display_name=Individuālais projekts type-2.display_name=Repozitorija projekts diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a4da8177bc..5c07b521e9 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -2537,6 +2537,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 22f701219d..13c05eebe0 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2430,6 +2430,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8fb869898b..c053b33210 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -3353,6 +3353,7 @@ runs.empty_commit_message=(mensagem de commit vazia) need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork. + [projects] type-1.display_name=Projeto individual type-2.display_name=Projeto do repositório diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 4db7b5a2a2..13e7318c6f 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -3770,6 +3770,9 @@ variables.creation.success=A variável "%s" foi adicionada. variables.update.failed=Falha ao editar a variável. variables.update.success=A variável foi editada. +logs.always_auto_scroll=Rolar registos de forma automática e permanente +logs.always_expand_running=Expandir sempre os registos que vão rolando + [projects] deleted.display_name=Planeamento eliminado type-1.display_name=Planeamento individual diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 735077bd41..0662c1f5f9 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -3373,6 +3373,7 @@ variables.creation.success=Переменная «%s» добавлена. variables.update.failed=Не удалось изменить переменную. variables.update.success=Переменная изменена. + [projects] type-1.display_name=Индивидуальный проект type-2.display_name=Проект репозитория diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 506fa5b492..debcf8e3e9 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -2470,6 +2470,7 @@ runs.commit=කැප + [projects] [git.filemode] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index b4bf6fb552..39c13c358e 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1328,6 +1328,7 @@ runners.labels=Štítky + [projects] [git.filemode] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index fc138381db..2fd8277b76 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -2005,6 +2005,7 @@ runs.commit=Commit + [projects] [git.filemode] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9b7f2cb5c6..d17985dba5 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -3633,6 +3633,7 @@ variables.creation.success=`"%s" değişkeni eklendi.` variables.update.failed=Değişken düzenlenemedi. variables.update.success=Değişken düzenlendi. + [projects] deleted.display_name=Silinmiş Proje type-1.display_name=Kişisel Proje diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index efefeeb436..541a5bd0b5 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2538,6 +2538,7 @@ runs.commit=Коміт + [projects] [git.filemode] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 60fb75df4f..a1fc26528e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1945,6 +1945,8 @@ pulls.delete.title=删除此合并请求? pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s +pulls.upstream_diverging_prompt_behind_1=该分支落后于 %[2]s %[1]d 个提交 +pulls.upstream_diverging_prompt_behind_n=该分支落后于 %[2]s %[1]d 个提交 pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改 pulls.upstream_diverging_merge=同步派生 @@ -3770,6 +3772,7 @@ variables.creation.success=变量 “%s” 添加成功。 variables.update.failed=编辑变量失败。 variables.update.success=该变量已被编辑。 + [projects] deleted.display_name=已删除项目 type-1.display_name=个人项目 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 6f37d30efc..e1ca6ebfc8 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -975,6 +975,7 @@ runners.task_list.repository=儲存庫 + [projects] [git.filemode] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 0bfc6d6365..267f8b4947 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -3763,6 +3763,7 @@ variables.creation.success=已新增變數「%s」。 variables.update.failed=編輯變數失敗。 variables.update.success=已編輯變數。 + [projects] deleted.display_name=已刪除的專案 type-1.display_name=個人專案 diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 0a7f92ac40..910edd6d58 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -126,11 +126,10 @@ func ArtifactsRoutes(prefix string) *web.Router { func ArtifactContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) + ctx.SetContextValue(artifactContextKey, ctx) // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN // we should verify the ACTIONS_RUNTIME_TOKEN diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 6dd36888d2..8917a7a8a2 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -126,12 +126,9 @@ type artifactV4Routes struct { func ArtifactV4Contexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() - + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) - + ctx.SetContextValue(artifactContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 9a31aec314..f6df866efc 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -729,15 +729,11 @@ func CreateBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() } // FIXME: since we only need to recheck files protected rules, we could improve this matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) @@ -1061,15 +1057,11 @@ func EditBranchProtection(ctx *context.APIContext) { } else { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() } // FIXME: since we only need to recheck files protected rules, we could improve this diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 1678bc033c..87b890cb62 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -44,13 +44,12 @@ func CompareDiff(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } infoPath := ctx.PathParam("*") diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index 3620c1465f..eb967772ed 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -28,13 +28,12 @@ func DownloadArchive(ctx *context.APIContext) { } if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 83848b7add..6591b9a752 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -287,13 +287,12 @@ func GetArchive(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) return } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } archiveDownload(ctx) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 40990a28cb..f0f5db0536 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -726,12 +726,11 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) if err != nil { ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) return err } - defer ctx.Repo.GitRepo.Close() } // Default branch only updated if changed and exist or the repository is empty diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index 787ec34404..b2090cac41 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -100,7 +100,7 @@ func Transfer(ctx *context.APIContext) { } if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() + _ = ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo = nil } diff --git a/routers/common/errpage_test.go b/routers/common/errpage_test.go index 4fd63ba49e..dfea55f510 100644 --- a/routers/common/errpage_test.go +++ b/routers/common/errpage_test.go @@ -12,8 +12,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/web/middleware" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ import ( func TestRenderPanicErrorPage(t *testing.T) { w := httptest.NewRecorder() req := &http.Request{URL: &url.URL{}} - req = req.WithContext(middleware.WithContextData(context.Background())) + req = req.WithContext(reqctx.NewRequestContextForTest(context.Background())) RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)")) respContent := w.Body.String() assert.Contains(t, respContent, `class="page-content status-page-500"`) diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 51e42d87a0..047d327ce8 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -4,16 +4,14 @@ package common import ( - go_context "context" "fmt" "net/http" "strings" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/services/context" @@ -24,54 +22,12 @@ import ( // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery func ProtocolMiddlewares() (handlers []any) { - // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly - handlers = append(handlers, func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) - if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() - } else { - ctx.RoutePath = req.URL.RawPath - } - next.ServeHTTP(resp, req) - }) - }) + // the order is important + handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths + handlers = append(handlers, RequestContextHandler()) // // prepare the context and panic recovery - // prepare the ContextData and panic recovery - handlers = append(handlers, func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - defer func() { - if err := recover(); err != nil { - RenderPanicErrorPage(resp, req, err) // it should never panic - } - }() - req = req.WithContext(middleware.WithContextData(req.Context())) - req = req.WithContext(go_context.WithValue(req.Context(), httplib.RequestContextKey, req)) - next.ServeHTTP(resp, req) - }) - }) - - // wrap the request and response, use the process context and add it to the process manager - handlers = append(handlers, func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx, _, finished := process.GetManager().AddTypedContext(req.Context(), fmt.Sprintf("%s: %s", req.Method, req.RequestURI), process.RequestProcessType, true) - defer finished() - next.ServeHTTP(context.WrapResponseWriter(resp), req.WithContext(cache.WithCacheContext(ctx))) - }) - }) - - if setting.ReverseProxyLimit > 0 { - opt := proxy.NewForwardedHeadersOptions(). - WithForwardLimit(setting.ReverseProxyLimit). - ClearTrustedProxies() - for _, n := range setting.ReverseProxyTrustedProxies { - if !strings.Contains(n, "/") { - opt.AddTrustedProxy(n) - } else { - opt.AddTrustedNetwork(n) - } - } - handlers = append(handlers, proxy.ForwardedHeaders(opt)) + if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 { + handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies)) } if setting.IsRouteLogEnabled() { @@ -85,6 +41,59 @@ func ProtocolMiddlewares() (handlers []any) { return handlers } +func RequestContextHandler() func(h http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) + ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) + defer finished() + + defer func() { + if err := recover(); err != nil { + RenderPanicErrorPage(resp, req, err) // it should never panic + } + }() + + ds := reqctx.GetRequestDataStore(ctx) + req = req.WithContext(cache.WithCacheContext(ctx)) + ds.SetContextValue(httplib.RequestContextKey, req) + ds.AddCleanUp(func() { + if req.MultipartForm != nil { + _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory + } + }) + next.ServeHTTP(context.WrapResponseWriter(resp), req) + }) + } +} + +func ChiRoutePathHandler() func(h http.Handler) http.Handler { + // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + ctx := chi.RouteContext(req.Context()) + if req.URL.RawPath == "" { + ctx.RoutePath = req.URL.EscapedPath() + } else { + ctx.RoutePath = req.URL.RawPath + } + next.ServeHTTP(resp, req) + }) + } +} + +func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Handler) http.Handler { + opt := proxy.NewForwardedHeadersOptions().WithForwardLimit(limit).ClearTrustedProxies() + for _, n := range trustedProxies { + if !strings.Contains(n, "/") { + opt.AddTrustedProxy(n) + } else { + opt.AddTrustedNetwork(n) + } + } + return proxy.ForwardedHeaders(opt) +} + func Sessioner() func(next http.Handler) http.Handler { return session.Sessioner(session.Options{ Provider: setting.SessionConfig.Provider, diff --git a/routers/init.go b/routers/init.go index 2091f5967a..e7aa765bf0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) { highlight.NewContext() external.RegisterRenderers() - markup.Init(markup_service.ProcessorHelper()) + markup.Init(markup_service.FormalRenderHelperFuncs()) if setting.EnableSQLite3 { log.Info("SQLite3 support is enabled") @@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) { auth.Init() mustInit(svg.Init) - actions_service.Init() + mustInitCtx(ctx, actions_service.Init) mustInit(repo_service.InitLicenseClassifier) diff --git a/routers/install/install.go b/routers/install/install.go index 1819bafc62..8a1d57aa0b 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" @@ -61,15 +62,11 @@ func Contexter() func(next http.Handler) http.Handler { envConfigKeys := setting.CollectEnvConfigKeys() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() - + base := context.NewBaseContext(resp, req) ctx := context.NewWebContext(base, rnd, session.GetSession(req)) - ctx.AppendContextValue(context.WebContextKey, ctx) + ctx.SetContextValue(context.WebContextKey, ctx) ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - ctx.Data.MergeFrom(middleware.ContextData{ - "Context": ctx, // TODO: use "ctx" in template and remove this - "locale": ctx.Locale, + ctx.Data.MergeFrom(reqctx.ContextData{ "Title": ctx.Locale.Tr("install.install"), "PageIsInstall": true, "DbTypeNames": dbTypeNames, diff --git a/routers/private/internal.go b/routers/private/internal.go index 1fb72f13d9..a78c76f897 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -63,8 +63,8 @@ func Routes() *web.Router { r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) - r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext, bind(private.HookOptions{}), HookPostReceive) - r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext, RepoAssignment, bind(private.HookOptions{}), HookProcReceive) + r.Post("/hook/post-receive/{owner}/{repo}", context.OverrideContext(), bind(private.HookOptions{}), HookPostReceive) + r.Post("/hook/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) @@ -88,7 +88,7 @@ func Routes() *web.Router { // Fortunately, the LFS handlers are able to handle requests without a complete web context common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) { webContext := &context.Context{Base: ctx.Base} - ctx.AppendContextValue(context.WebContextKey, webContext) + ctx.SetContextValue(context.WebContextKey, webContext) }) }) diff --git a/routers/private/internal_repo.go b/routers/private/internal_repo.go index aad0a3fb1a..4255be7e52 100644 --- a/routers/private/internal_repo.go +++ b/routers/private/internal_repo.go @@ -4,7 +4,6 @@ package private import ( - "context" "fmt" "net/http" @@ -17,40 +16,29 @@ import ( // This file contains common functions relating to setting the Repository for the internal routes -// RepoAssignment assigns the repository and gitrepository to the private context -func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { +// RepoAssignment assigns the repository and git repository to the private context +func RepoAssignment(ctx *gitea_context.PrivateContext) { ownerName := ctx.PathParam(":owner") repoName := ctx.PathParam(":repo") repo := loadRepository(ctx, ownerName, repoName) if ctx.Written() { // Error handled in loadRepository - return nil + return } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) if err != nil { log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) ctx.JSON(http.StatusInternalServerError, private.Response{ Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), }) - return nil + return } - ctx.Repo = &gitea_context.Repository{ Repository: repo, GitRepo: gitRepo, } - - // We opened it, we should close it - cancel := func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } - } - - return cancel } func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository { diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index f38933226b..6d77bdd2fa 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r ctx.ServerError("ResetRunnerRegistrationToken", err) return } - ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) - ctx.Redirect(redirectTo) + ctx.JSONRedirect(redirectTo) } // RunnerDeletePost response for deleting a runner diff --git a/routers/web/web.go b/routers/web/web.go index ab29a45452..5e5ed25c6c 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -4,7 +4,6 @@ package web import ( - gocontext "context" "net/http" "strings" @@ -463,7 +462,7 @@ func registerRoutes(m *web.Router) { m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) - m.Get("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) + m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken) }) } @@ -1526,24 +1525,23 @@ func registerRoutes(m *web.Router) { m.Group("/blob_excerpt", func() { m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) - }, func(ctx *context.Context) gocontext.CancelFunc { + }, func(ctx *context.Context) { // FIXME: refactor this function, use separate routes for wiki/code if ctx.FormBool("wiki") { ctx.Data["PageIsWiki"] = true repo.MustEnableWiki(ctx) - return nil + return } if ctx.Written() { - return nil + return } - cancel := context.RepoRef()(ctx) + context.RepoRef()(ctx) if ctx.Written() { - return cancel + return } repo.MustBeNotEmpty(ctx) - return cancel }) m.Group("/media", func() { diff --git a/services/actions/init.go b/services/actions/init.go index 0f49cb6297..7136da05ed 100644 --- a/services/actions/init.go +++ b/services/actions/init.go @@ -4,23 +4,68 @@ package actions import ( + "context" + "errors" + "fmt" + "os" + "strings" + + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) -func Init() { +func initGlobalRunnerToken(ctx context.Context) error { + // use the same env name as the runner, for consistency + token := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN") + tokenFile := os.Getenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE") + if token != "" && tokenFile != "" { + return errors.New("both GITEA_RUNNER_REGISTRATION_TOKEN and GITEA_RUNNER_REGISTRATION_TOKEN_FILE are set, only one can be used") + } + if tokenFile != "" { + file, err := os.ReadFile(tokenFile) + if err != nil { + return fmt.Errorf("unable to read GITEA_RUNNER_REGISTRATION_TOKEN_FILE: %w", err) + } + token = strings.TrimSpace(string(file)) + } + if token == "" { + return nil + } + + if len(token) < 32 { + return errors.New("GITEA_RUNNER_REGISTRATION_TOKEN must be at least 32 random characters") + } + + existing, err := actions_model.GetRunnerToken(ctx, token) + if err != nil && !errors.Is(err, util.ErrNotExist) { + return fmt.Errorf("unable to check existing token: %w", err) + } + if existing != nil { + if !existing.IsActive { + log.Warn("The token defined by GITEA_RUNNER_REGISTRATION_TOKEN is already invalidated, please use the latest one from web UI") + } + return nil + } + _, err = actions_model.NewRunnerTokenWithValue(ctx, 0, 0, token) + return err +} + +func Init(ctx context.Context) error { if !setting.Actions.Enabled { - return + return nil } jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) if jobEmitterQueue == nil { - log.Fatal("Unable to create actions_ready_job queue") + return errors.New("unable to create actions_ready_job queue") } go graceful.GetManager().RunWithCancel(jobEmitterQueue) notify_service.RegisterNotifier(NewNotifier()) + return initGlobalRunnerToken(ctx) } diff --git a/services/actions/init_test.go b/services/actions/init_test.go new file mode 100644 index 0000000000..59c321ccd7 --- /dev/null +++ b/services/actions/init_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "os" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + FixtureFiles: []string{"action_runner_token.yml"}, + }) + os.Exit(m.Run()) +} + +func TestInitToken(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("NoToken", func(t *testing.T) { + _, _ = db.Exec(db.DefaultContext, "DELETE FROM action_runner_token") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + notEmpty, err := db.IsTableNotEmpty(&actions_model.ActionRunnerToken{}) + require.NoError(t, err) + assert.False(t, notEmpty) + }) + + t.Run("EnvToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", tokenValue) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", "") + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // init with the same token again, should not create a new token + err = initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.Equal(t, token.ID, token2.ID) + assert.True(t, token.IsActive) + }) + + t.Run("EnvFileToken", func(t *testing.T) { + tokenValue, _ := util.CryptoRandomString(32) + f := t.TempDir() + "/token" + _ = os.WriteFile(f, []byte(tokenValue), 0o644) + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "") + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN_FILE", f) + err := initGlobalRunnerToken(db.DefaultContext) + require.NoError(t, err) + token := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.True(t, token.IsActive) + + // if the env token is invalidated by another new token, then it shouldn't be active anymore + _, err = actions_model.NewRunnerToken(db.DefaultContext, 0, 0) + require.NoError(t, err) + token = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunnerToken{Token: tokenValue}) + assert.False(t, token.IsActive) + }) + + t.Run("InvalidToken", func(t *testing.T) { + t.Setenv("GITEA_RUNNER_REGISTRATION_TOKEN", "abc") + err := initGlobalRunnerToken(db.DefaultContext) + assert.ErrorContains(t, err, "must be at least") + }) +} diff --git a/services/auth/interface.go b/services/auth/interface.go index ece28af12d..275b4dd56c 100644 --- a/services/auth/interface.go +++ b/services/auth/interface.go @@ -8,12 +8,11 @@ import ( "net/http" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" - "code.gitea.io/gitea/modules/web/middleware" ) -// DataStore represents a data store -type DataStore middleware.ContextDataStore +type DataStore = reqctx.ContextDataProvider // SessionStore represents a session store type SessionStore session.Store diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index b706847e8e..0d9e793cf3 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/services/actions" "github.com/stretchr/testify/assert" @@ -23,7 +23,7 @@ func TestUserIDFromToken(t *testing.T) { token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) assert.NoError(t, err) - ds := make(middleware.ContextData) + ds := make(reqctx.ContextData) o := OAuth2{} uid := o.userIDFromToken(context.Background(), token, ds) diff --git a/services/context/api.go b/services/context/api.go index b45e80a329..7b604c5ea1 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -5,7 +5,6 @@ package context import ( - "context" "fmt" "net/http" "net/url" @@ -212,17 +211,15 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { func APIContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(w, req) + base := NewBaseContext(w, req) ctx := &APIContext{ Base: base, Cache: cache.GetCache(), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &APIOrganization{}, } - defer baseCleanUp() - ctx.Base.AppendContextValue(apiContextKey, ctx) - ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) + ctx.SetContextValue(apiContextKey, ctx) // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") { @@ -267,31 +264,22 @@ func (ctx *APIContext) NotFound(objs ...any) { // ReferencesGitRepo injects the GitRepo into the Context // you can optional skip the IsEmpty check -func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) { - return func(ctx *APIContext) (cancel context.CancelFunc) { +func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { + return func(ctx *APIContext) { // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) { - return nil + return } // For API calls. if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + var err error + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) - return cancel - } - ctx.Repo.GitRepo = gitRepo - // We opened it, we should close it - return func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - _ = ctx.Repo.GitRepo.Close() - } + return } } - - return cancel } } diff --git a/services/context/base.go b/services/context/base.go index d627095584..cb89cdea02 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -12,12 +12,12 @@ import ( "net/url" "strconv" "strings" - "time" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/web/middleware" @@ -25,65 +25,25 @@ import ( "github.com/go-chi/chi/v5" ) -type contextValuePair struct { - key any - valueFn func() any -} - type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType type Base struct { - originCtx context.Context - contextValues []contextValuePair + context.Context + reqctx.RequestDataStore Resp ResponseWriter Req *http.Request // Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. // Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler - Data middleware.ContextData + Data reqctx.ContextData // Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation Locale translation.Locale } -func (b *Base) Deadline() (deadline time.Time, ok bool) { - return b.originCtx.Deadline() -} - -func (b *Base) Done() <-chan struct{} { - return b.originCtx.Done() -} - -func (b *Base) Err() error { - return b.originCtx.Err() -} - -func (b *Base) Value(key any) any { - for _, pair := range b.contextValues { - if pair.key == key { - return pair.valueFn() - } - } - return b.originCtx.Value(key) -} - -func (b *Base) AppendContextValueFunc(key any, valueFn func() any) any { - b.contextValues = append(b.contextValues, contextValuePair{key, valueFn}) - return b -} - -func (b *Base) AppendContextValue(key, value any) any { - b.contextValues = append(b.contextValues, contextValuePair{key, func() any { return value }}) - return b -} - -func (b *Base) GetData() middleware.ContextData { - return b.Data -} - // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header func (b *Base) AppendAccessControlExposeHeaders(names ...string) { val := b.RespHeader().Get("Access-Control-Expose-Headers") @@ -295,13 +255,6 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r) } -// Close frees all resources hold by Context -func (b *Base) cleanUp() { - if b.Req != nil && b.Req.MultipartForm != nil { - _ = b.Req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory - } -} - func (b *Base) Tr(msg string, args ...any) template.HTML { return b.Locale.Tr(msg, args...) } @@ -310,17 +263,28 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return b.Locale.TrN(cnt, key1, keyN, args...) } -func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { - b = &Base{ - originCtx: req.Context(), - Req: req, - Resp: WrapResponseWriter(resp), - Locale: middleware.Locale(resp, req), - Data: middleware.GetContextData(req.Context()), +func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base { + ds := reqctx.GetRequestDataStore(req.Context()) + b := &Base{ + Context: req.Context(), + RequestDataStore: ds, + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: ds.GetData(), } b.Req = b.Req.WithContext(b) - b.AppendContextValue(BaseContextKey, b) - b.AppendContextValue(translation.ContextKey, b.Locale) - b.AppendContextValue(httplib.RequestContextKey, b.Req) - return b, b.cleanUp + ds.SetContextValue(BaseContextKey, b) + ds.SetContextValue(translation.ContextKey, b.Locale) + ds.SetContextValue(httplib.RequestContextKey, b.Req) + return b +} + +func NewBaseContextForTest(resp http.ResponseWriter, req *http.Request) *Base { + if !setting.IsInTesting { + panic("This function is only for testing") + } + ctx := reqctx.NewRequestContextForTest(req.Context()) + *req = *req.WithContext(ctx) + return NewBaseContext(resp, req) } diff --git a/services/context/base_test.go b/services/context/base_test.go index 823f20e00b..b936b76f58 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -14,6 +14,7 @@ import ( ) func TestRedirect(t *testing.T) { + setting.IsInTesting = true req, _ := http.NewRequest("GET", "/", nil) cases := []struct { @@ -28,10 +29,9 @@ func TestRedirect(t *testing.T) { } for _, c := range cases { resp := httptest.NewRecorder() - b, cleanup := NewBaseContext(resp, req) + b := NewBaseContextForTest(resp, req) resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String()) b.Redirect(c.url) - cleanup() has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" assert.Equal(t, c.keep, has, "url = %q", c.url) } @@ -39,9 +39,8 @@ func TestRedirect(t *testing.T) { req, _ = http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() req.Header.Add("HX-Request", "true") - b, cleanup := NewBaseContext(resp, req) + b := NewBaseContextForTest(resp, req) b.Redirect("/other") - cleanup() assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) assert.Equal(t, http.StatusNoContent, resp.Code) } diff --git a/services/context/context.go b/services/context/context.go index 0d5429e366..6715c5663d 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -18,7 +18,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/setting" @@ -153,14 +152,9 @@ func Contexter() func(next http.Handler) http.Handler { } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() + base := NewBaseContext(resp, req) ctx := NewWebContext(base, rnd, session.GetContextSession(req)) - ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) - if setting.IsProd && !setting.IsInTesting { - ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this - } ctx.Data["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI() ctx.Data["Link"] = ctx.Link @@ -168,9 +162,7 @@ func Contexter() func(next http.Handler) http.Handler { ctx.PageData = map[string]any{} ctx.Data["PageData"] = ctx.PageData - ctx.Base.AppendContextValue(WebContextKey, ctx) - ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo }) - + ctx.Base.SetContextValue(WebContextKey, ctx) ctx.Csrf = NewCSRFProtector(csrfOpts) // Get the last flash message from cookie diff --git a/services/context/context_response.go b/services/context/context_response.go index 4c086ea9f5..c7044791eb 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) { } // RenderToHTML renders the template content to a HTML string -func (ctx *Context) RenderToHTML(name templates.TplName, data map[string]any) (template.HTML, error) { +func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) { var buf strings.Builder err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext) return template.HTML(buf.String()), err diff --git a/services/context/context_test.go b/services/context/context_test.go index 984593398d..54044644f0 100644 --- a/services/context/context_test.go +++ b/services/context/context_test.go @@ -26,6 +26,7 @@ func TestRemoveSessionCookieHeader(t *testing.T) { } func TestRedirectToCurrentSite(t *testing.T) { + setting.IsInTesting = true defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() cases := []struct { @@ -40,8 +41,7 @@ func TestRedirectToCurrentSite(t *testing.T) { t.Run(c.location, func(t *testing.T) { req := &http.Request{URL: &url.URL{Path: "/"}} resp := httptest.NewRecorder() - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() + base := NewBaseContextForTest(resp, req) ctx := NewWebContext(base, nil, nil) ctx.RedirectToCurrentSite(c.location) redirect := test.RedirectURL(resp) diff --git a/services/context/package.go b/services/context/package.go index 271b61e99c..e98e01acbb 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -153,12 +153,10 @@ func PackageContexter() func(next http.Handler) http.Handler { renderer := templates.HTMLRenderer() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(resp, req) - defer baseCleanUp() - + base := NewBaseContext(resp, req) // it is still needed when rendering 500 page in a package handler ctx := NewWebContext(base, renderer, nil) - ctx.Base.AppendContextValue(WebContextKey, ctx) + ctx.SetContextValue(WebContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/services/context/private.go b/services/context/private.go index 8b41949f60..51857da8fe 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -64,11 +64,9 @@ func GetPrivateContext(req *http.Request) *PrivateContext { func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - base, baseCleanUp := NewBaseContext(w, req) + base := NewBaseContext(w, req) ctx := &PrivateContext{Base: base} - defer baseCleanUp() - ctx.Base.AppendContextValue(privateContextKey, ctx) - + ctx.SetContextValue(privateContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } @@ -78,8 +76,15 @@ func PrivateContexter() func(http.Handler) http.Handler { // This function should be used when there is a need for work to continue even if the request has been cancelled. // Primarily this affects hook/post-receive and hook/proc-receive both of which need to continue working even if // the underlying request has timed out from the ssh/http push -func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) { - // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work - ctx.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) - return cancel +func OverrideContext() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // We now need to override the request context as the base for our work because even if the request is cancelled we have to continue this work + ctx := GetPrivateContext(req) + var finished func() + ctx.Override, _, finished = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) + defer finished() + next.ServeHTTP(ctx.Resp, ctx.Req) + }) + } } diff --git a/services/context/repo.go b/services/context/repo.go index e96916ca42..db5308415e 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -397,11 +397,13 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { } // RepoAssignment returns a middleware to handle repository assignment -func RepoAssignment(ctx *Context) context.CancelFunc { +func RepoAssignment(ctx *Context) { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { // FIXME: it should panic in dev/test modes to have a clear behavior - log.Trace("RepoAssignment was exec already, skipping second call ...") - return nil + if !setting.IsProd || setting.IsInTesting { + panic("RepoAssignment should not be executed twice") + } + return } ctx.Data["repoAssignmentExecuted"] = true @@ -429,7 +431,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { // https://github.com/golang/go/issues/19760 if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return nil + return } if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { @@ -442,7 +444,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else { ctx.ServerError("GetUserByName", err) } - return nil + return } } ctx.Repo.Owner = owner @@ -467,7 +469,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { redirectPath += "?" + ctx.Req.URL.RawQuery } ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) - return nil + return } // Get repository. @@ -480,7 +482,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else if repo_model.IsErrRedirectNotExist(err) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) - return nil + return } ctx.NotFound("GetRepositoryByName", nil) } else { @@ -489,13 +491,13 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } else { ctx.ServerError("GetRepositoryByName", err) } - return nil + return } repo.Owner = owner repoAssignment(ctx, repo) if ctx.Written() { - return nil + return } ctx.Repo.RepoLink = repo.Link() @@ -520,7 +522,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return nil + return } ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ // only show draft releases for users who can write, read-only users shouldn't see draft releases. @@ -529,7 +531,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { }) if err != nil { ctx.ServerError("GetReleaseCountByRepoID", err) - return nil + return } ctx.Data["Title"] = owner.Name + "/" + repo.Name @@ -546,14 +548,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc { canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("CanUserForkRepo", err) - return nil + return } ctx.Data["CanSignedUserFork"] = canSignedUserFork userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetForksByUserAndOrgs", err) - return nil + return } ctx.Data["UserAndOrgForks"] = userAndOrgForks @@ -587,14 +589,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if repo.IsFork { RetrieveBaseRepo(ctx, repo) if ctx.Written() { - return nil + return } } if repo.IsGenerated() { RetrieveTemplateRepo(ctx, repo) if ctx.Written() { - return nil + return } } @@ -609,10 +611,18 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return nil + return } - gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if ctx.Repo.GitRepo != nil { + if !setting.IsProd || setting.IsInTesting { + panic("RepoAssignment: GitRepo should be nil") + } + _ = ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } + + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo) if err != nil { if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err) @@ -622,28 +632,16 @@ func RepoAssignment(ctx *Context) context.CancelFunc { if !isHomeOrSettings { ctx.Redirect(ctx.Repo.RepoLink) } - return nil + return } ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err) - return nil - } - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } - ctx.Repo.GitRepo = gitRepo - - // We opened it, we should close it - cancel := func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } + return } // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch - return cancel + return } branchOpts := git_model.FindBranchOptions{ @@ -654,7 +652,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts) if err != nil { ctx.ServerError("CountBranches", err) - return cancel + return } // non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet @@ -662,7 +660,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) if err != nil { ctx.ServerError("SyncRepoBranches", err) - return cancel + return } } @@ -670,7 +668,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { // If no branch is set in the request URL, try to guess a default one. if len(ctx.Repo.BranchName) == 0 { - if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { + if len(ctx.Repo.Repository.DefaultBranch) > 0 && ctx.Repo.GitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) { ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch } else { ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) @@ -711,12 +709,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc { repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError("GetPendingRepositoryTransfer", err) - return cancel + return } if err := repoTransfer.LoadAttributes(ctx); err != nil { ctx.ServerError("LoadRecipient", err) - return cancel + return } ctx.Data["RepoTransfer"] = repoTransfer @@ -731,7 +729,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" } - return cancel } // RepoRefType type of repo reference @@ -750,7 +747,7 @@ const headRefName = "HEAD" // RepoRef handles repository reference names when the ref name is not // explicitly given -func RepoRef() func(*Context) context.CancelFunc { +func RepoRef() func(*Context) { // since no ref name is explicitly specified, ok to just use branch return RepoRefByType(RepoRefBranch) } @@ -865,9 +862,9 @@ type RepoRefByTypeOptions struct { // RepoRefByType handles repository reference name for a specific type // of repository reference -func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) context.CancelFunc { +func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func(*Context) { opt := util.OptionalArg(opts) - return func(ctx *Context) (cancel context.CancelFunc) { + return func(ctx *Context) { refType := detectRefType // Empty repository does not have reference information. if ctx.Repo.Repository.IsEmpty { @@ -875,7 +872,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.IsViewBranch = true ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch ctx.Data["TreePath"] = "" - return nil + return } var ( @@ -884,17 +881,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ) if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, ctx.Repo.Repository) if err != nil { ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) - return nil - } - // We opened it, we should close it - cancel = func() { - // If it's been set to nil then assume someone else has closed it. - if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() - } + return } } @@ -924,7 +914,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Repository.MarkAsBrokenEmpty() } else { ctx.ServerError("GetBranchCommit", err) - return cancel + return } ctx.Repo.IsViewBranch = true } else { @@ -941,7 +931,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1) ctx.Redirect(link) - return cancel + return } if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { @@ -951,7 +941,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) if err != nil { ctx.ServerError("GetBranchCommit", err) - return cancel + return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { @@ -962,10 +952,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func if err != nil { if git.IsErrNotExist(err) { ctx.NotFound("GetTagCommit", err) - return cancel + return } ctx.ServerError("GetTagCommit", err) - return cancel + return } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { @@ -975,7 +965,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName) if err != nil { ctx.NotFound("GetCommit", err) - return cancel + return } // If short commit ID add canonical link header if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { @@ -984,10 +974,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func } } else { if opt.IgnoreNotExistErr { - return cancel + return } ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) - return cancel + return } if guessLegacyPath { @@ -999,7 +989,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.BranchNameSubURL(), util.PathEscapeSegments(ctx.Repo.TreePath)) ctx.Redirect(redirect) - return cancel + return } } @@ -1017,12 +1007,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() if err != nil { ctx.ServerError("GetCommitsCount", err) - return cancel + return } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) - - return cancel } } diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go index 39ad5a362f..b0f71cad20 100644 --- a/services/contexttest/context_tests.go +++ b/services/contexttest/context_tests.go @@ -21,6 +21,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" @@ -40,7 +41,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request { requestURL, err := url.Parse(path) assert.NoError(t, err) req := &http.Request{Method: method, Host: requestURL.Host, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} - req = req.WithContext(middleware.WithContextData(req.Context())) + req = req.WithContext(reqctx.NewRequestContextForTest(req.Context())) return req } @@ -60,17 +61,16 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont } resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} chiCtx := chi.NewRouteContext() ctx := context.NewWebContext(base, opt.Render, nil) - ctx.AppendContextValue(context.WebContextKey, ctx) - ctx.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(context.WebContextKey, ctx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) if opt.SessionStore != nil { - ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore) + ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore) ctx.Session = opt.SessionStore } ctx.Cache = cache.GetCache() @@ -83,27 +83,24 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.APIContext{Base: base} - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later - chiCtx := chi.NewRouteContext() - ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { resp := httptest.NewRecorder() req := mockRequest(t, reqPath) - base, baseCleanUp := context.NewBaseContext(resp, req) + base := context.NewBaseContext(resp, req) base.Data = middleware.GetContextData(req.Context()) base.Locale = &translation.MockLocale{} ctx := &context.PrivateContext{Base: base} - _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later chiCtx := chi.NewRouteContext() - ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) + ctx.SetContextValue(chi.RouteCtxKey, chiCtx) return ctx, resp } diff --git a/services/markup/main_test.go b/services/markup/main_test.go index 5553ebc058..d04a18bfa1 100644 --- a/services/markup/main_test.go +++ b/services/markup/main_test.go @@ -11,6 +11,6 @@ import ( func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ - FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"}, + FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml", "issue.yml"}, }) } diff --git a/services/markup/processorhelper.go b/services/markup/renderhelper.go similarity index 88% rename from services/markup/processorhelper.go rename to services/markup/renderhelper.go index 1f1abf496a..4b9852b48b 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/renderhelper.go @@ -11,9 +11,10 @@ import ( gitea_context "code.gitea.io/gitea/services/context" ) -func ProcessorHelper() *markup.RenderHelperFuncs { +func FormalRenderHelperFuncs() *markup.RenderHelperFuncs { return &markup.RenderHelperFuncs{ RenderRepoFileCodePreview: renderRepoFileCodePreview, + RenderRepoIssueIconTitle: renderRepoIssueIconTitle, IsUsernameMentionable: func(ctx context.Context, username string) bool { mentionedUser, err := user.GetUserByName(ctx, username) if err != nil { diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/renderhelper_codepreview.go similarity index 97% rename from services/markup/processorhelper_codepreview.go rename to services/markup/renderhelper_codepreview.go index 0500e57e46..170c70c409 100644 --- a/services/markup/processorhelper_codepreview.go +++ b/services/markup/renderhelper_codepreview.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/repository/files" ) @@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie return "", err } if !perms.CanRead(unit.TypeCode) { - return "", fmt.Errorf("no permission") + return "", util.ErrPermissionDenied } gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/renderhelper_codepreview_test.go similarity index 95% rename from services/markup/processorhelper_codepreview_test.go rename to services/markup/renderhelper_codepreview_test.go index 154e4e8e44..ea945584b4 100644 --- a/services/markup/processorhelper_codepreview_test.go +++ b/services/markup/renderhelper_codepreview_test.go @@ -9,12 +9,13 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestProcessorHelperCodePreview(t *testing.T) { +func TestRenderHelperCodePreview(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) @@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) { LineStart: 1, LineStop: 10, }) - assert.ErrorContains(t, err, "no permission") + assert.ErrorIs(t, err, util.ErrPermissionDenied) } diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go new file mode 100644 index 0000000000..53a508e908 --- /dev/null +++ b/services/markup/renderhelper_issueicontitle.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "context" + "fmt" + "html/template" + + "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/util" + gitea_context "code.gitea.io/gitea/services/context" +) + +func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) { + webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context) + if !ok { + return "", fmt.Errorf("context is not a web context") + } + + textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex) + dbRepo := webCtx.Repo.Repository + if opts.OwnerName != "" { + dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName) + if err != nil { + return "", err + } + textIssueIndex = fmt.Sprintf("(%s/%s#%d)", dbRepo.OwnerName, dbRepo.Name, opts.IssueIndex) + } + if dbRepo == nil { + return "", nil + } + + issue, err := issues.GetIssueByIndex(ctx, dbRepo.ID, opts.IssueIndex) + if err != nil { + return "", err + } + + if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID { + perms, err := access.GetUserRepoPermission(ctx, dbRepo, webCtx.Doer) + if err != nil { + return "", err + } + if !perms.CanReadIssuesOrPulls(issue.IsPull) { + return "", util.ErrPermissionDenied + } + } + + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + return "", err + } + } + + htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue) + if err != nil { + return "", err + } + + return htmlutil.HTMLFormat(`%s %s %s`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil +} diff --git a/services/markup/renderhelper_issueicontitle_test.go b/services/markup/renderhelper_issueicontitle_test.go new file mode 100644 index 0000000000..adce8401e0 --- /dev/null +++ b/services/markup/renderhelper_issueicontitle_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "testing" + + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestRenderHelperIssueIconTitle(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo1", + LinkHref: "/link", + IssueIndex: 1, + }) + assert.NoError(t, err) + assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (user2/repo1#1)`, string(htm)) + + ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) + _, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{ + OwnerName: "user2", + RepoName: "repo2", + LinkHref: "/link", + IssueIndex: 2, + }) + assert.ErrorIs(t, err, util.ErrPermissionDenied) +} diff --git a/services/markup/processorhelper_test.go b/services/markup/renderhelper_mention_test.go similarity index 57% rename from services/markup/processorhelper_test.go rename to services/markup/renderhelper_mention_test.go index 170edae0e0..c244fa3d21 100644 --- a/services/markup/processorhelper_test.go +++ b/services/markup/renderhelper_mention_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestProcessorHelper(t *testing.T) { +func TestRenderHelperMention(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) userPublic := "user1" @@ -32,23 +32,22 @@ func TestProcessorHelper(t *testing.T) { unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) // when using general context, use user's visibility to check - assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch)) // when using web context, use user.IsUserVisibleToViewer to check req, err := http.NewRequest("GET", "/", nil) assert.NoError(t, err) - base, baseCleanUp := gitea_context.NewBaseContext(httptest.NewRecorder(), req) - defer baseCleanUp() + base := gitea_context.NewBaseContextForTest(httptest.NewRecorder(), req) giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) assert.NoError(t, err) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) - assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic)) + assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate)) } diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 197d8102dd..8f8a5d82e7 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -10,7 +10,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -25,12 +24,12 @@ func getAuthorSignatureSquash(ctx *mergeContext) (*git.Signature, error) { // Try to get an signature from the same user in one of the commits, as the // poster email might be private or commits might have a different signature // than the primary email address of the poster. - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpenPath(ctx, ctx.tmpBasePath) + gitRepo, err := git.OpenRepository(ctx, ctx.tmpBasePath) if err != nil { log.Error("%-v Unable to open base repository: %v", ctx.pr, err) return nil, err } - defer closer.Close() + defer gitRepo.Close() commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD") if err != nil { diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl index 2aea036ec2..62308e4aa0 100644 --- a/templates/package/metadata/arch.tmpl +++ b/templates/package/metadata/arch.tmpl @@ -1,4 +1,4 @@ {{if eq .PackageDescriptor.Package.Type "arch"}} - {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} + {{range .PackageDescriptor.Metadata.Licenses}}
{{svg "octicon-law"}} {{.}}
{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "packages.details.project_site"}}
{{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 3f9af18027..9fdbf45939 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -693,7 +693,7 @@ {{else if eq .Type 38}}
{{svg "octicon-clock"}} - {{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} + {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} {{$timeStr := .Content|TimeEstimateString}} diff --git a/templates/shared/actions/runner_list.tmpl b/templates/shared/actions/runner_list.tmpl index f652d56e09..e5907da8e8 100644 --- a/templates/shared/actions/runner_list.tmpl +++ b/templates/shared/actions/runner_list.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
-
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl index a62714e988..f828de5c66 100644 --- a/templates/shared/issueicon.tmpl +++ b/templates/shared/issueicon.tmpl @@ -1,25 +1,25 @@ -{{if .IsPull}} - {{if not .PullRequest}} +{{- if .IsPull -}} + {{- if not .PullRequest -}} No PullRequest - {{else}} - {{if .IsClosed}} - {{if .PullRequest.HasMerged}} - {{svg "octicon-git-merge" 16 "text purple"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text red"}} - {{end}} - {{else}} - {{if .PullRequest.IsWorkInProgress ctx}} - {{svg "octicon-git-pull-request-draft" 16 "text grey"}} - {{else}} - {{svg "octicon-git-pull-request" 16 "text green"}} - {{end}} - {{end}} - {{end}} -{{else}} - {{if .IsClosed}} - {{svg "octicon-issue-closed" 16 "text red"}} - {{else}} - {{svg "octicon-issue-opened" 16 "text green"}} - {{end}} -{{end}} + {{- else -}} + {{- if .IsClosed -}} + {{- if .PullRequest.HasMerged -}} + {{- svg "octicon-git-merge" 16 "text purple" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text red" -}} + {{- end -}} + {{- else -}} + {{- if .PullRequest.IsWorkInProgress ctx -}} + {{- svg "octicon-git-pull-request-draft" 16 "text grey" -}} + {{- else -}} + {{- svg "octicon-git-pull-request" 16 "text green" -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{- if .IsClosed -}} + {{- svg "octicon-issue-closed" 16 "text red" -}} + {{- else -}} + {{- svg "octicon-issue-opened" 16 "text green" -}} + {{- end -}} +{{- end -}} diff --git a/tests/test_utils.go b/tests/test_utils.go index 0fe0200ea7..96eb5731b4 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -58,7 +58,7 @@ func InitTest(requireGitea bool) { _ = os.Setenv("GITEA_CONF", giteaConf) fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) if !setting.EnableSQLite3 { - testlogger.Fatalf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify` + "\n") + testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n") } } if !filepath.IsAbs(giteaConf) {