diff --git a/models/activities/action.go b/models/activities/action.go
index e74deef1df..546d4340ae 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 e5dee33ae0..64330ebbb3 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/comment.go b/models/issues/comment.go
index 48b8e335d4..6650817e50 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -1108,7 +1108,7 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList,
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
}
- if opts.Page != 0 {
+ if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 9ccf2859ea..fd89267b84 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -641,7 +641,7 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
Where("issue_id = ?", issue.ID).
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
- if opts.Page != 0 {
+ if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
}
err = sess.Find(&issueDeps)
diff --git a/models/issues/issue_watch.go b/models/issues/issue_watch.go
index 9e616a0eb1..560be17eb6 100644
--- a/models/issues/issue_watch.go
+++ b/models/issues/issue_watch.go
@@ -105,7 +105,7 @@ func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOpt
And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
watches := make([]*IssueWatch, 0, listOptions.PageSize)
return watches, sess.Find(&watches)
diff --git a/models/issues/label.go b/models/issues/label.go
index 2530f71004..d80578193e 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -390,7 +390,7 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
sess.Asc("name")
}
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}
@@ -462,7 +462,7 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
sess.Asc("name")
}
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
index a0cc8e6d75..c2ff084c23 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/models/issues/reaction.go b/models/issues/reaction.go
index eb7faefc79..11b3c6be20 100644
--- a/models/issues/reaction.go
+++ b/models/issues/reaction.go
@@ -163,7 +163,7 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
Where(opts.toConds()).
In("reaction.`type`", setting.UI.Reactions).
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
- if opts.Page != 0 {
+ if opts.Page > 0 {
sess = db.SetSessionPagination(sess, &opts)
reactions := make([]*Reaction, 0, opts.PageSize)
diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go
index fd9c7d7875..629af95b57 100644
--- a/models/issues/stopwatch.go
+++ b/models/issues/stopwatch.go
@@ -96,7 +96,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
sws := make([]*Stopwatch, 0, 8)
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
}
diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go
index caa582a9fc..ea404d36cd 100644
--- a/models/issues/tracked_time.go
+++ b/models/issues/tracked_time.go
@@ -139,7 +139,7 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
sess = sess.Where(opts.ToConds())
- if opts.Page != 0 {
+ if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}
diff --git a/models/user/search.go b/models/user/search.go
index 382b6fac2b..6af3389237 100644
--- a/models/user/search.go
+++ b/models/user/search.go
@@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close()
- if opts.Page != 0 {
+ if opts.Page > 0 {
sessQuery = db.SetSessionPagination(sessQuery, opts)
}
diff --git a/models/user/user.go b/models/user/user.go
index a2d9166291..bd92693b6e 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -330,7 +330,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type=?", UserTypeIndividual).
And(isUserVisibleToViewerCond(viewer))
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize)
@@ -352,7 +352,7 @@ func GetUserFollowing(ctx context.Context, u, viewer *User, listOptions db.ListO
And("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).
And(isUserVisibleToViewerCond(viewer))
- if listOptions.Page != 0 {
+ if listOptions.Page > 0 {
sess = db.SetSessionPagination(sess, &listOptions)
users := make([]*User, 0, listOptions.PageSize)
diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go
index 78fbe7f792..d04088531a 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/api.go b/routers/api/v1/api.go
index 23f466873b..aee76325a8 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
}
@@ -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/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 921e7b2750..6063e54cdc 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 .
Quick Links
Here are some links to the most important topics. You can find the full list of pages at the sidebar.
-Configuration
-
+Configuration
+
`,
}
@@ -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"
- testRenderMarkdown(t, "gfm", false, input, `Link
-
+ testRenderMarkdown(t, "gfm", false, input, `Link
+
`, http.StatusOK)
- testRenderMarkdown(t, "gfm", false, input, `Link
-
+ testRenderMarkdown(t, "gfm", false, input, `Link
+
`, http.StatusOK)
- testRenderMarkup(t, "gfm", false, "", input, `Link
-
+ testRenderMarkup(t, "gfm", false, "", input, `Link
+
`, http.StatusOK)
- testRenderMarkup(t, "file", false, "path/new-file.md", input, `Link
-
+ testRenderMarkup(t, "file", false, "path/new-file.md", input, `Link
+
`, 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/api/v1/repo/download.go b/routers/api/v1/repo/download.go
new file mode 100644
index 0000000000..3620c1465f
--- /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 05650cc9be..959a4b952a 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/common/markup.go b/routers/common/markup.go
index e3e6d9cfcf..533b546a2a 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,
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index b62fd21585..f5e59b0357 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/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go
index d94e15d5f2..77ddce0534 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 d239229f4b..2d4df7aea2 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/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index c33369d047..e1addbed33 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 b3f3ed7bf3..2ab18edf49 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/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 26737f110e..20e0c9db66 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}}
-
- {{svg "octicon-kebab-horizontal" 18 "icon tw-mx-2"}}
-
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 5b9f16ef96..9f75478ebf 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -144,6 +144,18 @@ func TestAPICreateIssue(t *testing.T) {
func TestAPICreateIssueParallel(t *testing.T) {
defer tests.PrepareTestEnv(t)()
+
+ // FIXME: There seems to be a bug in github.com/mattn/go-sqlite3 with sqlite_unlock_notify, when doing concurrent writes to the same database,
+ // some requests may get stuck in "go-sqlite3.(*SQLiteRows).Next", "go-sqlite3.(*SQLiteStmt).exec" and "go-sqlite3.unlock_notify_wait",
+ // because the "unlock_notify_wait" never returns and the internal lock never gets releases.
+ //
+ // The trigger is: a previous test created issues and made the real issue indexer queue start processing, then this test does concurrent writing.
+ // Adding this "Sleep" makes go-sqlite3 "finish" some internal operations before concurrent writes and then won't get stuck.
+ // To reproduce: make a new test run these 2 tests enough times:
+ // > func TestBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
+ // Usually the test gets stuck in fewer than 10 iterations without this "sleep".
+ time.Sleep(time.Second)
+
const body, title = "apiTestBody", "apiTestTitle"
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go
index eecb84d5d1..8589199da3 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)
+}
diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go
index feb262b50e..f177bd3a23 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) {
diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go
index def6506253..ad0be72dcb 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 53c3d5aaea..55b9751cc6 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 0d489665a2..f39de96f5b 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 7a9bca7a37..0c7d2e8592 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/markup/mermaid.ts b/web_src/js/markup/mermaid.ts
index 004795d367..2dbed280c2 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);
diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts
index cb99a85511..6e71596850 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('foo bar
');
expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
});
+
+test('queryElemChildren', () => {
+ const el = createElementFromHTML('a b
');
+ 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 4bbb0c414a..da9ce71644 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) {
@@ -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);
}