mirror of
https://github.com/go-gitea/gitea
synced 2025-01-19 07:27:44 +01:00
Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show
This commit is contained in:
commit
fa4c61e160
@ -770,7 +770,7 @@ func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64)
|
|||||||
// CountActionCreatedUnixString count actions where created_unix is an empty string
|
// CountActionCreatedUnixString count actions where created_unix is an empty string
|
||||||
func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
|
func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
|
||||||
if setting.Database.Type.IsSQLite3() {
|
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
|
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
|
// FixActionCreatedUnixString set created_unix to zero if it is an empty string
|
||||||
func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
|
func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
|
||||||
if setting.Database.Type.IsSQLite3() {
|
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -256,7 +256,7 @@ func TestConsistencyUpdateAction(t *testing.T) {
|
|||||||
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
|
||||||
ID: int64(id),
|
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)
|
assert.NoError(t, err)
|
||||||
actions := make([]*activities_model.Action, 0, 1)
|
actions := make([]*activities_model.Action, 0, 1)
|
||||||
//
|
//
|
||||||
|
@ -1108,7 +1108,7 @@ func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList,
|
|||||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Page != 0 {
|
if opts.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
sess = db.SetSessionPagination(sess, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,7 +641,7 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
|
|||||||
Where("issue_id = ?", issue.ID).
|
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
|
// 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)
|
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)
|
sess = db.SetSessionPagination(sess, &opts)
|
||||||
}
|
}
|
||||||
err = sess.Find(&issueDeps)
|
err = sess.Find(&issueDeps)
|
||||||
|
@ -105,7 +105,7 @@ func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOpt
|
|||||||
And("`user`.prohibit_login = ?", false).
|
And("`user`.prohibit_login = ?", false).
|
||||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
|
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
|
||||||
|
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
watches := make([]*IssueWatch, 0, listOptions.PageSize)
|
watches := make([]*IssueWatch, 0, listOptions.PageSize)
|
||||||
return watches, sess.Find(&watches)
|
return watches, sess.Find(&watches)
|
||||||
|
@ -390,7 +390,7 @@ func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listO
|
|||||||
sess.Asc("name")
|
sess.Asc("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -462,7 +462,7 @@ func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOpt
|
|||||||
sess.Asc("name")
|
sess.Asc("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,7 +406,7 @@ func TestDeleteIssueLabel(t *testing.T) {
|
|||||||
PosterID: doerID,
|
PosterID: doerID,
|
||||||
IssueID: issueID,
|
IssueID: issueID,
|
||||||
LabelID: labelID,
|
LabelID: labelID,
|
||||||
}, `content=""`)
|
}, `content=''`)
|
||||||
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
|
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID})
|
||||||
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
|
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
|
||||||
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
|
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
|
||||||
|
@ -163,7 +163,7 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
|
|||||||
Where(opts.toConds()).
|
Where(opts.toConds()).
|
||||||
In("reaction.`type`", setting.UI.Reactions).
|
In("reaction.`type`", setting.UI.Reactions).
|
||||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id")
|
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)
|
sess = db.SetSessionPagination(sess, &opts)
|
||||||
|
|
||||||
reactions := make([]*Reaction, 0, opts.PageSize)
|
reactions := make([]*Reaction, 0, opts.PageSize)
|
||||||
|
@ -96,7 +96,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
|
|||||||
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
|
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
|
||||||
sws := make([]*Stopwatch, 0, 8)
|
sws := make([]*Stopwatch, 0, 8)
|
||||||
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
|
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
|
|||||||
|
|
||||||
sess = sess.Where(opts.ToConds())
|
sess = sess.Where(opts.ToConds())
|
||||||
|
|
||||||
if opts.Page != 0 {
|
if opts.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
sess = db.SetSessionPagination(sess, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _
|
|||||||
|
|
||||||
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
|
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
|
||||||
defer sessQuery.Close()
|
defer sessQuery.Close()
|
||||||
if opts.Page != 0 {
|
if opts.Page > 0 {
|
||||||
sessQuery = db.SetSessionPagination(sessQuery, opts)
|
sessQuery = db.SetSessionPagination(sessQuery, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +330,7 @@ func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListO
|
|||||||
And("`user`.type=?", UserTypeIndividual).
|
And("`user`.type=?", UserTypeIndividual).
|
||||||
And(isUserVisibleToViewerCond(viewer))
|
And(isUserVisibleToViewerCond(viewer))
|
||||||
|
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
|
|
||||||
users := make([]*User, 0, listOptions.PageSize)
|
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("`user`.type IN (?, ?)", UserTypeIndividual, UserTypeOrganization).
|
||||||
And(isUserVisibleToViewerCond(viewer))
|
And(isUserVisibleToViewerCond(viewer))
|
||||||
|
|
||||||
if listOptions.Page != 0 {
|
if listOptions.Page > 0 {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
|
|
||||||
users := make([]*User, 0, listOptions.PageSize)
|
users := make([]*User, 0, listOptions.PageSize)
|
||||||
|
@ -22,8 +22,6 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type codeSearchResult struct {
|
type codeSearchResult struct {
|
||||||
|
@ -321,7 +321,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !allow {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1377,6 +1377,8 @@ func Routes() *web.Router {
|
|||||||
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
|
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
|
||||||
m.Delete("", repo.DeleteAvatar)
|
m.Delete("", repo.DeleteAvatar)
|
||||||
}, reqAdmin(), reqToken())
|
}, reqAdmin(), reqToken())
|
||||||
|
|
||||||
|
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
|
||||||
}, repoAssignment(), checkTokenPublicOnly())
|
}, repoAssignment(), checkTokenPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||||
|
|
||||||
|
@ -7,15 +7,19 @@ import (
|
|||||||
go_context "context"
|
go_context "context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
context_service "code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -23,10 +27,17 @@ import (
|
|||||||
|
|
||||||
const AppURL = "http://localhost:3000/"
|
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) {
|
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||||
context := "/gogits/gogs"
|
context := "/user2/repo1"
|
||||||
if !wiki {
|
if !wiki {
|
||||||
context += path.Join("/src/branch/main", path.Dir(filePath))
|
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,
|
FilePath: filePath,
|
||||||
}
|
}
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
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)
|
web.SetForm(ctx, &options)
|
||||||
Markup(ctx)
|
Markup(ctx)
|
||||||
assert.Equal(t, expectedBody, resp.Body.String())
|
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) {
|
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
|
||||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
context := "/gogits/gogs"
|
context := "/user2/repo1"
|
||||||
if !wiki {
|
if !wiki {
|
||||||
context += "/src/branch/main"
|
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) {
|
func TestAPI_RenderGFM(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
markup.Init(&markup.RenderHelperFuncs{
|
markup.Init(&markup.RenderHelperFuncs{
|
||||||
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
|
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
|
||||||
return username == "r-lyeh"
|
return username == "r-lyeh"
|
||||||
@ -82,20 +96,20 @@ func TestAPI_RenderGFM(t *testing.T) {
|
|||||||
// rendered
|
// rendered
|
||||||
`<p>Wiki! Enjoy :)</p>
|
`<p>Wiki! Enjoy :)</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
<li><a href="http://localhost:3000/user2/repo1/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||||
<li><a href="http://localhost:3000/gogits/gogs/wiki/Tips" rel="nofollow">Tips</a></li>
|
<li><a href="http://localhost:3000/user2/repo1/wiki/Tips" rel="nofollow">Tips</a></li>
|
||||||
<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
// Guard wiki sidebar: special syntax
|
// Guard wiki sidebar: special syntax
|
||||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||||
// rendered
|
// rendered
|
||||||
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
`<p><a href="http://localhost:3000/user2/repo1/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||||
`,
|
`,
|
||||||
// special syntax
|
// special syntax
|
||||||
`[[Name|Link]]`,
|
`[[Name|Link]]`,
|
||||||
// rendered
|
// rendered
|
||||||
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p>
|
`<p><a href="http://localhost:3000/user2/repo1/wiki/Link" rel="nofollow">Name</a></p>
|
||||||
`,
|
`,
|
||||||
// empty
|
// empty
|
||||||
``,
|
``,
|
||||||
@ -119,8 +133,8 @@ Here are some links to the most important topics. You can find the full list of
|
|||||||
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
||||||
<h2 id="user-content-quick-links">Quick Links</h2>
|
<h2 id="user-content-quick-links">Quick Links</h2>
|
||||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||||
<p><a href="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a>
|
<p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a>
|
||||||
<a href="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
<a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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)"
|
input := "[Link](test.md)\n![Image](image.png)"
|
||||||
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
`, http.StatusOK)
|
`, http.StatusOK)
|
||||||
|
|
||||||
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
`, http.StatusOK)
|
`, http.StatusOK)
|
||||||
|
|
||||||
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
|
||||||
`, http.StatusOK)
|
`, http.StatusOK)
|
||||||
|
|
||||||
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/path/test.md" rel="nofollow">Link</a>
|
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a>
|
||||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
|
<a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
|
||||||
`, http.StatusOK)
|
`, http.StatusOK)
|
||||||
|
|
||||||
testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)
|
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{
|
options := api.MarkdownOption{
|
||||||
Mode: "markdown",
|
Mode: "markdown",
|
||||||
Text: "",
|
Text: "",
|
||||||
Context: "/gogits/gogs",
|
Context: "/user2/repo1",
|
||||||
}
|
}
|
||||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||||
for i := 0; i < len(simpleCases); i += 2 {
|
for i := 0; i < len(simpleCases); i += 2 {
|
||||||
|
53
routers/api/v1/repo/download.go
Normal file
53
routers/api/v1/repo/download.go
Normal file
@ -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)
|
||||||
|
}
|
@ -301,7 +301,13 @@ func GetArchive(ctx *context.APIContext) {
|
|||||||
|
|
||||||
func archiveDownload(ctx *context.APIContext) {
|
func archiveDownload(ctx *context.APIContext) {
|
||||||
uri := ctx.PathParam("*")
|
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 err != nil {
|
||||||
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
||||||
ctx.Error(http.StatusBadRequest, "unknown archive format", err)
|
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:
|
// Add nix format link header so tarballs lock correctly:
|
||||||
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
// 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(),
|
ctx.Repo.Repository.APIURL(),
|
||||||
archiver.CommitID, archiver.CommitID))
|
archiver.CommitID,
|
||||||
|
archiver.Type.String(),
|
||||||
|
archiver.CommitID,
|
||||||
|
))
|
||||||
|
|
||||||
rPath := archiver.RelativePath()
|
rPath := archiver.RelativePath()
|
||||||
if setting.RepoArchive.Storage.ServeDirect() {
|
if setting.RepoArchive.Storage.ServeDirect() {
|
||||||
|
@ -77,8 +77,10 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
|
|||||||
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||||
case "comment":
|
case "comment":
|
||||||
rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
|
rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
|
||||||
|
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||||
case "wiki":
|
case "wiki":
|
||||||
rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
|
rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
|
||||||
|
rctx = rctx.WithMarkupType(markdown.MarkupName)
|
||||||
case "file":
|
case "file":
|
||||||
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
|
rctx = renderhelper.NewRenderContextRepoFile(ctx, repoModel, renderhelper.RepoFileOptions{
|
||||||
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
|
DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName,
|
||||||
|
@ -464,7 +464,12 @@ func RedirectDownload(ctx *context.Context) {
|
|||||||
// Download an archive of a repository
|
// Download an archive of a repository
|
||||||
func Download(ctx *context.Context) {
|
func Download(ctx *context.Context) {
|
||||||
uri := ctx.PathParam("*")
|
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 err != nil {
|
||||||
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
||||||
ctx.Error(http.StatusBadRequest, err.Error())
|
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.
|
// kind of drop it on the floor if this is the case.
|
||||||
func InitiateDownload(ctx *context.Context) {
|
func InitiateDownload(ctx *context.Context) {
|
||||||
uri := ctx.PathParam("*")
|
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 err != nil {
|
||||||
ctx.ServerError("archiver_service.NewRequest", err)
|
ctx.ServerError("archiver_service.NewRequest", err)
|
||||||
return
|
return
|
||||||
|
@ -74,26 +74,32 @@ type AccessTokenResponse struct {
|
|||||||
// GrantAdditionalScopes returns valid scopes coming from grant
|
// GrantAdditionalScopes returns valid scopes coming from grant
|
||||||
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
|
func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
|
||||||
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
|
// scopes_supported from templates/user/auth/oidc_wellknown.tmpl
|
||||||
scopesSupported := []string{
|
generalScopesSupported := []string{
|
||||||
"openid",
|
"openid",
|
||||||
"profile",
|
"profile",
|
||||||
"email",
|
"email",
|
||||||
"groups",
|
"groups",
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenScopes []string
|
var accessScopes []string // the scopes for access control, but not for general information
|
||||||
for _, tokenScope := range strings.Split(grantScopes, " ") {
|
for _, scope := range strings.Split(grantScopes, " ") {
|
||||||
if slices.Index(scopesSupported, tokenScope) == -1 {
|
if scope != "" && !slices.Contains(generalScopesSupported, scope) {
|
||||||
tokenScopes = append(tokenScopes, tokenScope)
|
accessScopes = append(accessScopes, scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// since version 1.22, access tokens grant full access to the API
|
// since version 1.22, access tokens grant full access to the API
|
||||||
// with this access is reduced only if additional scopes are provided
|
// with this access is reduced only if additional scopes are provided
|
||||||
accessTokenScope := auth.AccessTokenScope(strings.Join(tokenScopes, ","))
|
if len(accessScopes) > 0 {
|
||||||
if accessTokenWithAdditionalScopes, err := accessTokenScope.Normalize(); err == nil && len(tokenScopes) > 0 {
|
accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
|
||||||
return accessTokenWithAdditionalScopes
|
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
|
return auth.AccessTokenScopeAll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ func TestGrantAdditionalScopes(t *testing.T) {
|
|||||||
grantScopes string
|
grantScopes string
|
||||||
expectedScopes string
|
expectedScopes string
|
||||||
}{
|
}{
|
||||||
|
{"", "all"}, // for old tokens without scope, treat it as "all"
|
||||||
{"openid profile email", "all"},
|
{"openid profile email", "all"},
|
||||||
{"openid profile email groups", "all"},
|
{"openid profile email groups", "all"},
|
||||||
{"openid profile email all", "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 read:repository", "read:repository,read:user"},
|
||||||
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
|
{"read:user write:issue public-only", "public-only,write:issue,read:user"},
|
||||||
{"openid profile email read:user", "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", "all"},
|
||||||
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
|
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", "all"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
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)
|
result := GrantAdditionalScopes(test.grantScopes)
|
||||||
assert.Equal(t, test.expectedScopes, string(result))
|
assert.Equal(t, test.expectedScopes, string(result))
|
||||||
})
|
})
|
||||||
|
@ -67,30 +67,36 @@ func (e RepoRefNotFoundError) Is(err error) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRequest creates an archival request, based on the URI. The
|
func ParseFileName(uri string) (ext string, tp git.ArchiveType, err error) {
|
||||||
// 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
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(uri, ".zip"):
|
case strings.HasSuffix(uri, ".zip"):
|
||||||
ext = ".zip"
|
ext = ".zip"
|
||||||
r.Type = git.ZIP
|
tp = git.ZIP
|
||||||
case strings.HasSuffix(uri, ".tar.gz"):
|
case strings.HasSuffix(uri, ".tar.gz"):
|
||||||
ext = ".tar.gz"
|
ext = ".tar.gz"
|
||||||
r.Type = git.TARGZ
|
tp = git.TARGZ
|
||||||
case strings.HasSuffix(uri, ".bundle"):
|
case strings.HasSuffix(uri, ".bundle"):
|
||||||
ext = ".bundle"
|
ext = ".bundle"
|
||||||
r.Type = git.BUNDLE
|
tp = git.BUNDLE
|
||||||
default:
|
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.
|
// Get corresponding commit.
|
||||||
commitID, err := repo.ConvertToGitID(r.refName)
|
commitID, err := repo.ConvertToGitID(r.refName)
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/models/actions"
|
_ "code.gitea.io/gitea/models/actions"
|
||||||
@ -31,47 +32,47 @@ func TestArchive_Basic(t *testing.T) {
|
|||||||
contexttest.LoadGitRepo(t, ctx)
|
contexttest.LoadGitRepo(t, ctx)
|
||||||
defer ctx.Repo.GitRepo.Close()
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, bogusReq)
|
assert.NotNil(t, bogusReq)
|
||||||
assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
|
assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
|
||||||
|
|
||||||
// Check a series of bogus requests.
|
// Check a series of bogus requests.
|
||||||
// Step 1, valid commit with a bad extension.
|
// 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.Error(t, err)
|
||||||
assert.Nil(t, bogusReq)
|
assert.Nil(t, bogusReq)
|
||||||
|
|
||||||
// Step 2, missing commit.
|
// 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.Error(t, err)
|
||||||
assert.Nil(t, bogusReq)
|
assert.Nil(t, bogusReq)
|
||||||
|
|
||||||
// Step 3, doesn't look like branch/tag/commit.
|
// 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.Error(t, err)
|
||||||
assert.Nil(t, bogusReq)
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, bogusReq)
|
assert.NotNil(t, bogusReq)
|
||||||
assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, bogusReq)
|
assert.NotNil(t, bogusReq)
|
||||||
assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
|
assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
|
||||||
|
|
||||||
// Now two valid requests, firstCommit with valid extensions.
|
// 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.NoError(t, err)
|
||||||
assert.NotNil(t, zipReq)
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, tgzReq)
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, secondReq)
|
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.
|
// Sleep two seconds to make sure the queue doesn't change.
|
||||||
time.Sleep(2 * time.Second)
|
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)
|
assert.NoError(t, err)
|
||||||
// This zipReq should match what's sitting in the queue, as we haven't
|
// 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
|
// 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
|
// Now we'll submit a request and TimedWaitForCompletion twice, before and
|
||||||
// after we release it. We should trigger both the timeout and non-timeout
|
// after we release it. We should trigger both the timeout and non-timeout
|
||||||
// cases.
|
// 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.NoError(t, err)
|
||||||
assert.NotNil(t, timedReq)
|
assert.NotNil(t, timedReq)
|
||||||
doArchive(db.DefaultContext, 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)
|
assert.NoError(t, err)
|
||||||
// Now, we're guaranteed to have released the original zipReq from the queue.
|
// 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
|
// Ensure that we don't get handed back the released entry somehow, but they
|
||||||
|
@ -164,12 +164,11 @@
|
|||||||
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}}
|
<input type="checkbox" name="{{$file.GetDiffFileName}}" autocomplete="off"{{if $file.IsViewed}} checked{{end}}> {{ctx.Locale.Tr "repo.pulls.has_viewed_file"}}
|
||||||
</label>
|
</label>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="ui dropdown basic">
|
<button class="btn diff-header-popup-btn tw-p-1">{{svg "octicon-kebab-horizontal" 18}}</button>
|
||||||
{{svg "octicon-kebab-horizontal" 18 "icon tw-mx-2"}}
|
<div class="tippy-target">
|
||||||
<div class="ui menu">
|
|
||||||
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
|
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}}
|
||||||
<button class="unescape-button item">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
|
<button class="unescape-button item" data-file-content-elem-id="diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
|
||||||
<button class="escape-button tw-hidden item">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
|
<button class="escape-button tw-hidden item" data-file-content-elem-id="diff-{{$file.NameHash}}">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
|
{{if and (not $file.IsSubmodule) (not $.PageIsWiki)}}
|
||||||
{{if $file.IsDeleted}}
|
{{if $file.IsDeleted}}
|
||||||
@ -183,7 +182,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</h4>
|
</h4>
|
||||||
<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
|
<div class="diff-file-body ui attached unstackable table segment" {{if and $file.IsViewed $.IsShowingAllCommits}}data-folded="true"{{end}}>
|
||||||
<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} tw-hidden{{end}}">
|
<div id="diff-source-{{$file.NameHash}}" class="file-body file-code unicode-escaped code-diff{{if $.IsSplitStyle}} code-diff-split{{else}} code-diff-unified{{end}}{{if $showFileViewToggle}} tw-hidden{{end}}">
|
||||||
|
@ -144,6 +144,18 @@ func TestAPICreateIssue(t *testing.T) {
|
|||||||
|
|
||||||
func TestAPICreateIssueParallel(t *testing.T) {
|
func TestAPICreateIssueParallel(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(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"
|
const body, title = "apiTestBody", "apiTestTitle"
|
||||||
|
|
||||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
@ -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))
|
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)
|
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)
|
||||||
|
}
|
||||||
|
@ -565,7 +565,7 @@ func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
|
|||||||
|
|
||||||
errorParsed := new(errorResponse)
|
errorParsed := new(errorResponse)
|
||||||
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
|
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) {
|
func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
|
||||||
@ -708,7 +708,7 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
|
|||||||
|
|
||||||
errorParsed := new(errorResponse)
|
errorParsed := new(errorResponse)
|
||||||
require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
|
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) {
|
func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
|
||||||
|
@ -97,7 +97,7 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) {
|
|||||||
user2Session := loginUser(t, "user2")
|
user2Session := loginUser(t, "user2")
|
||||||
resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK)
|
resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK)
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
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()) {
|
if assert.Equal(t, 1, nodes.Length()) {
|
||||||
// there is only "View File" button, no "Edit File" button
|
// there is only "View File" button, no "Edit File" button
|
||||||
assert.Equal(t, "View File", nodes.First().Text())
|
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
|
// 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)
|
resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK)
|
||||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
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()) {
|
if assert.Equal(t, 2, nodes.Length()) {
|
||||||
// there are "View File" button and "Edit File" button
|
// there are "View File" button and "Edit File" button
|
||||||
assert.Equal(t, "View File", nodes.First().Text())
|
assert.Equal(t, "View File", nodes.First().Text())
|
||||||
|
@ -77,8 +77,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 9px 18px;
|
padding: 9px 18px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
background: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme="menu"] .item:hover {
|
.tippy-box[data-theme="menu"] .item:hover {
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
} from '../utils/dom.ts';
|
} from '../utils/dom.ts';
|
||||||
import {POST, GET} from '../modules/fetch.ts';
|
import {POST, GET} from '../modules/fetch.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {createTippy} from '../modules/tippy.ts';
|
||||||
|
|
||||||
const {pageData, i18n} = window.config;
|
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
|
// Will be called when the show more (files) button has been pressed
|
||||||
function onShowMoreFiles() {
|
function onShowMoreFiles() {
|
||||||
initRepoIssueContentHistory();
|
initRepoIssueContentHistory();
|
||||||
initViewedCheckboxListenerFor();
|
initViewedCheckboxListenerFor();
|
||||||
countAndUpdateViewedFiles();
|
countAndUpdateViewedFiles();
|
||||||
initImageDiff();
|
initImageDiff();
|
||||||
|
initDiffHeaderPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMoreFiles(url) {
|
export async function loadMoreFiles(url) {
|
||||||
@ -221,6 +232,7 @@ export function initRepoDiffView() {
|
|||||||
initDiffFileList();
|
initDiffFileList();
|
||||||
initDiffCommitSelect();
|
initDiffCommitSelect();
|
||||||
initRepoDiffShowMore();
|
initRepoDiffShowMore();
|
||||||
|
initDiffHeaderPopup();
|
||||||
initRepoDiffFileViewToggle();
|
initRepoDiffFileViewToggle();
|
||||||
initViewedCheckboxListenerFor();
|
initViewedCheckboxListenerFor();
|
||||||
initExpandAndCollapseFilesButton();
|
initExpandAndCollapseFilesButton();
|
||||||
|
@ -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() {
|
export function initUnicodeEscapeButton() {
|
||||||
document.addEventListener('click', (e) => {
|
addDelegatedEventListener(document, 'click', '.escape-button, .unescape-button, .toggle-escape-button', (btn, e) => {
|
||||||
const btn = e.target.closest('.escape-button, .unescape-button, .toggle-escape-button');
|
|
||||||
if (!btn) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
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');
|
const fileView = fileContent?.querySelectorAll('.file-code, .file-view');
|
||||||
if (btn.matches('.escape-button')) {
|
if (btn.matches('.escape-button')) {
|
||||||
for (const el of fileView) el.classList.add('unicode-escaped');
|
for (const el of fileView) el.classList.add('unicode-escaped');
|
||||||
|
@ -58,16 +58,12 @@ export async function renderMermaid(): Promise<void> {
|
|||||||
mermaidBlock.append(btn);
|
mermaidBlock.append(btn);
|
||||||
|
|
||||||
const updateIframeHeight = () => {
|
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 <details> + <summary> block and the <details> 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', () => {
|
iframe.addEventListener('load', () => {
|
||||||
pre.replaceWith(mermaidBlock);
|
pre.replaceWith(mermaidBlock);
|
||||||
mermaidBlock.classList.remove('tw-hidden');
|
mermaidBlock.classList.remove('tw-hidden');
|
||||||
@ -76,6 +72,13 @@ export async function renderMermaid(): Promise<void> {
|
|||||||
mermaidBlock.classList.remove('is-loading');
|
mermaidBlock.classList.remove('is-loading');
|
||||||
iframe.classList.remove('tw-invisible');
|
iframe.classList.remove('tw-invisible');
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
// update height when element's visibility state changes, for example when the diagram is inside
|
||||||
|
// a <details> + <summary> block and the <details> 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);
|
document.body.append(mermaidBlock);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createElementFromAttrs, createElementFromHTML, querySingleVisibleElem} from './dom.ts';
|
import {createElementFromAttrs, createElementFromHTML, queryElemChildren, querySingleVisibleElem} from './dom.ts';
|
||||||
|
|
||||||
test('createElementFromHTML', () => {
|
test('createElementFromHTML', () => {
|
||||||
expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
|
expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
|
||||||
@ -26,3 +26,9 @@ test('querySingleVisibleElem', () => {
|
|||||||
el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
|
el = createElementFromHTML('<div><span>foo</span><span>bar</span></div>');
|
||||||
expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
|
expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('queryElemChildren', () => {
|
||||||
|
const el = createElementFromHTML('<div><span class="a">a</span><span class="b">b</span></div>');
|
||||||
|
const children = queryElemChildren(el, '.a');
|
||||||
|
expect(children.length).toEqual(1);
|
||||||
|
});
|
||||||
|
@ -2,10 +2,10 @@ import {debounce} from 'throttle-debounce';
|
|||||||
import type {Promisable} from 'type-fest';
|
import type {Promisable} from 'type-fest';
|
||||||
import type $ from 'jquery';
|
import type $ from 'jquery';
|
||||||
|
|
||||||
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
|
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
||||||
|
type ElementArg = Element | string | ArrayLikeIterable<Element> | ReturnType<typeof $>;
|
||||||
type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
|
type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
|
||||||
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
|
||||||
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
|
|
||||||
|
|
||||||
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
|
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
|
||||||
if (typeof el === 'string' || el instanceof String) {
|
if (typeof el === 'string' || el instanceof String) {
|
||||||
@ -76,6 +76,11 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
|
|||||||
|
|
||||||
// it works like jQuery.children: only the direct children are selected
|
// it works like jQuery.children: only the direct children are selected
|
||||||
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
|
||||||
|
if (window.vitest) {
|
||||||
|
// bypass the vitest bug: it doesn't support ":scope >"
|
||||||
|
const selected = Array.from<T>(parent.children as any).filter((child) => child.matches(selector));
|
||||||
|
return applyElemsCallback<T>(selected, fn);
|
||||||
|
}
|
||||||
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user