0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-19 02:00:58 +02:00

Merge branch 'main' into add-file-tree-to-file-view-page

This commit is contained in:
Lunny Xiao 2024-12-23 20:55:40 -08:00
commit a86c9e43a9
90 changed files with 923 additions and 562 deletions

View File

@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" 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/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
@ -51,7 +52,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } 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 return &runnerToken, nil
} }
@ -68,19 +69,15 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
return err 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. // 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 { if ownerID != 0 && repoID != 0 {
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // 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. // Remove OwnerID to avoid confusion; it's not worth returning an error here.
ownerID = 0 ownerID = 0
} }
token, err := util.CryptoRandomString(40)
if err != nil {
return nil, err
}
runnerToken := &ActionRunnerToken{ runnerToken := &ActionRunnerToken{
OwnerID: ownerID, OwnerID: ownerID,
RepoID: repoID, RepoID: repoID,
@ -95,11 +92,19 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
return err return err
} }
_, err = db.GetEngine(ctx).Insert(runnerToken) _, err := db.GetEngine(ctx).Insert(runnerToken)
return err 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 // GetLatestRunnerToken returns the latest runner token
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
if ownerID != 0 && repoID != 0 { if ownerID != 0 && repoID != 0 {

View File

@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error {
x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
if err != nil { if err != nil {
if strings.Contains(err.Error(), "unknown driver") { 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 return err
} }

View File

@ -216,8 +216,6 @@ type CommitsByFileAndRangeOptions struct {
// CommitsByFileAndRange return the commits according revision file and the page // CommitsByFileAndRange return the commits according revision file and the page
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
skip := (opts.Page - 1) * setting.Git.CommitsRangeSize
stdoutReader, stdoutWriter := io.Pipe() stdoutReader, stdoutWriter := io.Pipe()
defer func() { defer func() {
_ = stdoutReader.Close() _ = stdoutReader.Close()
@ -226,8 +224,8 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions)
go func() { go func() {
stderr := strings.Builder{} stderr := strings.Builder{}
gitCmd := NewCommand(repo.Ctx, "rev-list"). gitCmd := NewCommand(repo.Ctx, "rev-list").
AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize).
AddOptionFormat("--skip=%d", skip) AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize)
gitCmd.AddDynamicArguments(opts.Revision) gitCmd.AddDynamicArguments(opts.Revision)
if opts.Not != "" { if opts.Not != "" {

View File

@ -8,7 +8,11 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestRepository_GetCommitBranches(t *testing.T) { 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)
}

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "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. // contextKey is a value for use with context.WithValue.
type contextKey struct { type contextKey struct {
name string repoPath 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
} }
// RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it // 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) { func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) {
gitRepo := repositoryFromContext(ctx, repo) ds := reqctx.GetRequestDataStore(ctx)
if gitRepo != nil { if ds != nil {
return gitRepo, util.NopCloser{}, nil gitRepo, err := RepositoryFromRequestContextOrOpen(ctx, ds, repo)
return gitRepo, util.NopCloser{}, err
} }
gitRepo, err := OpenRepository(ctx, repo) gitRepo, err := OpenRepository(ctx, repo)
return gitRepo, gitRepo, err return gitRepo, gitRepo, err
} }
// repositoryFromContextPath attempts to get the repository from the context // RepositoryFromRequestContextOrOpen opens the repository at the given relative path in the provided request context
func repositoryFromContextPath(ctx context.Context, path string) *git.Repository { // The repo will be automatically closed when the request context is done
value := ctx.Value(RepositoryContextKey) func RepositoryFromRequestContextOrOpen(ctx context.Context, ds reqctx.RequestDataStore, repo Repository) (*git.Repository, error) {
if value == nil { ck := contextKey{repoPath: repoPath(repo)}
return nil if gitRepo, ok := ctx.Value(ck).(*git.Repository); ok {
return gitRepo, nil
} }
gitRepo, err := git.OpenRepository(ctx, ck.repoPath)
if repo, ok := value.(*git.Repository); ok && repo != nil { if err != nil {
if repo.Path == path { return nil, err
return repo
}
} }
ds.AddCloser(gitRepo)
return nil ds.SetContextValue(ck, gitRepo)
} return gitRepo, 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
} }

View File

@ -14,15 +14,11 @@ import (
// WalkReferences walks all the references from the repository // WalkReferences walks all the references from the repository
// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. // 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) { func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) {
gitRepo := repositoryFromContext(ctx, repo) gitRepo, closer, err := RepositoryFromContextOrOpen(ctx, repo)
if gitRepo == nil { if err != nil {
var err error return 0, err
gitRepo, err = OpenRepository(ctx, repo)
if err != nil {
return 0, err
}
defer gitRepo.Close()
} }
defer closer.Close()
i := 0 i := 0
iter, err := gitRepo.GoGitRepo().References() iter, err := gitRepo.GoGitRepo().References()

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"golang.org/x/net/html" "golang.org/x/net/html"
@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
node = node.NextSibling.NextSibling 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
}
}

View File

@ -4,9 +4,9 @@
package markup package markup
import ( import (
"strconv"
"strings" "strings"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
@ -16,8 +16,16 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"golang.org/x/net/html" "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) { func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil { if ctx.RenderOptions.Metas == nil {
return 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) { func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil { if ctx.RenderOptions.Metas == nil {
return return
@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true" crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
var ( var ref *references.RenderizableReference
found bool
ref *references.RenderizableReference
)
next := node.NextSibling next := node.NextSibling
for node != nil && node != next { for node != nil && node != next {
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"] _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
// Repos with external issue trackers might still need to reference local PRs // 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 // 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 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"] { switch ctx.RenderOptions.Metas["style"] {
case "", IssueNameStyleNumeric: case "", IssueNameStyleNumeric:
found, ref = foundNumeric, refNumeric ref = refNumeric
case IssueNameStyleAlphanumeric: case IssueNameStyleAlphanumeric:
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
case IssueNameStyleRegexp: case IssueNameStyleRegexp:
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"]) pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
if err != nil { if err != nil {
return 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 // 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 hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that // 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. // Allow a free-pass when non-numeric pattern wasn't found.
if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) { if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
found = foundNumeric
ref = refNumeric ref = refNumeric
} }
} }
if !found {
if ref == nil {
return return
} }
var link *html.Node 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 { if hasExtTrackFormat && !ref.IsPull {
ctx.RenderOptions.Metas["index"] = ref.Issue 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) 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 { } else {
// Path determines the type of link that will be rendered. It's unknown at this point whether // 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 // 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. // 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") issuePath := util.Iif(ref.IsPull, "pulls", "issues")
if ref.Owner == "" { linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
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") // at the moment, only render the issue index in a full line (or simple line) as icon+title
} else { // otherwise it would be too noisy for "take #1 as an example" in a sentence
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp) if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
link = createLink(ctx, linkHref, reftext, "ref-issue") 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 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
}
}

View File

@ -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("<div>issue #%d</div>", 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",
`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
)
})
t.Run("ListIssueRef", func(t *testing.T) {
test(
"* #12345",
`<ul>
<li><div>issue #12345</div></li>
</ul>`,
)
})
t.Run("ListIssueRefNormal", func(t *testing.T) {
test(
"* foo #12345 bar",
`<ul>
<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
</ul>`,
)
})
t.Run("ListTodoIssueRef", func(t *testing.T) {
test(
"* [ ] #12345",
`<ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="2"/><div>issue #12345</div></li>
</ul>`,
)
})
}

View File

@ -38,6 +38,7 @@ type RenderHelper interface {
type RenderHelperFuncs struct { type RenderHelperFuncs struct {
IsUsernameMentionable func(ctx context.Context, username string) bool IsUsernameMentionable func(ctx context.Context, username string) bool
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error) RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error)
} }
var DefaultRenderHelperFuncs *RenderHelperFuncs var DefaultRenderHelperFuncs *RenderHelperFuncs

View File

@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference {
} }
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. // 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 var match []int
if !crossLinkOnly { if !crossLinkOnly {
match = issueNumericPattern.FindStringSubmatchIndex(content) match = issueNumericPattern.FindStringSubmatchIndex(content)
} }
if match == nil { if match == nil {
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); 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) r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
if r == nil { if r == nil {
return false, nil return nil
} }
return true, &RenderizableReference{ return &RenderizableReference{
Issue: r.issue, Issue: r.issue,
Owner: r.owner, Owner: r.owner,
Name: r.name, Name: r.name,
@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe
} }
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string. // 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) match := pattern.FindStringSubmatchIndex(content)
if len(match) < 4 { if len(match) < 4 {
return false, nil return nil
} }
action, location := findActionKeywords([]byte(content), match[2]) action, location := findActionKeywords([]byte(content), match[2])
return &RenderizableReference{
return true, &RenderizableReference{
Issue: content[match[2]:match[3]], Issue: content[match[2]:match[3]],
RefLocation: &RefSpan{Start: match[0], End: match[1]}, RefLocation: &RefSpan{Start: match[0], End: match[1]},
Action: action, 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. // 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) match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
if match == nil { if match == nil {
return false, nil return nil
} }
action, location := findActionKeywords([]byte(content), match[2]) action, location := findActionKeywords([]byte(content), match[2])
return &RenderizableReference{
return true, &RenderizableReference{
Issue: content[match[2]:match[3]], Issue: content[match[2]:match[3]],
RefLocation: &RefSpan{Start: match[2], End: match[3]}, RefLocation: &RefSpan{Start: match[2], End: match[3]},
Action: action, Action: action,

View File

@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) {
} }
for _, fixture := range alnumFixtures { for _, fixture := range alnumFixtures {
found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) ref := FindRenderizableReferenceAlphanumeric(fixture.input)
if fixture.issue == "" { if fixture.issue == "" {
assert.False(t, found, "Failed to parse: {%s}", fixture.input) assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input)
} else { } 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.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.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input)
assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input)

123
modules/reqctx/datastore.go Normal file
View File

@ -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)}}
}

View File

@ -10,7 +10,7 @@ import (
"sync" "sync"
) )
type normalizeVarsStruct struct { type globalVarsStruct struct {
reXMLDoc, reXMLDoc,
reComment, reComment,
reAttrXMLNs, reAttrXMLNs,
@ -18,26 +18,23 @@ type normalizeVarsStruct struct {
reAttrClassPrefix *regexp.Regexp reAttrClassPrefix *regexp.Regexp
} }
var ( var globalVars = sync.OnceValue(func() *globalVarsStruct {
normalizeVars *normalizeVarsStruct return &globalVarsStruct{
normalizeVarsOnce sync.Once 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 // 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. // 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 { func Normalize(data []byte, size int) []byte {
normalizeVarsOnce.Do(func() { vars := globalVars()
normalizeVars = &normalizeVarsStruct{ data = vars.reXMLDoc.ReplaceAll(data, nil)
reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`), data = vars.reComment.ReplaceAll(data, nil)
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)
data = bytes.TrimSpace(data) data = bytes.TrimSpace(data)
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">")) svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte {
return data return data
} }
normalized := bytes.Clone(svgTag) normalized := bytes.Clone(svgTag)
normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil) normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil)
normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil) normalized = vars.reAttrSize.ReplaceAll(normalized, nil)
normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`)) normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
normalized = bytes.TrimSpace(normalized) normalized = bytes.TrimSpace(normalized)
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size) normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
if !bytes.Contains(normalized, []byte(` class="`)) { if !bytes.Contains(normalized, []byte(` class="`)) {

View File

@ -4,7 +4,6 @@
package web package web
import ( import (
goctx "context"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@ -51,7 +50,6 @@ func (r *responseWriter) WriteHeader(statusCode int) {
var ( var (
httpReqType = reflect.TypeOf((*http.Request)(nil)) httpReqType = reflect.TypeOf((*http.Request)(nil))
respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() 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 // 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 { if !hasStatusProvider {
panic(fmt.Sprintf("handler should have at least one ResponseStatusProvider argument, but got %s", fn.Type())) 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 { if fn.Type().NumOut() != 0 {
panic(fmt.Sprintf("handler should have no return value or only one argument, but got %s", fn.Type())) panic(fmt.Sprintf("handler should have no return value other than registered ones, 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()))
} }
} }
@ -105,16 +100,10 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect
return argsIn return argsIn
} }
func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc { func handleResponse(fn reflect.Value, ret []reflect.Value) {
if len(ret) == 1 { if len(ret) != 0 {
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 {
panic(fmt.Sprintf("unsupported return values: %s", fn.Type())) panic(fmt.Sprintf("unsupported return values: %s", fn.Type()))
} }
return nil
} }
func hasResponseBeenWritten(argsIn []reflect.Value) bool { 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) routing.UpdateFuncInfo(req.Context(), funcInfo)
ret := fn.Call(argsIn) ret := fn.Call(argsIn)
// handle the return value, and defer the cancel function if there is one // handle the return value (no-op at the moment)
cancelFunc := handleResponse(fn, ret) handleResponse(fn, ret)
if cancelFunc != nil {
defer cancelFunc()
}
// if the response has not been written, call the next handler // if the response has not been written, call the next handler
if next != nil && !hasResponseBeenWritten(argsIn) { if next != nil && !hasResponseBeenWritten(argsIn) {

View File

@ -7,46 +7,21 @@ import (
"context" "context"
"time" "time"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "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" const ContextDataKeySignedUser = "SignedUser"
type contextDataKeyType struct{} func GetContextData(c context.Context) reqctx.ContextData {
if rc := reqctx.GetRequestDataStore(c); rc != nil {
var contextDataKey contextDataKeyType return rc.GetData()
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
} }
return nil return nil
} }
func CommonTemplateContextData() ContextData { func CommonTemplateContextData() reqctx.ContextData {
return ContextData{ return reqctx.ContextData{
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations, "IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,
"ShowRegistrationButton": setting.Service.ShowRegistrationButton, "ShowRegistrationButton": setting.Service.ShowRegistrationButton,

View File

@ -7,11 +7,13 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"net/url" "net/url"
"code.gitea.io/gitea/modules/reqctx"
) )
// Flash represents a one time data transfer between two requests. // Flash represents a one time data transfer between two requests.
type Flash struct { type Flash struct {
DataStore ContextDataStore DataStore reqctx.RequestDataStore
url.Values url.Values
ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string ErrorMsg, WarningMsg, InfoMsg, SuccessMsg string
} }

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -29,12 +30,12 @@ func Bind[T any](_ T) http.HandlerFunc {
} }
// SetForm set the form object // SetForm set the form object
func SetForm(dataStore middleware.ContextDataStore, obj any) { func SetForm(dataStore reqctx.ContextDataProvider, obj any) {
dataStore.GetData()["__form"] = obj dataStore.GetData()["__form"] = obj
} }
// GetForm returns the validate form information // GetForm returns the validate form information
func GetForm(dataStore middleware.ContextDataStore) any { func GetForm(dataStore reqctx.RequestDataStore) any {
return dataStore.GetData()["__form"] return dataStore.GetData()["__form"]
} }

View File

@ -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.failed=Úprava proměnné se nezdařila.
variables.update.success=Proměnná byla upravena. variables.update.success=Proměnná byla upravena.
[projects] [projects]
deleted.display_name=Odstraněný projekt deleted.display_name=Odstraněný projekt
type-1.display_name=Samostatný projekt type-1.display_name=Samostatný projekt

View File

@ -3556,6 +3556,7 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt.
variables.update.failed=Fehler beim Bearbeiten der Variable. variables.update.failed=Fehler beim Bearbeiten der Variable.
variables.update.success=Die Variable wurde bearbeitet. variables.update.success=Die Variable wurde bearbeitet.
[projects] [projects]
type-1.display_name=Individuelles Projekt type-1.display_name=Individuelles Projekt
type-2.display_name=Repository-Projekt type-2.display_name=Repository-Projekt

View File

@ -3439,6 +3439,7 @@ variables.creation.success=Η μεταβλητή "%s" έχει προστεθε
variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής. variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής.
variables.update.success=Η μεταβλητή έχει τροποποιηθεί. variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
[projects] [projects]
type-1.display_name=Ατομικό Έργο type-1.display_name=Ατομικό Έργο
type-2.display_name=Έργο Αποθετηρίου type-2.display_name=Έργο Αποθετηρίου

View File

@ -3722,6 +3722,7 @@ runners.status.active = Active
runners.status.offline = Offline runners.status.offline = Offline
runners.version = Version runners.version = Version
runners.reset_registration_token = Reset registration token 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 runners.reset_registration_token_success = Runner registration token reset successfully
runs.all_workflows = All Workflows runs.all_workflows = All Workflows

View File

@ -3415,6 +3415,7 @@ variables.creation.success=La variable "%s" ha sido añadida.
variables.update.failed=Error al editar la variable. variables.update.failed=Error al editar la variable.
variables.update.success=La variable ha sido editada. variables.update.success=La variable ha sido editada.
[projects] [projects]
type-1.display_name=Proyecto individual type-1.display_name=Proyecto individual
type-2.display_name=Proyecto repositorio type-2.display_name=Proyecto repositorio

View File

@ -2529,6 +2529,7 @@ runs.commit=کامیت
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -1707,6 +1707,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -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.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 <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches=Vous avez soumis sur la branche <strong>%[1]s</strong> %[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_prompt_base_newer=La branche de base %s a de nouveaux changements
pulls.upstream_diverging_merge=Synchroniser la bifurcation 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.failed=Impossible déditer la variable.
variables.update.success=La variable a bien été modifiée. variables.update.success=La variable a bien été modifiée.
[projects] [projects]
deleted.display_name=Projet supprimé deleted.display_name=Projet supprimé
type-1.display_name=Projet personnel type-1.display_name=Projet personnel

View File

@ -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.failed=Theip ar athróg a chur in eagar.
variables.update.success=Tá an t-athróg curtha in eagar. variables.update.success=Tá an t-athróg curtha in eagar.
[projects] [projects]
deleted.display_name=Tionscadal scriosta deleted.display_name=Tionscadal scriosta
type-1.display_name=Tionscadal Aonair type-1.display_name=Tionscadal Aonair

View File

@ -1615,6 +1615,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -1444,6 +1444,7 @@ variables.creation.success=Variabel "%s" telah ditambahkan.
variables.update.failed=Gagal mengedit variabel. variables.update.failed=Gagal mengedit variabel.
variables.update.success=Variabel telah diedit. variables.update.success=Variabel telah diedit.
[projects] [projects]
type-1.display_name=Proyek Individu type-1.display_name=Proyek Individu
type-2.display_name=Proyek Repositori type-2.display_name=Proyek Repositori

View File

@ -1342,6 +1342,7 @@ runs.commit=Framlag
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -2807,6 +2807,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -3762,6 +3762,7 @@ variables.creation.success=変数 "%s" を追加しました。
variables.update.failed=変数を更新できませんでした。 variables.update.failed=変数を更新できませんでした。
variables.update.success=変数を更新しました。 variables.update.success=変数を更新しました。
[projects] [projects]
deleted.display_name=削除されたプロジェクト deleted.display_name=削除されたプロジェクト
type-1.display_name=個人プロジェクト type-1.display_name=個人プロジェクト

View File

@ -1563,6 +1563,7 @@ runs.commit=커밋
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -3443,6 +3443,7 @@ variables.creation.success=Mainīgais "%s" tika pievienots.
variables.update.failed=Neizdevās labot mainīgo. variables.update.failed=Neizdevās labot mainīgo.
variables.update.success=Mainīgais tika labots. variables.update.success=Mainīgais tika labots.
[projects] [projects]
type-1.display_name=Individuālais projekts type-1.display_name=Individuālais projekts
type-2.display_name=Repozitorija projekts type-2.display_name=Repozitorija projekts

View File

@ -2537,6 +2537,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -2430,6 +2430,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -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. need_approval_desc=Precisa de aprovação para executar workflows para pull request do fork.
[projects] [projects]
type-1.display_name=Projeto individual type-1.display_name=Projeto individual
type-2.display_name=Projeto do repositório type-2.display_name=Projeto do repositório

View File

@ -3770,6 +3770,9 @@ variables.creation.success=A variável "%s" foi adicionada.
variables.update.failed=Falha ao editar a variável. variables.update.failed=Falha ao editar a variável.
variables.update.success=A variável foi editada. 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] [projects]
deleted.display_name=Planeamento eliminado deleted.display_name=Planeamento eliminado
type-1.display_name=Planeamento individual type-1.display_name=Planeamento individual

View File

@ -3373,6 +3373,7 @@ variables.creation.success=Переменная «%s» добавлена.
variables.update.failed=Не удалось изменить переменную. variables.update.failed=Не удалось изменить переменную.
variables.update.success=Переменная изменена. variables.update.success=Переменная изменена.
[projects] [projects]
type-1.display_name=Индивидуальный проект type-1.display_name=Индивидуальный проект
type-2.display_name=Проект репозитория type-2.display_name=Проект репозитория

View File

@ -2470,6 +2470,7 @@ runs.commit=කැප
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -1328,6 +1328,7 @@ runners.labels=Štítky
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -2005,6 +2005,7 @@ runs.commit=Commit
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -3633,6 +3633,7 @@ variables.creation.success=`"%s" değişkeni eklendi.`
variables.update.failed=Değişken düzenlenemedi. variables.update.failed=Değişken düzenlenemedi.
variables.update.success=Değişken düzenlendi. variables.update.success=Değişken düzenlendi.
[projects] [projects]
deleted.display_name=Silinmiş Proje deleted.display_name=Silinmiş Proje
type-1.display_name=Kişisel Proje type-1.display_name=Kişisel Proje

View File

@ -2538,6 +2538,7 @@ runs.commit=Коміт
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -1945,6 +1945,8 @@ pulls.delete.title=删除此合并请求?
pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它)
pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong> pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 <strong>%[1]s</strong>
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_prompt_base_newer=基础分支 %s 有新的更改
pulls.upstream_diverging_merge=同步派生 pulls.upstream_diverging_merge=同步派生
@ -3770,6 +3772,7 @@ variables.creation.success=变量 “%s” 添加成功。
variables.update.failed=编辑变量失败。 variables.update.failed=编辑变量失败。
variables.update.success=该变量已被编辑。 variables.update.success=该变量已被编辑。
[projects] [projects]
deleted.display_name=已删除项目 deleted.display_name=已删除项目
type-1.display_name=个人项目 type-1.display_name=个人项目

View File

@ -975,6 +975,7 @@ runners.task_list.repository=儲存庫
[projects] [projects]
[git.filemode] [git.filemode]

View File

@ -3763,6 +3763,7 @@ variables.creation.success=已新增變數「%s」。
variables.update.failed=編輯變數失敗。 variables.update.failed=編輯變數失敗。
variables.update.success=已編輯變數。 variables.update.success=已編輯變數。
[projects] [projects]
deleted.display_name=已刪除的專案 deleted.display_name=已刪除的專案
type-1.display_name=個人專案 type-1.display_name=個人專案

View File

@ -126,11 +126,10 @@ func ArtifactsRoutes(prefix string) *web.Router {
func ArtifactContexter() func(next http.Handler) http.Handler { func ArtifactContexter() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
defer baseCleanUp()
ctx := &ArtifactContext{Base: base} ctx := &ArtifactContext{Base: base}
ctx.AppendContextValue(artifactContextKey, ctx) ctx.SetContextValue(artifactContextKey, ctx)
// action task call server api with Bearer ACTIONS_RUNTIME_TOKEN // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN
// we should verify the ACTIONS_RUNTIME_TOKEN // we should verify the ACTIONS_RUNTIME_TOKEN

View File

@ -126,12 +126,9 @@ type artifactV4Routes struct {
func ArtifactV4Contexter() func(next http.Handler) http.Handler { func ArtifactV4Contexter() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
defer baseCleanUp()
ctx := &ArtifactContext{Base: base} ctx := &ArtifactContext{Base: base}
ctx.AppendContextValue(artifactContextKey, ctx) ctx.SetContextValue(artifactContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req) next.ServeHTTP(ctx.Resp, ctx.Req)
}) })
} }

View File

@ -729,15 +729,11 @@ func CreateBranchProtection(ctx *context.APIContext) {
} else { } else {
if !isPlainRule { if !isPlainRule {
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return 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 // 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) matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName)
@ -1061,15 +1057,11 @@ func EditBranchProtection(ctx *context.APIContext) {
} else { } else {
if !isPlainRule { if !isPlainRule {
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return 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 // FIXME: since we only need to recheck files protected rules, we could improve this

View File

@ -44,13 +44,12 @@ func CompareDiff(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return return
} }
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
} }
infoPath := ctx.PathParam("*") infoPath := ctx.PathParam("*")

View File

@ -28,13 +28,12 @@ func DownloadArchive(ctx *context.APIContext) {
} }
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return return
} }
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
} }
r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp)

View File

@ -287,13 +287,12 @@ func GetArchive(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return return
} }
ctx.Repo.GitRepo = gitRepo
defer gitRepo.Close()
} }
archiveDownload(ctx) archiveDownload(ctx)

View File

@ -726,12 +726,11 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
if ctx.Repo.GitRepo == nil && !repo.IsEmpty { if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
var err error var err error
ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err)
return err return err
} }
defer ctx.Repo.GitRepo.Close()
} }
// Default branch only updated if changed and exist or the repository is empty // Default branch only updated if changed and exist or the repository is empty

View File

@ -100,7 +100,7 @@ func Transfer(ctx *context.APIContext) {
} }
if ctx.Repo.GitRepo != nil { if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close() _ = ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil ctx.Repo.GitRepo = nil
} }

View File

@ -12,8 +12,8 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web/middleware"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -21,7 +21,7 @@ import (
func TestRenderPanicErrorPage(t *testing.T) { func TestRenderPanicErrorPage(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
req := &http.Request{URL: &url.URL{}} 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)")) RenderPanicErrorPage(w, req, errors.New("fake panic error (for test only)"))
respContent := w.Body.String() respContent := w.Body.String()
assert.Contains(t, respContent, `class="page-content status-page-500"`) assert.Contains(t, respContent, `class="page-content status-page-500"`)

View File

@ -4,16 +4,14 @@
package common package common
import ( import (
go_context "context"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/httplib" "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/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
@ -24,54 +22,12 @@ import (
// ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery // ProtocolMiddlewares returns HTTP protocol related middlewares, and it provides a global panic recovery
func ProtocolMiddlewares() (handlers []any) { func ProtocolMiddlewares() (handlers []any) {
// make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly // the order is important
handlers = append(handlers, func(next http.Handler) http.Handler { handlers = append(handlers, ChiRoutePathHandler()) // make sure chi has correct paths
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { handlers = append(handlers, RequestContextHandler()) // // prepare the context and panic recovery
ctx := chi.RouteContext(req.Context())
if req.URL.RawPath == "" {
ctx.RoutePath = req.URL.EscapedPath()
} else {
ctx.RoutePath = req.URL.RawPath
}
next.ServeHTTP(resp, req)
})
})
// prepare the ContextData and panic recovery if setting.ReverseProxyLimit > 0 && len(setting.ReverseProxyTrustedProxies) > 0 {
handlers = append(handlers, func(next http.Handler) http.Handler { handlers = append(handlers, ForwardedHeadersHandler(setting.ReverseProxyLimit, setting.ReverseProxyTrustedProxies))
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.IsRouteLogEnabled() { if setting.IsRouteLogEnabled() {
@ -85,6 +41,59 @@ func ProtocolMiddlewares() (handlers []any) {
return handlers 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 { func Sessioner() func(next http.Handler) http.Handler {
return session.Sessioner(session.Options{ return session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider, Provider: setting.SessionConfig.Provider,

View File

@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) {
highlight.NewContext() highlight.NewContext()
external.RegisterRenderers() external.RegisterRenderers()
markup.Init(markup_service.ProcessorHelper()) markup.Init(markup_service.FormalRenderHelperFuncs())
if setting.EnableSQLite3 { if setting.EnableSQLite3 {
log.Info("SQLite3 support is enabled") log.Info("SQLite3 support is enabled")
@ -171,7 +171,7 @@ func InitWebInstalled(ctx context.Context) {
auth.Init() auth.Init()
mustInit(svg.Init) mustInit(svg.Init)
actions_service.Init() mustInitCtx(ctx, actions_service.Init)
mustInit(repo_service.InitLicenseClassifier) mustInit(repo_service.InitLicenseClassifier)

View File

@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
@ -61,15 +62,11 @@ func Contexter() func(next http.Handler) http.Handler {
envConfigKeys := setting.CollectEnvConfigKeys() envConfigKeys := setting.CollectEnvConfigKeys()
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
defer baseCleanUp()
ctx := context.NewWebContext(base, rnd, session.GetSession(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.CommonTemplateContextData())
ctx.Data.MergeFrom(middleware.ContextData{ ctx.Data.MergeFrom(reqctx.ContextData{
"Context": ctx, // TODO: use "ctx" in template and remove this
"locale": ctx.Locale,
"Title": ctx.Locale.Tr("install.install"), "Title": ctx.Locale.Tr("install.install"),
"PageIsInstall": true, "PageIsInstall": true,
"DbTypeNames": dbTypeNames, "DbTypeNames": dbTypeNames,

View File

@ -63,8 +63,8 @@ func Routes() *web.Router {
r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo)
r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog)
r.Post("/hook/pre-receive/{owner}/{repo}", RepoAssignment, bind(private.HookOptions{}), HookPreReceive) 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/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/proc-receive/{owner}/{repo}", context.OverrideContext(), RepoAssignment, bind(private.HookOptions{}), HookProcReceive)
r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch) r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", RepoAssignment, SetDefaultBranch)
r.Get("/serv/none/{keyid}", ServNoCommand) r.Get("/serv/none/{keyid}", ServNoCommand)
r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) 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 // Fortunately, the LFS handlers are able to handle requests without a complete web context
common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) { common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
webContext := &context.Context{Base: ctx.Base} webContext := &context.Context{Base: ctx.Base}
ctx.AppendContextValue(context.WebContextKey, webContext) ctx.SetContextValue(context.WebContextKey, webContext)
}) })
}) })

View File

@ -4,7 +4,6 @@
package private package private
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
@ -17,40 +16,29 @@ import (
// This file contains common functions relating to setting the Repository for the internal routes // This file contains common functions relating to setting the Repository for the internal routes
// RepoAssignment assigns the repository and gitrepository to the private context // RepoAssignment assigns the repository and git repository to the private context
func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { func RepoAssignment(ctx *gitea_context.PrivateContext) {
ownerName := ctx.PathParam(":owner") ownerName := ctx.PathParam(":owner")
repoName := ctx.PathParam(":repo") repoName := ctx.PathParam(":repo")
repo := loadRepository(ctx, ownerName, repoName) repo := loadRepository(ctx, ownerName, repoName)
if ctx.Written() { if ctx.Written() {
// Error handled in loadRepository // Error handled in loadRepository
return nil return
} }
gitRepo, err := gitrepo.OpenRepository(ctx, repo) gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx, repo)
if err != nil { if err != nil {
log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err)
ctx.JSON(http.StatusInternalServerError, private.Response{ ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err),
}) })
return nil return
} }
ctx.Repo = &gitea_context.Repository{ ctx.Repo = &gitea_context.Repository{
Repository: repo, Repository: repo,
GitRepo: gitRepo, 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 { func loadRepository(ctx *gitea_context.PrivateContext, ownerName, repoName string) *repo_model.Repository {

View File

@ -136,9 +136,8 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
ctx.ServerError("ResetRunnerRegistrationToken", err) ctx.ServerError("ResetRunnerRegistrationToken", err)
return return
} }
ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success")) ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
ctx.Redirect(redirectTo) ctx.JSONRedirect(redirectTo)
} }
// RunnerDeletePost response for deleting a runner // RunnerDeletePost response for deleting a runner

View File

@ -4,7 +4,6 @@
package web package web
import ( import (
gocontext "context"
"net/http" "net/http"
"strings" "strings"
@ -463,7 +462,7 @@ func registerRoutes(m *web.Router) {
m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit). m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost) Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost) 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.Group("/blob_excerpt", func() {
m.Get("/{sha}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.ExcerptBlob) 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 // FIXME: refactor this function, use separate routes for wiki/code
if ctx.FormBool("wiki") { if ctx.FormBool("wiki") {
ctx.Data["PageIsWiki"] = true ctx.Data["PageIsWiki"] = true
repo.MustEnableWiki(ctx) repo.MustEnableWiki(ctx)
return nil return
} }
if ctx.Written() { if ctx.Written() {
return nil return
} }
cancel := context.RepoRef()(ctx) context.RepoRef()(ctx)
if ctx.Written() { if ctx.Written() {
return cancel return
} }
repo.MustBeNotEmpty(ctx) repo.MustBeNotEmpty(ctx)
return cancel
}) })
m.Group("/media", func() { m.Group("/media", func() {

View File

@ -4,23 +4,68 @@
package actions package actions
import ( 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/graceful"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify" 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 { if !setting.Actions.Enabled {
return return nil
} }
jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler) jobEmitterQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "actions_ready_job", jobEmitterQueueHandler)
if jobEmitterQueue == nil { 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) go graceful.GetManager().RunWithCancel(jobEmitterQueue)
notify_service.RegisterNotifier(NewNotifier()) notify_service.RegisterNotifier(NewNotifier())
return initGlobalRunnerToken(ctx)
} }

View File

@ -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")
})
}

View File

@ -8,12 +8,11 @@ import (
"net/http" "net/http"
user_model "code.gitea.io/gitea/models/user" 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/session"
"code.gitea.io/gitea/modules/web/middleware"
) )
// DataStore represents a data store type DataStore = reqctx.ContextDataProvider
type DataStore middleware.ContextDataStore
// SessionStore represents a session store // SessionStore represents a session store
type SessionStore session.Store type SessionStore session.Store

View File

@ -9,7 +9,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" 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" "code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -23,7 +23,7 @@ func TestUserIDFromToken(t *testing.T) {
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2) token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
assert.NoError(t, err) assert.NoError(t, err)
ds := make(middleware.ContextData) ds := make(reqctx.ContextData)
o := OAuth2{} o := OAuth2{}
uid := o.userIDFromToken(context.Background(), token, ds) uid := o.userIDFromToken(context.Background(), token, ds)

View File

@ -5,7 +5,6 @@
package context package context
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -212,17 +211,15 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
func APIContexter() func(http.Handler) http.Handler { func APIContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(w, req) base := NewBaseContext(w, req)
ctx := &APIContext{ ctx := &APIContext{
Base: base, Base: base,
Cache: cache.GetCache(), Cache: cache.GetCache(),
Repo: &Repository{PullRequest: &PullRequest{}}, Repo: &Repository{PullRequest: &PullRequest{}},
Org: &APIOrganization{}, Org: &APIOrganization{},
} }
defer baseCleanUp()
ctx.Base.AppendContextValue(apiContextKey, ctx) ctx.SetContextValue(apiContextKey, ctx)
ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid. // 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") { 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 // ReferencesGitRepo injects the GitRepo into the Context
// you can optional skip the IsEmpty check // you can optional skip the IsEmpty check
func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) { func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) {
return func(ctx *APIContext) (cancel context.CancelFunc) { return func(ctx *APIContext) {
// Empty repository does not have reference information. // Empty repository does not have reference information.
if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) { if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
return nil return
} }
// For API calls. // For API calls.
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
return cancel return
}
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 cancel
} }
} }

View File

@ -12,12 +12,12 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -25,65 +25,25 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
type contextValuePair struct {
key any
valueFn func() any
}
type BaseContextKeyType struct{} type BaseContextKeyType struct{}
var BaseContextKey BaseContextKeyType var BaseContextKey BaseContextKeyType
type Base struct { type Base struct {
originCtx context.Context context.Context
contextValues []contextValuePair reqctx.RequestDataStore
Resp ResponseWriter Resp ResponseWriter
Req *http.Request Req *http.Request
// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData. // 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 // 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 is mainly for Web context, although the API context also uses it in some cases: message response, form validation
Locale translation.Locale 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 // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
func (b *Base) AppendAccessControlExposeHeaders(names ...string) { func (b *Base) AppendAccessControlExposeHeaders(names ...string) {
val := b.RespHeader().Get("Access-Control-Expose-Headers") 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) 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 { func (b *Base) Tr(msg string, args ...any) template.HTML {
return b.Locale.Tr(msg, args...) 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...) return b.Locale.TrN(cnt, key1, keyN, args...)
} }
func NewBaseContext(resp http.ResponseWriter, req *http.Request) (b *Base, closeFunc func()) { func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base {
b = &Base{ ds := reqctx.GetRequestDataStore(req.Context())
originCtx: req.Context(), b := &Base{
Req: req, Context: req.Context(),
Resp: WrapResponseWriter(resp), RequestDataStore: ds,
Locale: middleware.Locale(resp, req), Req: req,
Data: middleware.GetContextData(req.Context()), Resp: WrapResponseWriter(resp),
Locale: middleware.Locale(resp, req),
Data: ds.GetData(),
} }
b.Req = b.Req.WithContext(b) b.Req = b.Req.WithContext(b)
b.AppendContextValue(BaseContextKey, b) ds.SetContextValue(BaseContextKey, b)
b.AppendContextValue(translation.ContextKey, b.Locale) ds.SetContextValue(translation.ContextKey, b.Locale)
b.AppendContextValue(httplib.RequestContextKey, b.Req) ds.SetContextValue(httplib.RequestContextKey, b.Req)
return b, b.cleanUp 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)
} }

View File

@ -14,6 +14,7 @@ import (
) )
func TestRedirect(t *testing.T) { func TestRedirect(t *testing.T) {
setting.IsInTesting = true
req, _ := http.NewRequest("GET", "/", nil) req, _ := http.NewRequest("GET", "/", nil)
cases := []struct { cases := []struct {
@ -28,10 +29,9 @@ func TestRedirect(t *testing.T) {
} }
for _, c := range cases { for _, c := range cases {
resp := httptest.NewRecorder() 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()) resp.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "dummy"}).String())
b.Redirect(c.url) b.Redirect(c.url)
cleanup()
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy" has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
assert.Equal(t, c.keep, has, "url = %q", c.url) assert.Equal(t, c.keep, has, "url = %q", c.url)
} }
@ -39,9 +39,8 @@ func TestRedirect(t *testing.T) {
req, _ = http.NewRequest("GET", "/", nil) req, _ = http.NewRequest("GET", "/", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req.Header.Add("HX-Request", "true") req.Header.Add("HX-Request", "true")
b, cleanup := NewBaseContext(resp, req) b := NewBaseContextForTest(resp, req)
b.Redirect("/other") b.Redirect("/other")
cleanup()
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
assert.Equal(t, http.StatusNoContent, resp.Code) assert.Equal(t, http.StatusNoContent, resp.Code)
} }

View File

@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "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 func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(resp, req) base := NewBaseContext(resp, req)
defer baseCleanUp()
ctx := NewWebContext(base, rnd, session.GetContextSession(req)) ctx := NewWebContext(base, rnd, session.GetContextSession(req))
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) 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["CurrentURL"] = setting.AppSubURL + req.URL.RequestURI()
ctx.Data["Link"] = ctx.Link ctx.Data["Link"] = ctx.Link
@ -168,9 +162,7 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{} ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData ctx.Data["PageData"] = ctx.PageData
ctx.Base.AppendContextValue(WebContextKey, ctx) ctx.Base.SetContextValue(WebContextKey, ctx)
ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
ctx.Csrf = NewCSRFProtector(csrfOpts) ctx.Csrf = NewCSRFProtector(csrfOpts)
// Get the last flash message from cookie // Get the last flash message from cookie

View File

@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) {
} }
// RenderToHTML renders the template content to a HTML string // 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 var buf strings.Builder
err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext) err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext)
return template.HTML(buf.String()), err return template.HTML(buf.String()), err

View File

@ -26,6 +26,7 @@ func TestRemoveSessionCookieHeader(t *testing.T) {
} }
func TestRedirectToCurrentSite(t *testing.T) { func TestRedirectToCurrentSite(t *testing.T) {
setting.IsInTesting = true
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")() defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
cases := []struct { cases := []struct {
@ -40,8 +41,7 @@ func TestRedirectToCurrentSite(t *testing.T) {
t.Run(c.location, func(t *testing.T) { t.Run(c.location, func(t *testing.T) {
req := &http.Request{URL: &url.URL{Path: "/"}} req := &http.Request{URL: &url.URL{Path: "/"}}
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
base, baseCleanUp := NewBaseContext(resp, req) base := NewBaseContextForTest(resp, req)
defer baseCleanUp()
ctx := NewWebContext(base, nil, nil) ctx := NewWebContext(base, nil, nil)
ctx.RedirectToCurrentSite(c.location) ctx.RedirectToCurrentSite(c.location)
redirect := test.RedirectURL(resp) redirect := test.RedirectURL(resp)

View File

@ -153,12 +153,10 @@ func PackageContexter() func(next http.Handler) http.Handler {
renderer := templates.HTMLRenderer() renderer := templates.HTMLRenderer()
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(resp, req) base := NewBaseContext(resp, req)
defer baseCleanUp()
// it is still needed when rendering 500 page in a package handler // it is still needed when rendering 500 page in a package handler
ctx := NewWebContext(base, renderer, nil) ctx := NewWebContext(base, renderer, nil)
ctx.Base.AppendContextValue(WebContextKey, ctx) ctx.SetContextValue(WebContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req) next.ServeHTTP(ctx.Resp, ctx.Req)
}) })
} }

View File

@ -64,11 +64,9 @@ func GetPrivateContext(req *http.Request) *PrivateContext {
func PrivateContexter() func(http.Handler) http.Handler { func PrivateContexter() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(w, req) base := NewBaseContext(w, req)
ctx := &PrivateContext{Base: base} ctx := &PrivateContext{Base: base}
defer baseCleanUp() ctx.SetContextValue(privateContextKey, ctx)
ctx.Base.AppendContextValue(privateContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req) 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. // 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 // 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 // the underlying request has timed out from the ssh/http push
func OverrideContext(ctx *PrivateContext) (cancel context.CancelFunc) { func OverrideContext() func(http.Handler) http.Handler {
// 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 return func(next http.Handler) http.Handler {
ctx.Override, _, cancel = process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), fmt.Sprintf("PrivateContext: %s", ctx.Req.RequestURI), process.RequestProcessType, true) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
return cancel // 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)
})
}
} }

View File

@ -397,11 +397,13 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
} }
// RepoAssignment returns a middleware to handle repository assignment // RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) context.CancelFunc { func RepoAssignment(ctx *Context) {
if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce {
// FIXME: it should panic in dev/test modes to have a clear behavior // FIXME: it should panic in dev/test modes to have a clear behavior
log.Trace("RepoAssignment was exec already, skipping second call ...") if !setting.IsProd || setting.IsInTesting {
return nil panic("RepoAssignment should not be executed twice")
}
return
} }
ctx.Data["repoAssignmentExecuted"] = true ctx.Data["repoAssignmentExecuted"] = true
@ -429,7 +431,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
// https://github.com/golang/go/issues/19760 // https://github.com/golang/go/issues/19760
if ctx.FormString("go-get") == "1" { if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx) EarlyResponseForGoGetMeta(ctx)
return nil return
} }
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
@ -442,7 +444,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
} else { } else {
ctx.ServerError("GetUserByName", err) ctx.ServerError("GetUserByName", err)
} }
return nil return
} }
} }
ctx.Repo.Owner = owner ctx.Repo.Owner = owner
@ -467,7 +469,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
redirectPath += "?" + ctx.Req.URL.RawQuery redirectPath += "?" + ctx.Req.URL.RawQuery
} }
ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) ctx.Redirect(path.Join(setting.AppSubURL, redirectPath))
return nil return
} }
// Get repository. // Get repository.
@ -480,7 +482,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
} else if repo_model.IsErrRedirectNotExist(err) { } else if repo_model.IsErrRedirectNotExist(err) {
if ctx.FormString("go-get") == "1" { if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx) EarlyResponseForGoGetMeta(ctx)
return nil return
} }
ctx.NotFound("GetRepositoryByName", nil) ctx.NotFound("GetRepositoryByName", nil)
} else { } else {
@ -489,13 +491,13 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
} else { } else {
ctx.ServerError("GetRepositoryByName", err) ctx.ServerError("GetRepositoryByName", err)
} }
return nil return
} }
repo.Owner = owner repo.Owner = owner
repoAssignment(ctx, repo) repoAssignment(ctx, repo)
if ctx.Written() { if ctx.Written() {
return nil return
} }
ctx.Repo.RepoLink = repo.Link() ctx.Repo.RepoLink = repo.Link()
@ -520,7 +522,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
}) })
if err != nil { if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err) ctx.ServerError("GetReleaseCountByRepoID", err)
return nil return
} }
ctx.Data["NumReleases"], err = db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ 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. // 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 { if err != nil {
ctx.ServerError("GetReleaseCountByRepoID", err) ctx.ServerError("GetReleaseCountByRepoID", err)
return nil return
} }
ctx.Data["Title"] = owner.Name + "/" + repo.Name 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) canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil { if err != nil {
ctx.ServerError("CanUserForkRepo", err) ctx.ServerError("CanUserForkRepo", err)
return nil return
} }
ctx.Data["CanSignedUserFork"] = canSignedUserFork ctx.Data["CanSignedUserFork"] = canSignedUserFork
userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository) userAndOrgForks, err := repo_model.GetForksByUserAndOrgs(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil { if err != nil {
ctx.ServerError("GetForksByUserAndOrgs", err) ctx.ServerError("GetForksByUserAndOrgs", err)
return nil return
} }
ctx.Data["UserAndOrgForks"] = userAndOrgForks ctx.Data["UserAndOrgForks"] = userAndOrgForks
@ -587,14 +589,14 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
if repo.IsFork { if repo.IsFork {
RetrieveBaseRepo(ctx, repo) RetrieveBaseRepo(ctx, repo)
if ctx.Written() { if ctx.Written() {
return nil return
} }
} }
if repo.IsGenerated() { if repo.IsGenerated() {
RetrieveTemplateRepo(ctx, repo) RetrieveTemplateRepo(ctx, repo)
if ctx.Written() { if ctx.Written() {
return nil return
} }
} }
@ -609,10 +611,18 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
if !isHomeOrSettings { if !isHomeOrSettings {
ctx.Redirect(ctx.Repo.RepoLink) 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 err != nil {
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") { 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) 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 { if !isHomeOrSettings {
ctx.Redirect(ctx.Repo.RepoLink) ctx.Redirect(ctx.Repo.RepoLink)
} }
return nil return
} }
ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err) ctx.ServerError("RepoAssignment Invalid repo "+repo.FullName(), err)
return nil return
}
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()
}
} }
// Stop at this point when the repo is empty. // Stop at this point when the repo is empty.
if ctx.Repo.Repository.IsEmpty { if ctx.Repo.Repository.IsEmpty {
ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch
return cancel return
} }
branchOpts := git_model.FindBranchOptions{ branchOpts := git_model.FindBranchOptions{
@ -654,7 +652,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts) branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
if err != nil { if err != nil {
ctx.ServerError("CountBranches", err) 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 // 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) branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
if err != nil { if err != nil {
ctx.ServerError("SyncRepoBranches", err) 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 no branch is set in the request URL, try to guess a default one.
if len(ctx.Repo.BranchName) == 0 { 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 ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
} else { } else {
ctx.Repo.BranchName, _ = gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository) 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) repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil { if err != nil {
ctx.ServerError("GetPendingRepositoryTransfer", err) ctx.ServerError("GetPendingRepositoryTransfer", err)
return cancel return
} }
if err := repoTransfer.LoadAttributes(ctx); err != nil { if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err) ctx.ServerError("LoadRecipient", err)
return cancel return
} }
ctx.Data["RepoTransfer"] = repoTransfer ctx.Data["RepoTransfer"] = repoTransfer
@ -731,7 +729,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}" ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}" ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
} }
return cancel
} }
// RepoRefType type of repo reference // RepoRefType type of repo reference
@ -750,7 +747,7 @@ const headRefName = "HEAD"
// RepoRef handles repository reference names when the ref name is not // RepoRef handles repository reference names when the ref name is not
// explicitly given // explicitly given
func RepoRef() func(*Context) context.CancelFunc { func RepoRef() func(*Context) {
// since no ref name is explicitly specified, ok to just use branch // since no ref name is explicitly specified, ok to just use branch
return RepoRefByType(RepoRefBranch) return RepoRefByType(RepoRefBranch)
} }
@ -865,9 +862,9 @@ type RepoRefByTypeOptions struct {
// RepoRefByType handles repository reference name for a specific type // RepoRefByType handles repository reference name for a specific type
// of repository reference // 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) opt := util.OptionalArg(opts)
return func(ctx *Context) (cancel context.CancelFunc) { return func(ctx *Context) {
refType := detectRefType refType := detectRefType
// Empty repository does not have reference information. // Empty repository does not have reference information.
if ctx.Repo.Repository.IsEmpty { if ctx.Repo.Repository.IsEmpty {
@ -875,7 +872,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.Repo.IsViewBranch = true ctx.Repo.IsViewBranch = true
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
ctx.Data["TreePath"] = "" ctx.Data["TreePath"] = ""
return nil return
} }
var ( var (
@ -884,17 +881,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
) )
if ctx.Repo.GitRepo == nil { 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 { if err != nil {
ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err) ctx.ServerError(fmt.Sprintf("Open Repository %v failed", ctx.Repo.Repository.FullName()), err)
return nil return
}
// 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()
}
} }
} }
@ -924,7 +914,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.Repo.Repository.MarkAsBrokenEmpty() ctx.Repo.Repository.MarkAsBrokenEmpty()
} else { } else {
ctx.ServerError("GetBranchCommit", err) ctx.ServerError("GetBranchCommit", err)
return cancel return
} }
ctx.Repo.IsViewBranch = true ctx.Repo.IsViewBranch = true
} else { } else {
@ -941,7 +931,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.Flash.Info(ctx.Tr("repo.branch.renamed", refName, renamedBranchName)) 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) link := setting.AppSubURL + strings.Replace(ctx.Req.URL.EscapedPath(), util.PathEscapeSegments(refName), util.PathEscapeSegments(renamedBranchName), 1)
ctx.Redirect(link) ctx.Redirect(link)
return cancel return
} }
if refType == RepoRefBranch && ctx.Repo.GitRepo.IsBranchExist(refName) { 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) ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName)
if err != nil { if err != nil {
ctx.ServerError("GetBranchCommit", err) ctx.ServerError("GetBranchCommit", err)
return cancel return
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) { } else if refType == RepoRefTag && ctx.Repo.GitRepo.IsTagExist(refName) {
@ -962,10 +952,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
if err != nil { if err != nil {
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {
ctx.NotFound("GetTagCommit", err) ctx.NotFound("GetTagCommit", err)
return cancel return
} }
ctx.ServerError("GetTagCommit", err) ctx.ServerError("GetTagCommit", err)
return cancel return
} }
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
} else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { } 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) ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
if err != nil { if err != nil {
ctx.NotFound("GetCommit", err) ctx.NotFound("GetCommit", err)
return cancel return
} }
// If short commit ID add canonical link header // If short commit ID add canonical link header
if len(refName) < ctx.Repo.GetObjectFormat().FullLength() { if len(refName) < ctx.Repo.GetObjectFormat().FullLength() {
@ -984,10 +974,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
} }
} else { } else {
if opt.IgnoreNotExistErr { if opt.IgnoreNotExistErr {
return cancel return
} }
ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName)) ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName))
return cancel return
} }
if guessLegacyPath { if guessLegacyPath {
@ -999,7 +989,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.Repo.BranchNameSubURL(), ctx.Repo.BranchNameSubURL(),
util.PathEscapeSegments(ctx.Repo.TreePath)) util.PathEscapeSegments(ctx.Repo.TreePath))
ctx.Redirect(redirect) ctx.Redirect(redirect)
return cancel return
} }
} }
@ -1017,12 +1007,10 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
if err != nil { if err != nil {
ctx.ServerError("GetCommitsCount", err) ctx.ServerError("GetCommitsCount", err)
return cancel return
} }
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache())
return cancel
} }
} }

View File

@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/session" "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
@ -40,7 +41,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
requestURL, err := url.Parse(path) requestURL, err := url.Parse(path)
assert.NoError(t, err) assert.NoError(t, err)
req := &http.Request{Method: method, Host: requestURL.Host, URL: requestURL, Form: maps.Clone(requestURL.Query()), Header: http.Header{}} 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 return req
} }
@ -60,17 +61,16 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
} }
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req := mockRequest(t, reqPath) req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
ctx := context.NewWebContext(base, opt.Render, nil) ctx := context.NewWebContext(base, opt.Render, nil)
ctx.AppendContextValue(context.WebContextKey, ctx) ctx.SetContextValue(context.WebContextKey, ctx)
ctx.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
if opt.SessionStore != nil { if opt.SessionStore != nil {
ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore) ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore)
ctx.Session = opt.SessionStore ctx.Session = opt.SessionStore
} }
ctx.Cache = cache.GetCache() 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) { func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req := mockRequest(t, reqPath) req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := &context.APIContext{Base: base} 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() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp return ctx, resp
} }
func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) { func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req := mockRequest(t, reqPath) req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base := context.NewBaseContext(resp, req)
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := &context.PrivateContext{Base: base} 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() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp return ctx, resp
} }

View File

@ -11,6 +11,6 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{ 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"},
}) })
} }

View File

@ -11,9 +11,10 @@ import (
gitea_context "code.gitea.io/gitea/services/context" gitea_context "code.gitea.io/gitea/services/context"
) )
func ProcessorHelper() *markup.RenderHelperFuncs { func FormalRenderHelperFuncs() *markup.RenderHelperFuncs {
return &markup.RenderHelperFuncs{ return &markup.RenderHelperFuncs{
RenderRepoFileCodePreview: renderRepoFileCodePreview, RenderRepoFileCodePreview: renderRepoFileCodePreview,
RenderRepoIssueIconTitle: renderRepoIssueIconTitle,
IsUsernameMentionable: func(ctx context.Context, username string) bool { IsUsernameMentionable: func(ctx context.Context, username string) bool {
mentionedUser, err := user.GetUserByName(ctx, username) mentionedUser, err := user.GetUserByName(ctx, username)
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
gitea_context "code.gitea.io/gitea/services/context" gitea_context "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/repository/files" "code.gitea.io/gitea/services/repository/files"
) )
@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
return "", err return "", err
} }
if !perms.CanRead(unit.TypeCode) { if !perms.CanRead(unit.TypeCode) {
return "", fmt.Errorf("no permission") return "", util.ErrPermissionDenied
} }
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo) gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)

View File

@ -9,12 +9,13 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestProcessorHelperCodePreview(t *testing.T) { func TestRenderHelperCodePreview(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()}) ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) {
LineStart: 1, LineStart: 1,
LineStop: 10, LineStop: 10,
}) })
assert.ErrorContains(t, err, "no permission") assert.ErrorIs(t, err, util.ErrPermissionDenied)
} }

View File

@ -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(`<a href="%s">%s %s %s</a>`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil
}

View File

@ -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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (#1)</a>`, 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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (user2/repo1#1)</a>`, 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)
}

