From 44909f6e2c8f94b3153cb5114078e4eebe65a4a8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 26 Nov 2024 03:04:55 +0800 Subject: [PATCH 1/9] Fix markup render regression and fix some tests (#32640) Fix #32639, https://github.com/go-gitea/gitea/issues/32608#issuecomment-2497918210 By the way, fix some incorrect SQLs (use single quote but not double quote) --- models/activities/action.go | 4 +-- models/activities/action_test.go | 2 +- models/issues/label_test.go | 2 +- modules/indexer/code/indexer_test.go | 2 -- routers/api/v1/misc/markup_test.go | 48 ++++++++++++++++++---------- routers/common/markup.go | 2 ++ 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/models/activities/action.go b/models/activities/action.go index e74deef1df4cf..546d4340aedca 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -770,7 +770,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) // CountActionCreatedUnixString count actions where created_unix is an empty string func CountActionCreatedUnixString(ctx context.Context) (int64, error) { if setting.Database.Type.IsSQLite3() { - return db.GetEngine(ctx).Where(`created_unix = ""`).Count(new(Action)) + return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action)) } return 0, nil } @@ -778,7 +778,7 @@ func CountActionCreatedUnixString(ctx context.Context) (int64, error) { // FixActionCreatedUnixString set created_unix to zero if it is an empty string func FixActionCreatedUnixString(ctx context.Context) (int64, error) { if setting.Database.Type.IsSQLite3() { - res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ""`) + res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`) if err != nil { return 0, err } diff --git a/models/activities/action_test.go b/models/activities/action_test.go index e5dee33ae0224..64330ebbb3e9a 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -256,7 +256,7 @@ func TestConsistencyUpdateAction(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ ID: int64(id), }) - _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = "" WHERE id = ?`, id) + _, err := db.GetEngine(db.DefaultContext).Exec(`UPDATE action SET created_unix = '' WHERE id = ?`, id) assert.NoError(t, err) actions := make([]*activities_model.Action, 0, 1) // diff --git a/models/issues/label_test.go b/models/issues/label_test.go index a0cc8e6d75640..c2ff084c23632 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -406,7 +406,7 @@ func TestDeleteIssueLabel(t *testing.T) { PosterID: doerID, IssueID: issueID, LabelID: labelID, - }, `content=""`) + }, `content=''`) label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}) assert.EqualValues(t, expectedNumIssues, label.NumIssues) assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues) diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 78fbe7f79247b..d04088531ac28 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -22,8 +22,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - _ "github.com/mattn/go-sqlite3" ) type codeSearchResult struct { diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index 921e7b2750e77..6063e54cdcb26 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -7,15 +7,19 @@ import ( go_context "context" "io" "net/http" + "os" "path" "strings" "testing" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" + context_service "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" @@ -23,10 +27,17 @@ import ( const AppURL = "http://localhost:3000/" +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + FixtureFiles: []string{"repository.yml", "user.yml"}, + }) + os.Exit(m.Run()) +} + func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) { setting.AppURL = AppURL defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - context := "/gogits/gogs" + context := "/user2/repo1" if !wiki { context += path.Join("/src/branch/main", path.Dir(filePath)) } @@ -38,6 +49,8 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe FilePath: filePath, } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup") + ctx.Repo = &context_service.Repository{} + ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) web.SetForm(ctx, &options) Markup(ctx) assert.Equal(t, expectedBody, resp.Body.String()) @@ -48,7 +61,7 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) { defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() setting.AppURL = AppURL - context := "/gogits/gogs" + context := "/user2/repo1" if !wiki { context += "/src/branch/main" } @@ -67,6 +80,7 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody } func TestAPI_RenderGFM(t *testing.T) { + unittest.PrepareTestEnv(t) markup.Init(&markup.RenderHelperFuncs{ IsUsernameMentionable: func(ctx go_context.Context, username string) bool { return username == "r-lyeh" @@ -82,20 +96,20 @@ func TestAPI_RenderGFM(t *testing.T) { // rendered `

Wiki! Enjoy :)

`, // Guard wiki sidebar: special syntax `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, // rendered - `

Guardfile-DSL / Configuring-Guard

