Decouple the different contexts from each other (#24786)

Replace #16455

Close #21803

Mixing different Gitea contexts together causes some problems:

1. Unable to respond proper content when error occurs, eg: Web should
respond HTML while API should respond JSON
2. Unclear dependency, eg: it's unclear when Context is used in
APIContext, which fields should be initialized, which methods are
necessary.


To make things clear, this PR introduces a Base context, it only
provides basic Req/Resp/Data features.

This PR mainly moves code. There are still many legacy problems and
TODOs in code, leave unrelated changes to future PRs.
This commit is contained in:
wxiaoguang 2023-05-21 09:50:53 +08:00 committed by GitHub
parent 6ba4f89723
commit 6b33152b7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 885 additions and 781 deletions

View file

@ -149,7 +149,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err := user_model.LookupUserRedirect(userName); err == nil {
context.RedirectToUser(ctx.Context, userName, redirectUserID)
context.RedirectToUser(ctx.Base, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
@ -170,7 +170,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if repo_model.IsErrRepoNotExist(err) {
redirectRepoID, err := repo_model.LookupRedirect(owner.ID, repoName)
if err == nil {
context.RedirectToRepo(ctx.Context, redirectRepoID)
context.RedirectToRepo(ctx.Base, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
ctx.NotFound()
} else {
@ -274,7 +274,7 @@ func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APICo
ctx.Error(http.StatusForbidden, "reqToken", "token does not have required scope: "+requiredScope)
return
}
if ctx.Context.IsBasicAuth {
if ctx.IsBasicAuth {
ctx.CheckForOTP()
return
}
@ -295,7 +295,7 @@ func reqExploreSignIn() func(ctx *context.APIContext) {
func reqBasicAuth() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Context.IsBasicAuth {
if !ctx.IsBasicAuth {
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
return
}
@ -375,7 +375,7 @@ func reqAnyRepoReader() func(ctx *context.APIContext) {
// reqOrgOwnership user should be an organization owner, or a site admin
func reqOrgOwnership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Context.IsUserSiteAdmin() {
if ctx.IsUserSiteAdmin() {
return
}
@ -407,7 +407,7 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Context.IsUserSiteAdmin() {
if ctx.IsUserSiteAdmin() {
return
}
if ctx.Org.Team == nil {
@ -444,7 +444,7 @@ func reqTeamMembership() func(ctx *context.APIContext) {
// reqOrgMembership user should be an organization member, or a site admin
func reqOrgMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.Context.IsUserSiteAdmin() {
if ctx.IsUserSiteAdmin() {
return
}
@ -512,7 +512,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if organization.IsErrOrgNotExist(err) {
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
if err == nil {
context.RedirectToUser(ctx.Context, ctx.Params(":org"), redirectUserID)
context.RedirectToUser(ctx.Base, ctx.Params(":org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetOrgByName", err)
} else {

View file

@ -41,7 +41,7 @@ func Markup(ctx *context.APIContext) {
return
}
common.RenderMarkup(ctx.Context, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
common.RenderMarkup(ctx.Base, ctx.Repo, form.Mode, form.Text, form.Context, form.FilePath, form.Wiki)
}
// Markdown render markdown document to HTML
@ -76,7 +76,7 @@ func Markdown(ctx *context.APIContext) {
mode = form.Mode
}
common.RenderMarkup(ctx.Context, mode, form.Text, form.Context, "", form.Wiki)
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "", form.Wiki)
}
// MarkdownRaw render raw markdown HTML

View file

@ -16,7 +16,6 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
@ -30,26 +29,16 @@ const (
AppSubURL = AppURL + Repo + "/"
)
func createContext(req *http.Request) (*context.Context, *httptest.ResponseRecorder) {
rnd := templates.HTMLRenderer()
func createAPIContext(req *http.Request) (*context.APIContext, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder()
c := &context.Context{
Req: req,
Resp: context.NewResponse(resp),
Render: rnd,
Data: make(middleware.ContextData),
}
defer c.Close()
base, baseCleanUp := context.NewBaseContext(resp, req)
base.Data = middleware.ContextData{}
c := &context.APIContext{Base: base}
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
return c, resp
}
func wrap(ctx *context.Context) *context.APIContext {
return &context.APIContext{
Context: ctx,
}
}
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
setting.AppURL = AppURL
@ -65,8 +54,7 @@ func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, r
Method: "POST",
URL: requrl,
}
m, resp := createContext(req)
ctx := wrap(m)
ctx, resp := createAPIContext(req)
options.Text = text
web.SetForm(ctx, &options)
@ -90,8 +78,7 @@ func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseC
Method: "POST",
URL: requrl,
}
m, resp := createContext(req)
ctx := wrap(m)
ctx, resp := createAPIContext(req)
options.Text = text
web.SetForm(ctx, &options)
@ -211,8 +198,7 @@ func TestAPI_RenderSimple(t *testing.T) {
Method: "POST",
URL: requrl,
}
m, resp := createContext(req)
ctx := wrap(m)
ctx, resp := createAPIContext(req)
for i := 0; i < len(simpleCases); i += 2 {
options.Text = simpleCases[i]
@ -231,8 +217,7 @@ func TestAPI_RenderRaw(t *testing.T) {
Method: "POST",
URL: requrl,
}
m, resp := createContext(req)
ctx := wrap(m)
ctx, resp := createAPIContext(req)
for i := 0; i < len(simpleCases); i += 2 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))

View file

@ -25,7 +25,7 @@ func NewAvailable(ctx *context.APIContext) {
}
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return nil

View file

@ -80,7 +80,7 @@ func GetRawFile(ctx *context.APIContext) {
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
}
}
@ -137,7 +137,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// OK not cached - serve!
if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil {
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
@ -159,7 +159,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
if err := dataRc.Close(); err != nil {
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Context.Repo.Repository, err)
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
}
// Check if the blob represents a pointer
@ -173,7 +173,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// OK not cached - serve!
common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
return
}
@ -187,7 +187,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
return
}
common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
return
} else if err != nil {
ctx.ServerError("GetLFSMetaObjectByOid", err)
@ -215,7 +215,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
defer lfsDataRc.Close()
common.ServeContentByReadSeeker(ctx.Context, ctx.Repo.TreePath, lastModified, lfsDataRc)
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {

View file

@ -9,7 +9,6 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
@ -18,12 +17,12 @@ import (
func TestTestHook(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
ctx := test.MockAPIContext(t, "user2/repo1/wiki/_pages")
ctx.SetParams(":id", "1")
test.LoadRepo(t, ctx, 1)
test.LoadRepoCommit(t, ctx)
test.LoadUser(t, ctx, 2)
TestHook(&context.APIContext{Context: ctx, Org: nil})
TestHook(ctx)
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{

View file

@ -116,7 +116,7 @@ func SearchIssues(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/IssueList"
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -368,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/IssueList"
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return

View file

@ -59,7 +59,7 @@ func ListIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -156,7 +156,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TimelineList"
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -259,7 +259,7 @@ func ListRepoIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
before, since, err := context.GetQueryBeforeSince(ctx.Context)
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return

View file

@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
opts.UserID = user.ID
}
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@ -596,7 +596,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}

View file

@ -79,7 +79,7 @@ func Migrate(ctx *context.APIContext) {
return
}
if ctx.HasError() {
if ctx.HasAPIError() {
ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg())
return
}

View file

@ -9,7 +9,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@ -20,7 +19,7 @@ import (
func TestRepoEdit(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1")
ctx := test.MockAPIContext(t, "user2/repo1")
test.LoadRepo(t, ctx, 1)
test.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
@ -54,9 +53,8 @@ func TestRepoEdit(t *testing.T) {
Archived: &archived,
}
apiCtx := &context.APIContext{Context: ctx, Org: nil}
web.SetForm(apiCtx, &opts)
Edit(apiCtx)
web.SetForm(ctx, &opts)
Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
@ -67,7 +65,7 @@ func TestRepoEdit(t *testing.T) {
func TestRepoEditNameChange(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1")
ctx := test.MockAPIContext(t, "user2/repo1")
test.LoadRepo(t, ctx, 1)
test.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
@ -76,9 +74,8 @@ func TestRepoEditNameChange(t *testing.T) {
Name: &name,
}
apiCtx := &context.APIContext{Context: ctx, Org: nil}
web.SetForm(apiCtx, &opts)
Edit(apiCtx)
web.SetForm(ctx, &opts)
Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{

View file

@ -183,7 +183,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
ctx.Error(http.StatusBadRequest, "ref/sha not given", nil)
return
}
sha = utils.MustConvertToSHA1(ctx.Context, sha)
sha = utils.MustConvertToSHA1(ctx.Base, ctx.Repo, sha)
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)

View file

@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User
if err != nil {
if user_model.IsErrUserNotExist(err) {
if redirectUserID, err2 := user_model.LookupUserRedirect(username); err2 == nil {
context.RedirectToUser(ctx.Context, username, redirectUserID)
context.RedirectToUser(ctx.Base, username, redirectUserID)
} else {
ctx.NotFound("GetUserByName", err)
}

View file

@ -4,6 +4,7 @@
package utils
import (
gocontext "context"
"fmt"
"net/http"
@ -33,7 +34,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string {
}
}
sha = MustConvertToSHA1(ctx.Context, sha)
sha = MustConvertToSHA1(ctx, ctx.Repo, sha)
if ctx.Repo.GitRepo != nil {
err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha)
@ -69,7 +70,7 @@ func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (str
}
// ConvertToSHA1 returns a full-length SHA1 from a potential ID string
func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
func ConvertToSHA1(ctx gocontext.Context, repo *context.Repository, commitID string) (git.SHA1, error) {
if len(commitID) == git.SHAFullLength && git.IsValidSHAPattern(commitID) {
sha1, err := git.NewIDFromString(commitID)
if err == nil {
@ -77,7 +78,7 @@ func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
}
}
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, ctx.Repo.Repository.RepoPath())
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.Repository.RepoPath())
if err != nil {
return git.SHA1{}, fmt.Errorf("RepositoryFromContextOrOpen: %w", err)
}
@ -87,8 +88,8 @@ func ConvertToSHA1(ctx *context.Context, commitID string) (git.SHA1, error) {
}
// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1
func MustConvertToSHA1(ctx *context.Context, commitID string) string {
sha, err := ConvertToSHA1(ctx, commitID)
func MustConvertToSHA1(ctx gocontext.Context, repo *context.Repository, commitID string) string {
sha, err := ConvertToSHA1(ctx, repo, commitID)
if err != nil {
return commitID
}