View File

@ -18,7 +18,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestProcessorHelper(t *testing.T) { func TestRenderHelperMention(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
userPublic := "user1" userPublic := "user1"
@ -32,23 +32,22 @@ func TestProcessorHelper(t *testing.T) {
unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0) unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0)
// when using general context, use user's visibility to check // when using general context, use user's visibility to check
assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic)) assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic))
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited)) assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited))
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate)) assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate))
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch)) assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch))
// when using web context, use user.IsUserVisibleToViewer to check // when using web context, use user.IsUserVisibleToViewer to check
req, err := http.NewRequest("GET", "/", nil) req, err := http.NewRequest("GET", "/", nil)
assert.NoError(t, err) assert.NoError(t, err)
base, baseCleanUp := gitea_context.NewBaseContext(httptest.NewRecorder(), req) base := gitea_context.NewBaseContextForTest(httptest.NewRecorder(), req)
defer baseCleanUp()
giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil) giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil)
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate) giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic)) assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate)) assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
} }

View File

@ -10,7 +10,6 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "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 // 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 // poster email might be private or commits might have a different signature
// than the primary email address of the poster. // 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 { if err != nil {
log.Error("%-v Unable to open base repository: %v", ctx.pr, err) log.Error("%-v Unable to open base repository: %v", ctx.pr, err)
return nil, err return nil, err
} }
defer closer.Close() defer gitRepo.Close()
commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD") commits, err := gitRepo.CommitsBetweenIDs(trackingBranch, "HEAD")
if err != nil { if err != nil {

View File

@ -1,4 +1,4 @@
{{if eq .PackageDescriptor.Package.Type "arch"}} {{if eq .PackageDescriptor.Package.Type "arch"}}
{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}} {{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}} {{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
{{end}} {{end}}

View File

@ -693,7 +693,7 @@
{{else if eq .Type 38}} {{else if eq .Type 38}}
<div class="timeline-item event" id="{{.HashTag}}"> <div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-clock"}}</span> <span class="badge">{{svg "octicon-clock"}}</span>
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} {{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links"> <span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}} {{template "shared/user/authorlink" .Poster}}
{{$timeStr := .Content|TimeEstimateString}} {{$timeStr := .Content|TimeEstimateString}}

View File

@ -3,7 +3,7 @@
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}}) {{ctx.Locale.Tr "actions.runners.runner_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right"> <div class="ui right">
<div class="ui top right pointing dropdown"> <div class="ui top right pointing dropdown jump">
<button class="ui primary tiny button"> <button class="ui primary tiny button">
{{ctx.Locale.Tr "actions.runners.new"}} {{ctx.Locale.Tr "actions.runners.new"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
@ -17,14 +17,18 @@
Registration Token Registration Token
</div> </div>
<div class="ui input"> <div class="ui input">
<input type="text" value="{{.RegistrationToken}}"> <input type="text" value="{{.RegistrationToken}}" readonly>
<button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}"> <button class="ui basic label button" aria-label="{{ctx.Locale.Tr "copy"}}" data-clipboard-text="{{.RegistrationToken}}">
{{svg "octicon-copy" 14}} {{svg "octicon-copy" 14}}
</button> </button>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="item"> <div class="item">
<a href="{{$.Link}}/reset_registration_token">{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}</a> <a class="link-action" data-url="{{$.Link}}/reset_registration_token"
data-modal-confirm="{{ctx.Locale.Tr "actions.runners.reset_registration_token_confirm"}}"
>
{{ctx.Locale.Tr "actions.runners.reset_registration_token"}}
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,25 +1,25 @@
{{if .IsPull}} {{- if .IsPull -}}
{{if not .PullRequest}} {{- if not .PullRequest -}}
No PullRequest No PullRequest
{{else}} {{- else -}}
{{if .IsClosed}} {{- if .IsClosed -}}
{{if .PullRequest.HasMerged}} {{- if .PullRequest.HasMerged -}}
{{svg "octicon-git-merge" 16 "text purple"}} {{- svg "octicon-git-merge" 16 "text purple" -}}
{{else}} {{- else -}}
{{svg "octicon-git-pull-request" 16 "text red"}} {{- svg "octicon-git-pull-request" 16 "text red" -}}
{{end}} {{- end -}}
{{else}} {{- else -}}
{{if .PullRequest.IsWorkInProgress ctx}} {{- if .PullRequest.IsWorkInProgress ctx -}}
{{svg "octicon-git-pull-request-draft" 16 "text grey"}} {{- svg "octicon-git-pull-request-draft" 16 "text grey" -}}
{{else}} {{- else -}}
{{svg "octicon-git-pull-request" 16 "text green"}} {{- svg "octicon-git-pull-request" 16 "text green" -}}
{{end}} {{- end -}}
{{end}} {{- end -}}
{{end}} {{- end -}}
{{else}} {{- else -}}
{{if .IsClosed}} {{- if .IsClosed -}}
{{svg "octicon-issue-closed" 16 "text red"}} {{- svg "octicon-issue-closed" 16 "text red" -}}
{{else}} {{- else -}}
{{svg "octicon-issue-opened" 16 "text green"}} {{- svg "octicon-issue-opened" 16 "text green" -}}
{{end}} {{- end -}}
{{end}} {{- end -}}

View File

@ -58,7 +58,7 @@ func InitTest(requireGitea bool) {
_ = os.Setenv("GITEA_CONF", giteaConf) _ = os.Setenv("GITEA_CONF", giteaConf)
fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf)
if !setting.EnableSQLite3 { 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) { if !filepath.IsAbs(giteaConf) {