+ `

Guardfile-DSL / Configuring-Guard

`, // special syntax `[[Name|Link]]`, // rendered - `

Name

+ `

Name

`, // empty ``, @@ -119,8 +133,8 @@ Here are some links to the most important topics. You can find the full list of

Wine Staging on website wine-staging.com.

Here are some links to the most important topics. You can find the full list of pages at the sidebar.

-

Configuration -images/icon-bug.png

+

Configuration +images/icon-bug.png

`, } @@ -143,20 +157,20 @@ Here are some links to the most important topics. You can find the full list of } input := "[Link](test.md)\n![Image](image.png)" - testRenderMarkdown(t, "gfm", false, input, `

Link -Image

+ testRenderMarkdown(t, "gfm", false, input, `

Link +Image

`, http.StatusOK) - testRenderMarkdown(t, "gfm", false, input, `

Link -Image

+ testRenderMarkdown(t, "gfm", false, input, `

Link +Image

`, http.StatusOK) - testRenderMarkup(t, "gfm", false, "", input, `

Link -Image

+ testRenderMarkup(t, "gfm", false, "", input, `

Link +Image

`, http.StatusOK) - testRenderMarkup(t, "file", false, "path/new-file.md", input, `

Link -Image

+ testRenderMarkup(t, "file", false, "path/new-file.md", input, `

Link +Image

`, http.StatusOK) testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) @@ -186,7 +200,7 @@ func TestAPI_RenderSimple(t *testing.T) { options := api.MarkdownOption{ Mode: "markdown", Text: "", - Context: "/gogits/gogs", + Context: "/user2/repo1", } ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown") for i := 0; i < len(simpleCases); i += 2 { diff --git a/routers/common/markup.go b/routers/common/markup.go index e3e6d9cfcf86b..533b546a2a106 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -77,8 +77,10 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur rctx = rctx.WithMarkupType(markdown.MarkupName) case "comment": rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) + rctx = rctx.WithMarkupType(markdown.MarkupName) case "wiki": rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) + rctx = rctx.WithMarkupType(markdown.MarkupName) case "file": rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{ DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName, From 703be6bf307ed19ce8dc8cd311d24aeb6e5b9861 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 25 Nov 2024 11:35:49 -0800 Subject: [PATCH 2/9] Add github compatible tarball download API endpoints (#32572) Fix #29654 Fix #32481 --- routers/api/v1/api.go | 2 + routers/api/v1/repo/download.go | 53 +++++++++++++++++++ routers/api/v1/repo/file.go | 15 ++++-- routers/web/repo/repo.go | 14 ++++- services/repository/archiver/archiver.go | 34 +++++++----- services/repository/archiver/archiver_test.go | 25 ++++----- tests/integration/api_repo_archive_test.go | 40 ++++++++++++++ 7 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 routers/api/v1/repo/download.go diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 23f466873bad9..0079e8dc87449 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1377,6 +1377,8 @@ func Routes() *web.Router { m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar) m.Delete("", repo.DeleteAvatar) }, reqAdmin(), reqToken()) + + m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go new file mode 100644 index 0000000000000..3620c1465fe9c --- /dev/null +++ b/routers/api/v1/repo/download.go @@ -0,0 +1,53 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/services/context" + archiver_service "code.gitea.io/gitea/services/repository/archiver" +) + +func DownloadArchive(ctx *context.APIContext) { + var tp git.ArchiveType + switch ballType := ctx.PathParam("ball_type"); ballType { + case "tarball": + tp = git.TARGZ + case "zipball": + tp = git.ZIP + case "bundle": + tp = git.BUNDLE + default: + ctx.Error(http.StatusBadRequest, "", fmt.Sprintf("Unknown archive type: %s", ballType)) + return + } + + if ctx.Repo.GitRepo == nil { + gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + if err != nil { + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + return + } + ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() + } + + r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) + if err != nil { + ctx.ServerError("NewRequest", err) + return + } + + archive, err := r.Await(ctx) + if err != nil { + ctx.ServerError("archive.Await", err) + return + } + + download(ctx, r.GetArchiveName(), archive) +} diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 05650cc9bed23..959a4b952a18b 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -301,7 +301,13 @@ func GetArchive(ctx *context.APIContext) { func archiveDownload(ctx *context.APIContext) { uri := ctx.PathParam("*") - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + ext, tp, err := archiver_service.ParseFileName(uri) + if err != nil { + ctx.Error(http.StatusBadRequest, "ParseFileName", err) + return + } + + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) if err != nil { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { ctx.Error(http.StatusBadRequest, "unknown archive format", err) @@ -327,9 +333,12 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. // Add nix format link header so tarballs lock correctly: // https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md - ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`, + ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.%s?rev=%s>; rel="immutable"`, ctx.Repo.Repository.APIURL(), - archiver.CommitID, archiver.CommitID)) + archiver.CommitID, + archiver.Type.String(), + archiver.CommitID, + )) rPath := archiver.RelativePath() if setting.RepoArchive.Storage.ServeDirect() { diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index b62fd21585184..f5e59b0357b02 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -464,7 +464,12 @@ func RedirectDownload(ctx *context.Context) { // Download an archive of a repository func Download(ctx *context.Context) { uri := ctx.PathParam("*") - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + ext, tp, err := archiver_service.ParseFileName(uri) + if err != nil { + ctx.ServerError("ParseFileName", err) + return + } + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) if err != nil { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { ctx.Error(http.StatusBadRequest, err.Error()) @@ -523,7 +528,12 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep // kind of drop it on the floor if this is the case. func InitiateDownload(ctx *context.Context) { uri := ctx.PathParam("*") - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri) + ext, tp, err := archiver_service.ParseFileName(uri) + if err != nil { + ctx.ServerError("ParseFileName", err) + return + } + aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) if err != nil { ctx.ServerError("archiver_service.NewRequest", err) return diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index c33369d047e8e..e1addbed335cb 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -67,30 +67,36 @@ func (e RepoRefNotFoundError) Is(err error) bool { return ok } -// NewRequest creates an archival request, based on the URI. The -// resulting ArchiveRequest is suitable for being passed to Await() -// if it's determined that the request still needs to be satisfied. -func NewRequest(repoID int64, repo *git.Repository, uri string) (*ArchiveRequest, error) { - r := &ArchiveRequest{ - RepoID: repoID, - } - - var ext string +func ParseFileName(uri string) (ext string, tp git.ArchiveType, err error) { switch { case strings.HasSuffix(uri, ".zip"): ext = ".zip" - r.Type = git.ZIP + tp = git.ZIP case strings.HasSuffix(uri, ".tar.gz"): ext = ".tar.gz" - r.Type = git.TARGZ + tp = git.TARGZ case strings.HasSuffix(uri, ".bundle"): ext = ".bundle" - r.Type = git.BUNDLE + tp = git.BUNDLE default: - return nil, ErrUnknownArchiveFormat{RequestFormat: uri} + return "", 0, ErrUnknownArchiveFormat{RequestFormat: uri} + } + return ext, tp, nil +} + +// NewRequest creates an archival request, based on the URI. The +// resulting ArchiveRequest is suitable for being passed to Await() +// if it's determined that the request still needs to be satisfied. +func NewRequest(repoID int64, repo *git.Repository, refName string, fileType git.ArchiveType) (*ArchiveRequest, error) { + if fileType < git.ZIP || fileType > git.BUNDLE { + return nil, ErrUnknownArchiveFormat{RequestFormat: fileType.String()} } - r.refName = strings.TrimSuffix(uri, ext) + r := &ArchiveRequest{ + RepoID: repoID, + refName: refName, + Type: fileType, + } // Get corresponding commit. commitID, err := repo.ConvertToGitID(r.refName) diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index b3f3ed7bf3e68..2ab18edf4910e 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/contexttest" _ "code.gitea.io/gitea/models/actions" @@ -31,47 +32,47 @@ func TestArchive_Basic(t *testing.T) { contexttest.LoadGitRepo(t, ctx) defer ctx.Repo.GitRepo.Close() - bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + bogusReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName()) // Check a series of bogus requests. // Step 1, valid commit with a bad extension. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".dilbert") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, 100) assert.Error(t, err) assert.Nil(t, bogusReq) // Step 2, missing commit. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "dbffff", git.ZIP) assert.Error(t, err) assert.Nil(t, bogusReq) // Step 3, doesn't look like branch/tag/commit. - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "db", git.ZIP) assert.Error(t, err) assert.Nil(t, bogusReq) - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master", git.ZIP) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName()) - bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive.zip") + bogusReq, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive", git.ZIP) assert.NoError(t, err) assert.NotNil(t, bogusReq) assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName()) // Now two valid requests, firstCommit with valid extensions. - zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + zipReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) assert.NoError(t, err) assert.NotNil(t, zipReq) - tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".tar.gz") + tgzReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.TARGZ) assert.NoError(t, err) assert.NotNil(t, tgzReq) - secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".zip") + secondReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.ZIP) assert.NoError(t, err) assert.NotNil(t, secondReq) @@ -91,7 +92,7 @@ func TestArchive_Basic(t *testing.T) { // Sleep two seconds to make sure the queue doesn't change. time.Sleep(2 * time.Second) - zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + zipReq2, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) assert.NoError(t, err) // This zipReq should match what's sitting in the queue, as we haven't // let it release yet. From the consumer's point of view, this looks like @@ -106,12 +107,12 @@ func TestArchive_Basic(t *testing.T) { // Now we'll submit a request and TimedWaitForCompletion twice, before and // after we release it. We should trigger both the timeout and non-timeout // cases. - timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit+".tar.gz") + timedReq, err := NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, secondCommit, git.TARGZ) assert.NoError(t, err) assert.NotNil(t, timedReq) doArchive(db.DefaultContext, timedReq) - zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit+".zip") + zipReq2, err = NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP) assert.NoError(t, err) // Now, we're guaranteed to have released the original zipReq from the queue. // Ensure that we don't get handed back the released entry somehow, but they diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go index eecb84d5d1b89..8589199da39df 100644 --- a/tests/integration/api_repo_archive_test.go +++ b/tests/integration/api_repo_archive_test.go @@ -59,3 +59,43 @@ func TestAPIDownloadArchive(t *testing.T) { link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) } + +func TestAPIDownloadArchive2(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, user2.LowerName) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) + + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/zipball/master", user2.Name, repo.Name)) + resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) + bs, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Len(t, bs, 320) + + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/tarball/master", user2.Name, repo.Name)) + resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) + bs, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Len(t, bs, 266) + + // Must return a link to a commit ID as the "immutable" archive link + linkHeaderRe := regexp.MustCompile(`^<(https?://.*/api/v1/repos/user2/repo1/archive/[a-f0-9]+\.tar\.gz.*)>; rel="immutable"$`) + m := linkHeaderRe.FindStringSubmatch(resp.Header().Get("Link")) + assert.NotEmpty(t, m[1]) + resp = MakeRequest(t, NewRequest(t, "GET", m[1]).AddTokenAuth(token), http.StatusOK) + bs2, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + // The locked URL should give the same bytes as the non-locked one + assert.EqualValues(t, bs, bs2) + + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/bundle/master", user2.Name, repo.Name)) + resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) + bs, err = io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Len(t, bs, 382) + + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) + MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) +} From 25cacaf0aa56bece904c84638fbe126a826c1cd8 Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Tue, 26 Nov 2024 09:24:56 +0800 Subject: [PATCH 3/9] Fixed Issue of Review Menu Shown Behind (#32631) Fixed #31144 --------- Co-authored-by: wxiaoguang --- templates/repo/diff/box.tmpl | 30 ++++++++++------------ tests/integration/pull_compare_test.go | 4 +-- web_src/css/modules/tippy.css | 2 ++ web_src/js/features/repo-diff.ts | 12 +++++++++ web_src/js/features/repo-unicode-escape.ts | 12 ++++----- web_src/js/utils/dom.ts | 4 +-- 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 26737f110e41e..20e0c9db668c9 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -164,24 +164,22 @@ {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}} {{end}} - diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index def6506253f90..ad0be72dcbd51 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -97,7 +97,7 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { user2Session := loginUser(t, "user2") resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - nodes := htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .dropdown .menu a") + nodes := htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .tippy-target a") if assert.Equal(t, 1, nodes.Length()) { // there is only "View File" button, no "Edit File" button assert.Equal(t, "View File", nodes.First().Text()) @@ -121,7 +121,7 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { // user2 (admin of repo3) goes to the PR files page again resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) - nodes = htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .dropdown .menu a") + nodes = htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .tippy-target a") if assert.Equal(t, 2, nodes.Length()) { // there are "View File" button and "Edit File" button assert.Equal(t, "View File", nodes.First().Text()) diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css index 53c3d5aaeac6e..55b9751cc635a 100644 --- a/web_src/css/modules/tippy.css +++ b/web_src/css/modules/tippy.css @@ -77,8 +77,10 @@ align-items: center; padding: 9px 18px; color: inherit; + background: inherit; text-decoration: none; gap: 10px; + width: 100%; } .tippy-box[data-theme="menu"] .item:hover { diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 0d489665a2307..f39de96f5beaf 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -18,6 +18,7 @@ import { } from '../utils/dom.ts'; import {POST, GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {createTippy} from '../modules/tippy.ts'; const {pageData, i18n} = window.config; @@ -140,12 +141,22 @@ export function initRepoDiffConversationNav() { }); } +function initDiffHeaderPopup() { + for (const btn of document.querySelectorAll('.diff-header-popup-btn:not([data-header-popup-initialized])')) { + btn.setAttribute('data-header-popup-initialized', ''); + const popup = btn.nextElementSibling; + if (!popup?.matches('.tippy-target')) throw new Error('Popup element not found'); + createTippy(btn, {content: popup, theme: 'menu', placement: 'bottom', trigger: 'click', interactive: true, hideOnClick: true}); + } +} + // Will be called when the show more (files) button has been pressed function onShowMoreFiles() { initRepoIssueContentHistory(); initViewedCheckboxListenerFor(); countAndUpdateViewedFiles(); initImageDiff(); + initDiffHeaderPopup(); } export async function loadMoreFiles(url) { @@ -221,6 +232,7 @@ export function initRepoDiffView() { initDiffFileList(); initDiffCommitSelect(); initRepoDiffShowMore(); + initDiffHeaderPopup(); initRepoDiffFileViewToggle(); initViewedCheckboxListenerFor(); initExpandAndCollapseFilesButton(); diff --git a/web_src/js/features/repo-unicode-escape.ts b/web_src/js/features/repo-unicode-escape.ts index 7a9bca7a376c5..0c7d2e8592aa0 100644 --- a/web_src/js/features/repo-unicode-escape.ts +++ b/web_src/js/features/repo-unicode-escape.ts @@ -1,13 +1,13 @@ -import {hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.ts'; +import {addDelegatedEventListener, hideElem, queryElemSiblings, showElem, toggleElem} from '../utils/dom.ts'; export function initUnicodeEscapeButton() { - document.addEventListener('click', (e) => { - const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button'); - if (!btn) return; - + addDelegatedEventListener(document, 'click', '.escape-button, .unescape-button, .toggle-escape-button', (btn, e) => { e.preventDefault(); - const fileContent = btn.closest('.file-content, .non-diff-file-content'); + const fileContentElemId = btn.getAttribute('data-file-content-elem-id'); + const fileContent = fileContentElemId ? + document.querySelector(`#${fileContentElemId}`) : + btn.closest('.file-content, .non-diff-file-content'); const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); if (btn.matches('.escape-button')) { for (const el of fileView) el.classList.add('unicode-escaped'); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 4bbb0c414aca0..a4c7c0e4c6df1 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -2,10 +2,10 @@ import {debounce} from 'throttle-debounce'; import type {Promisable} from 'type-fest'; import type $ from 'jquery'; -type ElementArg = Element | string | NodeListOf | Array | ReturnType; +type ArrayLikeIterable = ArrayLike & Iterable; // for NodeListOf and Array +type ElementArg = Element | string | ArrayLikeIterable | ReturnType; type ElementsCallback = (el: T) => Promisable; type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable; -type ArrayLikeIterable = ArrayLike & Iterable; // for NodeListOf and Array function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) { if (typeof el === 'string' || el instanceof String) { From 9ed768adc426636b6fbcdb389ba89ba039bc7da3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 26 Nov 2024 10:03:02 +0800 Subject: [PATCH 4/9] Improve oauth2 scope token handling (#32633) --- routers/api/v1/api.go | 2 +- services/oauth2_provider/access_token.go | 22 ++++++++++++------- .../oauth2_provider/additional_scopes_test.go | 5 ++++- tests/integration/oauth_test.go | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 0079e8dc87449..aee76325a861b 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -321,7 +321,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC } if !allow { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) + ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope)) return } diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go index d94e15d5f2597..77ddce0534e20 100644 --- a/services/oauth2_provider/access_token.go +++ b/services/oauth2_provider/access_token.go @@ -74,26 +74,32 @@ type AccessTokenResponse struct { // GrantAdditionalScopes returns valid scopes coming from grant func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope { // scopes_supported from templates/user/auth/oidc_wellknown.tmpl - scopesSupported := []string{ + generalScopesSupported := []string{ "openid", "profile", "email", "groups", } - var tokenScopes []string - for _, tokenScope := range strings.Split(grantScopes, " ") { - if slices.Index(scopesSupported, tokenScope) == -1 { - tokenScopes = append(tokenScopes, tokenScope) + var accessScopes []string // the scopes for access control, but not for general information + for _, scope := range strings.Split(grantScopes, " ") { + if scope != "" && !slices.Contains(generalScopesSupported, scope) { + accessScopes = append(accessScopes, scope) } } // since version 1.22, access tokens grant full access to the API // with this access is reduced only if additional scopes are provided - accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ",")) - if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 { - return accessTokenWithAdditionalScopes + if len(accessScopes) > 0 { + accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ",")) + if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil { + return normalizedAccessTokenScope + } + // TODO: if there are invalid access scopes (err != nil), + // then it is treated as "all", maybe in the future we should make it stricter to return an error + // at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all" } + // fallback, empty access scope is treated as "all" access return auth.AccessTokenScopeAll } diff --git a/services/oauth2_provider/additional_scopes_test.go b/services/oauth2_provider/additional_scopes_test.go index d239229f4be78..2d4df7aea2b26 100644 --- a/services/oauth2_provider/additional_scopes_test.go +++ b/services/oauth2_provider/additional_scopes_test.go @@ -14,6 +14,7 @@ func TestGrantAdditionalScopes(t *testing.T) { grantScopes string expectedScopes string }{ + {"", "all"}, // for old tokens without scope, treat it as "all" {"openid profile email", "all"}, {"openid profile email groups", "all"}, {"openid profile email all", "all"}, @@ -22,12 +23,14 @@ func TestGrantAdditionalScopes(t *testing.T) { {"read:user read:repository", "read:repository,read:user"}, {"read:user write:issue public-only", "public-only,write:issue,read:user"}, {"openid profile email read:user", "read:user"}, + + // TODO: at the moment invalid tokens are treated as "all" to avoid breaking 1.22 behavior (more details are in GrantAdditionalScopes) {"read:invalid_scope", "all"}, {"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"}, } for _, test := range tests { - t.Run(test.grantScopes, func(t *testing.T) { + t.Run("scope:"+test.grantScopes, func(t *testing.T) { result := GrantAdditionalScopes(test.grantScopes) assert.Equal(t, test.expectedScopes, string(result)) }) diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index feb262b50e2cf..f177bd3a23bac 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -565,7 +565,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) { errorParsed := new(errorResponse) require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed)) - assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:repository]") + assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:repository]") } func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { @@ -708,7 +708,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { errorParsed := new(errorResponse) require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed)) - assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s): [read:user read:organization]") + assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:user read:organization]") } func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) { From 88f5d33ab267f330ffaf02eb019e772ed06ed34f Mon Sep 17 00:00:00 2001 From: william-allspice Date: Tue, 26 Nov 2024 00:37:24 -0600 Subject: [PATCH 5/9] Fix race condition in mermaid observer (#32599) This Pull Request addresses a race condition in the updateIframeHeight function where it is sometimes called when the iframe is not fully loaded or accessible resulting in an alarming error message for the user. To address this we: 1. Add defensive programming within the updateIframeHeight function 2. Delay instantiating the intersection observer until the iframe has loaded Co-authored-by: wxiaoguang --- web_src/js/markup/mermaid.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 004795d367980..2dbed280c2a30 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -58,16 +58,12 @@ export async function renderMermaid(): Promise { mermaidBlock.append(btn); const updateIframeHeight = () => { - iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`; + const body = iframe.contentWindow?.document?.body; + if (body) { + iframe.style.height = `${body.clientHeight}px`; + } }; - // update height when element's visibility state changes, for example when the diagram is inside - // a
+ block and the
block becomes visible upon user interaction, it - // would initially set a incorrect height and the correct height is set during this callback. - (new IntersectionObserver(() => { - updateIframeHeight(); - }, {root: document.documentElement})).observe(iframe); - iframe.addEventListener('load', () => { pre.replaceWith(mermaidBlock); mermaidBlock.classList.remove('tw-hidden'); @@ -76,6 +72,13 @@ export async function renderMermaid(): Promise { mermaidBlock.classList.remove('is-loading'); iframe.classList.remove('tw-invisible'); }, 0); + + // update height when element's visibility state changes, for example when the diagram is inside + // a
+ block and the
block becomes visible upon user interaction, it + // would initially set a incorrect height and the correct height is set during this callback. + (new IntersectionObserver(() => { + updateIframeHeight(); + }, {root: document.documentElement})).observe(iframe); }); document.body.append(mermaidBlock); From 722e703c6bc4df27f23a985b16f68e6c89e048c1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 26 Nov 2024 23:10:45 +0800 Subject: [PATCH 6/9] Bypass vitest bug (#32647) --- web_src/js/utils/dom.test.ts | 8 +++++++- web_src/js/utils/dom.ts | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index cb99a85511b06..6e7159685078c 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -1,4 +1,4 @@ -import {createElementFromAttrs, createElementFromHTML, querySingleVisibleElem} from './dom.ts'; +import {createElementFromAttrs, createElementFromHTML, queryElemChildren, querySingleVisibleElem} from './dom.ts'; test('createElementFromHTML', () => { expect(createElementFromHTML('foobar').outerHTML).toEqual('foobar'); @@ -26,3 +26,9 @@ test('querySingleVisibleElem', () => { el = createElementFromHTML('
foobar
'); expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); }); + +test('queryElemChildren', () => { + const el = createElementFromHTML('
ab
'); + const children = queryElemChildren(el, '.a'); + expect(children.length).toEqual(1); +}); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index a4c7c0e4c6df1..da9ce71644cab 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -76,6 +76,11 @@ export function queryElemSiblings(el: Element, selector = '*' // it works like jQuery.children: only the direct children are selected export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable { + if (window.vitest) { + // bypass the vitest bug: it doesn't support ":scope >" + const selected = Array.from(parent.children as any).filter((child) => child.matches(selector)); + return applyElemsCallback(selected, fn); + } return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn); } From 0f4b0cf8922ef1016fc8230fb0f9df883f03d1e0 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 26 Nov 2024 23:36:55 +0800 Subject: [PATCH 7/9] Refactor some frontend problems (#32646) 1. correct the modal usage on "admin email list" page (then `web_src/js/features/admin/emails.ts` is removed) 2. use `addDelegatedEventListener` instead of `jQuery().on` 3. more jQuery related changes and remove jQuery from `web_src/js/features/common-button.ts` 4. improve `confirmModal` to make it support header, and remove incorrect double-escaping 5. fix more typescript related types 6. fine tune devtest pages and add more tests --- routers/web/admin/emails.go | 2 +- routers/web/devtest/devtest.go | 4 +- templates/admin/emails/list.tmpl | 55 +++--- templates/devtest/devtest-footer.tmpl | 3 + templates/devtest/devtest-header.tmpl | 2 + .../devtest/{list.tmpl => devtest-list.tmpl} | 6 +- templates/devtest/fetch-action.tmpl | 5 +- templates/devtest/flex-list.tmpl | 5 +- templates/devtest/fomantic-dropdown.tmpl | 5 +- templates/devtest/fomantic-modal.tmpl | 28 ++- templates/devtest/gitea-ui.tmpl | 5 +- templates/devtest/global-button.tmpl | 16 ++ templates/devtest/label.tmpl | 5 +- templates/devtest/tmplerr.tmpl | 4 +- templates/devtest/toast.tmpl | 8 +- web_src/js/features/admin/emails.ts | 13 -- web_src/js/features/common-button.ts | 160 +++++++++--------- web_src/js/features/common-fetch-action.ts | 37 ++-- web_src/js/features/comp/ConfirmModal.ts | 4 +- web_src/js/features/repo-issue-list.ts | 2 +- web_src/js/index.ts | 4 - web_src/js/utils/dom.ts | 2 +- 22 files changed, 192 insertions(+), 183 deletions(-) create mode 100644 templates/devtest/devtest-footer.tmpl create mode 100644 templates/devtest/devtest-header.tmpl rename templates/devtest/{list.tmpl => devtest-list.tmpl} (65%) create mode 100644 templates/devtest/global-button.tmpl delete mode 100644 web_src/js/features/admin/emails.ts diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index 49338fbd7c448..e9c97d8b8f914 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -154,7 +154,7 @@ func ActivateEmail(ctx *context.Context) { // DeleteEmail serves a POST request for delete a user's email func DeleteEmail(ctx *context.Context) { - u, err := user_model.GetUserByID(ctx, ctx.FormInt64("Uid")) + u, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid")) if err != nil || u == nil { ctx.ServerError("GetUserByID", err) return diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 8c343197d91b9..0068c9fe88771 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -24,12 +24,12 @@ func List(ctx *context.Context) { var subNames []string for _, tmplName := range templateNames { subName := strings.TrimSuffix(tmplName, ".tmpl") - if subName != "list" { + if !strings.HasPrefix(subName, "devtest-") { subNames = append(subNames, subName) } } ctx.Data["SubNames"] = subNames - ctx.HTML(http.StatusOK, "devtest/list") + ctx.HTML(http.StatusOK, "devtest/devtest-list") } func FetchActionTest(ctx *context.Context) { diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index 835b77ea176aa..0dc1fb9d03fe3 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -50,10 +50,10 @@ {{svg (Iif .IsPrimary "octicon-check" "octicon-x")}} {{if .CanChange}} - + {{svg (Iif .IsActivated "octicon-check" "octicon-x")}} {{else}} @@ -61,9 +61,10 @@ {{end}} - + {{svg "octicon-trash"}} {{end}} @@ -77,40 +78,24 @@
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
-
+

{{ctx.Locale.Tr "admin.emails.change_email_text"}}

- - {{$.CsrfTokenHtml}} + {{$.CsrfTokenHtml}} - - - - + + + + - - - - + + + + -
- {{template "base/modal_actions_confirm" .}} -
-
-
+ {{template "base/modal_actions_confirm" .}} + - - - - {{template "admin/layout_footer" .}} diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl new file mode 100644 index 0000000000000..1c755508a5ec1 --- /dev/null +++ b/templates/devtest/devtest-footer.tmpl @@ -0,0 +1,3 @@ +{{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} + +{{template "base/footer" dict}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl new file mode 100644 index 0000000000000..a5910b96e6f46 --- /dev/null +++ b/templates/devtest/devtest-header.tmpl @@ -0,0 +1,2 @@ +{{template "base/head" dict}} + diff --git a/templates/devtest/list.tmpl b/templates/devtest/devtest-list.tmpl similarity index 65% rename from templates/devtest/list.tmpl rename to templates/devtest/devtest-list.tmpl index 90b1fcc9d0411..71ee6807f0d36 100644 --- a/templates/devtest/list.tmpl +++ b/templates/devtest/devtest-list.tmpl @@ -1,5 +1,4 @@ -{{template "base/head" .}} - +{{template "devtest/devtest-header"}}