From 5c084c883cacf2fb82e4e31b72c6093a2914da77 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 1 Jun 2026 01:23:43 +0000 Subject: [PATCH 01/88] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 13 +++++++++++++ options/locale/locale_ko-KR.json | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 2f2843f2e26..3e3fd8746f3 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -2725,6 +2725,7 @@ "graphs.code_frequency.what": "minicíocht cód", "graphs.contributors.what": "ranníocaíochtaí", "graphs.recent_commits.what": "tiomantáin le déanaí", + "graphs.chart_zoom_hint": "tarraing: súmáil, shift+tarraing: panáil, cliceáil faoi dhó: athshocraigh súmáil", "org.org_name_holder": "Ainm na hEagraíochta", "org.org_full_name_holder": "Ainm iomlán na hEagraíochta", "org.org_name_helper": "Ba cheart go mbeadh ainmneacha eagraíochta gearr agus i gcuimhne.", @@ -3792,11 +3793,23 @@ "actions.runs.view_workflow_file": "Féach ar chomhad sreabha oibre", "actions.runs.summary": "Achoimre", "actions.runs.all_jobs": "Gach post", + "actions.runs.expand_caller_jobs": "Taispeáin poist an ghlaoiteora sreabha oibre in-athúsáidte seo", + "actions.runs.collapse_caller_jobs": "Folaigh poist an ghlaoiteora sreabha oibre in-athúsáidte seo", "actions.runs.attempt": "Iarracht", "actions.runs.latest": "Is déanaí", "actions.runs.latest_attempt": "An iarracht is déanaí", "actions.runs.triggered_via": "Spreagtha trí %s", "actions.runs.total_duration": "Fad iomlán:", + "actions.runs.workflow_dependencies": "Spleáchais ar Shreabhadh Oibre", + "actions.runs.graph_jobs_count_1": "%d post", + "actions.runs.graph_jobs_count_n": "%d poist", + "actions.runs.graph_dependencies_count_1": "%d spleáchas", + "actions.runs.graph_dependencies_count_n": "%d spleáchais", + "actions.runs.graph_success_rate": "%s rath", + "actions.runs.graph_zoom_in": "Zúmáil isteach (Ctrl/Cmd + scrollaigh ar an ngraf)", + "actions.runs.graph_zoom_max": "Ag súmáil 100% cheana féin", + "actions.runs.graph_zoom_out": "Zúmáil amach (Ctrl/Cmd + scrollaigh ar an ngraf)", + "actions.runs.graph_reset_view": "Athshocraigh an radharc", "actions.workflow.disable": "Díchumasaigh sreabhadh oibre", "actions.workflow.disable_success": "D'éirigh le sreabhadh oibre '%s' a dhíchumasú.", "actions.workflow.enable": "Cumasaigh sreabhadh oibre", diff --git a/options/locale/locale_ko-KR.json b/options/locale/locale_ko-KR.json index fcccd1eb324..121d0fb8bfe 100644 --- a/options/locale/locale_ko-KR.json +++ b/options/locale/locale_ko-KR.json @@ -2725,6 +2725,7 @@ "graphs.code_frequency.what": "코드 빈도", "graphs.contributors.what": "기여", "graphs.recent_commits.what": "최근 커밋", + "graphs.chart_zoom_hint": "드래그: 줌, shift+드래그: 팬, 더블 클릭: 줌 리셋", "org.org_name_holder": "조직 이름", "org.org_full_name_holder": "조직 전체 이름", "org.org_name_helper": "조직명은 짧고 기억하기 쉬워야 합니다.", @@ -3776,7 +3777,7 @@ "actions.runs.status": "상태", "actions.runs.actors_no_select": "모든 액터", "actions.runs.status_no_select": "모든 상태", - "actions.runs.branch": "브렌치", + "actions.runs.branch": "브랜치", "actions.runs.branches_no_select": "모든 브랜치", "actions.runs.no_results": "일치하는 결과가 없습니다.", "actions.runs.no_workflows": "아직 워크플로가 없습니다.", @@ -3792,11 +3793,23 @@ "actions.runs.view_workflow_file": "워크플로우 파일 표시", "actions.runs.summary": "요약", "actions.runs.all_jobs": "모든 작업", + "actions.runs.expand_caller_jobs": "이 재사용 워크플로 호출기의 작업 표시", + "actions.runs.collapse_caller_jobs": "이 재사용 워크플로 호출기의 작업 숨기기", "actions.runs.attempt": "시도", "actions.runs.latest": "최신", "actions.runs.latest_attempt": "최근 시도", "actions.runs.triggered_via": "%s를 통해 트리거됨", "actions.runs.total_duration": "총기간:", + "actions.runs.workflow_dependencies": "워크플로우 의존성", + "actions.runs.graph_jobs_count_1": "%d 작업", + "actions.runs.graph_jobs_count_n": "%d 작업", + "actions.runs.graph_dependencies_count_1": "%d 의존성", + "actions.runs.graph_dependencies_count_n": "%d 의존성", + "actions.runs.graph_success_rate": "%s 성공", + "actions.runs.graph_zoom_in": "줌인 (그래프에서 Ctrl/Cmd + 스크롤)", + "actions.runs.graph_zoom_max": "이미 100% 줌", + "actions.runs.graph_zoom_out": "줌아웃(그래프에서 Ctrl/Cmd + 스크롤)", + "actions.runs.graph_reset_view": "보기 다시설정", "actions.workflow.disable": "워크플로 비활성화", "actions.workflow.disable_success": "워크플로 '%s'가 성공적으로 비활성화되었습니다.", "actions.workflow.enable": "워크플로 활성화", From 9155a81b9daf1d46b2380aa91271e623ac947c1e Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Mon, 1 Jun 2026 18:22:17 +0200 Subject: [PATCH 02/88] docs: mark openapi3 as autogenerated in attributes (#37963) Change from Co-Authored by trailer to Assisted-By and explicitly forbid LLMs from signing off on commits. --------- Signed-off-by: bircni Signed-off-by: silverwind Co-authored-by: bircni Co-authored-by: silverwind --- .gitattributes | 1 + AGENTS.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index afd02555f59..3ddb8f641ba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,7 @@ /assets/*.json linguist-generated /public/assets/img/svg/*.svg linguist-generated /templates/swagger/v1_json.tmpl linguist-generated +/templates/swagger/v1_openapi3_json.tmpl linguist-generated /options/fileicon/** linguist-generated /vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored diff --git a/AGENTS.md b/AGENTS.md index a4cb7db74ce..5a4ecda6da9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,4 +16,5 @@ - In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist - For CSS layout, prefer `flex-*` helpers over per-child `tw-ml-*` / `tw-mr-*` margins; fall back to `tw-*` utilities when specificity requires `!important` - Include authorship attribution in issue and pull request comments -- Add `Co-Authored-By` lines to all commits, indicating name and model used +- Always add `Assisted-By` trailers to commit messages in format `Assisted-by: AGENT_NAME:MODEL_VERSION` +- Never add `Co-Authored-By` `Signed-off-by` trailer to commit messages. Sign off must be done by a human. From 689ace1ce28fd74244b8aa335d9928cdbf6b22f9 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 1 Jun 2026 13:16:04 -0700 Subject: [PATCH 03/88] feat(orgs): Add search bar for organization members tab page (#37347) Resolve #37072 image --------- Signed-off-by: Lunny Xiao Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: bircni --- models/organization/org.go | 44 ++++++++++++++++-- models/organization/org_test.go | 74 +++++++++++++++++++++++++++++++ routers/web/org/members.go | 18 +++++--- templates/org/member/members.tmpl | 17 +++++++ 4 files changed, 145 insertions(+), 8 deletions(-) diff --git a/models/organization/org.go b/models/organization/org.go index dfb6b690eb0..d9390fc147f 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -19,6 +19,7 @@ import ( "gitea.dev/modules/util" "xorm.io/builder" + "xorm.io/xorm" ) // ErrOrgNotExist represents a "OrgNotExist" kind of error. @@ -180,15 +181,47 @@ func (org *Organization) HomeLink() string { // FindOrgMembersOpts represents find org members conditions type FindOrgMembersOpts struct { db.ListOptions - Doer *user_model.User - IsDoerMember bool - OrgID int64 + Doer *user_model.User + IsDoerMember bool + OrgID int64 + Keyword string + SearchByEmail bool } func (opts FindOrgMembersOpts) PublicOnly() bool { return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin) } +func (opts FindOrgMembersOpts) applyKeywordFilter(sess *xorm.Session) (*xorm.Session, bool) { + if opts.Keyword == "" { + return sess, false + } + + lowerKeyword := strings.ToLower(opts.Keyword) + keywordCond := builder.Or( + builder.Like{"`user`.lower_name", lowerKeyword}, + builder.Like{"LOWER(`user`.full_name)", lowerKeyword}, + ) + if opts.SearchByEmail { + var emailCond builder.Cond = builder.Like{"LOWER(`user`.email)", lowerKeyword} + switch { + case opts.Doer == nil: + emailCond = emailCond.And(builder.Eq{"`user`.keep_email_private": false}) + case !opts.Doer.IsAdmin: + emailCond = emailCond.And( + builder.Or( + builder.Eq{"`user`.keep_email_private": false}, + builder.Eq{"`user`.id": opts.Doer.ID}, + ), + ) + } + keywordCond = keywordCond.Or(emailCond) + } + + sess = sess.Join("INNER", "`user`", "org_user.uid = `user`.id").And(keywordCond) + return sess, true +} + // applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess db.Session) { if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted { @@ -212,6 +245,7 @@ func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, erro } else { opts.applyTeamMatesOnlyFilter(sess) } + sess, _ = opts.applyKeywordFilter(sess) return sess.Count(new(OrgUser)) } @@ -460,7 +494,11 @@ func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUs } else { opts.applyTeamMatesOnlyFilter(sess) } + if keywordSess, hasKeyword := opts.applyKeywordFilter(sess); hasKeyword { + sess = keywordSess.Select("org_user.*") + } + sess = sess.OrderBy("org_user.uid ASC") if opts.ListOptions.PageSize > 0 { db.SetSessionPagination(sess, opts) diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 775ce965509..8fe4b792976 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -288,6 +288,80 @@ func TestGetOrgUsersByOrgID(t *testing.T) { assert.Empty(t, orgUsers) } +func TestOrgMembersSearch(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + testCases := []struct { + name string + opts *organization.FindOrgMembersOpts + expectedUIDs []int64 + }{ + { + name: "match by username", + opts: &organization.FindOrgMembersOpts{ + OrgID: 3, + Doer: member, + IsDoerMember: true, + Keyword: "user4", + SearchByEmail: true, + }, + expectedUIDs: []int64{4}, + }, + { + name: "match by full name", + opts: &organization.FindOrgMembersOpts{ + OrgID: 3, + Doer: member, + IsDoerMember: true, + Keyword: "user27", + SearchByEmail: true, + }, + expectedUIDs: []int64{28}, + }, + { + name: "private email hidden", + opts: &organization.FindOrgMembersOpts{ + OrgID: 3, + Doer: member, + IsDoerMember: true, + Keyword: "user2@example.com", + SearchByEmail: true, + }, + expectedUIDs: []int64{}, + }, + { + name: "admin can search private email", + opts: &organization.FindOrgMembersOpts{ + OrgID: 3, + Doer: admin, + Keyword: "user2@example.com", + SearchByEmail: true, + }, + expectedUIDs: []int64{2}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + count, err := organization.CountOrgMembers(t.Context(), tc.opts) + assert.NoError(t, err) + assert.EqualValues(t, len(tc.expectedUIDs), count) + + members, err := organization.GetOrgUsersByOrgID(t.Context(), tc.opts) + assert.NoError(t, err) + memberUIDs := make([]int64, 0, len(members)) + for _, member := range members { + memberUIDs = append(memberUIDs, member.UID) + } + slices.Sort(memberUIDs) + assert.Equal(t, tc.expectedUIDs, memberUIDs) + }) + } +} + func TestChangeOrgUserStatus(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 8b56e9b53f1..4a7799a6e28 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -31,10 +31,14 @@ func Members(ctx *context.Context) { ctx.Data["PageIsOrgMembers"] = true page := max(ctx.FormInt("page"), 1) + keyword := ctx.FormTrim("q") + ctx.Data["Keyword"] = keyword opts := &organization.FindOrgMembersOpts{ - Doer: ctx.Doer, - OrgID: org.ID, + Doer: ctx.Doer, + OrgID: org.ID, + Keyword: keyword, + SearchByEmail: true, } if ctx.Doer != nil { @@ -58,9 +62,11 @@ func Members(ctx *context.Context) { return } - pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5) - opts.ListOptions.Page = page - opts.ListOptions.PageSize = setting.UI.MembersPagingNum + pageSize := setting.UI.MembersPagingNum + pager := context.NewPagination(total, pageSize, page, 5) + pager.AddParamFromRequest(ctx.Req) + opts.ListOptions.Page = pager.Paginater.Current() + opts.ListOptions.PageSize = pageSize members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts) if err != nil { ctx.ServerError("GetMembers", err) @@ -68,6 +74,8 @@ func Members(ctx *context.Context) { } ctx.Data["Page"] = pager ctx.Data["Members"] = members + ctx.Data["MembersShown"] = len(members) + ctx.Data["MembersTotal"] = total ctx.Data["MembersIsPublicMember"] = membersIsPublic ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(ctx, members, org.ID) ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus(ctx) diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index b8e88b837d5..ea637e76fbb 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -11,6 +11,17 @@
{{end}} +
{{range .Members}} {{$isPublic := index $.MembersIsPublicMember .ID}} @@ -67,6 +78,12 @@ {{end}}
+ {{else}} +
+
+ {{ctx.Locale.Tr "search.no_results"}} +
+
{{end}} {{template "base/paginate" .}} From 85f563da6cb1789a112b581911b6aea83828288f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 2 Jun 2026 04:38:23 +0800 Subject: [PATCH 04/88] chore: various frontend changes (#37973) --- templates/repo/issue/view_content.tmpl | 11 ----------- web_src/css/base.css | 19 ------------------- web_src/css/index.css | 3 ++- web_src/css/modules/animations.css | 7 ------- web_src/css/modules/message.css | 5 +++++ web_src/css/modules/transition.css | 24 ++++++++++++++++++++++++ 6 files changed, 31 insertions(+), 38 deletions(-) create mode 100644 web_src/css/modules/transition.css diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index d1b327434bd..eaee1d87731 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -171,14 +171,3 @@ {{template "repo/issue/view_content/reference_issue_dialog" .}} {{template "shared/user/block_user_dialog" .}} - - diff --git a/web_src/css/base.css b/web_src/css/base.css index 670ae0c441b..49ddca57a30 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -382,16 +382,6 @@ a.label, color: var(--color-text-light-2); } -/* styles from removed fomantic transition module */ -.hidden.transition { - visibility: hidden; - display: none; -} -.visible.transition { - display: block !important; - visibility: visible !important; -} - .ui.comments .comment .metadata { color: var(--color-text-light-2); } @@ -438,11 +428,6 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -.ui.message.flash-message pre { - white-space: pre-line; - margin: 0; -} - .ui .header > i + .content { padding-left: 0.75rem; vertical-align: middle; @@ -486,10 +471,6 @@ img.ui.avatar, color: var(--color-text) !important; } -.ui .border { - border: 1px solid; -} - .user-menu > .item { width: 100%; border-radius: 0 !important; diff --git a/web_src/css/index.css b/web_src/css/index.css index 06f7101af95..89ad63ba1a8 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -22,8 +22,9 @@ @import "./modules/tab.css"; @import "./modules/form.css"; @import "./modules/dropdown.css"; -@import "./modules/shortcut.css"; +@import "./modules/transition.css"; +@import "./modules/shortcut.css"; @import "./modules/tippy.css"; @import "./modules/breadcrumb.css"; @import "./modules/comment.css"; diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index cdbcf45a969..4b68f486667 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -101,13 +101,6 @@ code.language-math.is-loading::after { animation: pulse-1p5 200ms linear; } -.ui.modal, -.ui.dimmer.transition { - animation-name: fadein; - animation-duration: 100ms; - animation-timing-function: ease-in-out; -} - .rotate-clockwise { animation: rotate-clockwise-keyframes 1s linear infinite; } diff --git a/web_src/css/modules/message.css b/web_src/css/modules/message.css index bb0965a077f..61ce04666c2 100644 --- a/web_src/css/modules/message.css +++ b/web_src/css/modules/message.css @@ -31,6 +31,11 @@ details.ui.message:not(:has(pre)) summary { cursor: text; } +.ui.message.flash-message pre { + white-space: pre-line; + margin: 0; +} + .ui.message:first-child { margin-top: 0; } diff --git a/web_src/css/modules/transition.css b/web_src/css/modules/transition.css new file mode 100644 index 00000000000..3031c217f7a --- /dev/null +++ b/web_src/css/modules/transition.css @@ -0,0 +1,24 @@ +/* styles for Fomantic transition (toggle): dimmer, modal, dropdown menu */ + +/* this is the only place using "hidden" and "visible" classes, it can safely work with tailwind without prefix in the future */ +.ui.modal.transition.hidden, +.ui.dropdown > .menu.transition.hidden { + display: none; + visibility: hidden; +} + +.ui.modal.transition.visible, +.ui.dropdown > .menu.transition.visible { + display: block; + visibility: visible; +} + +/* dimmer uses "active" but not "hidden/visible" classes, no special classes for it here */ + +/* only modal and dimmer need animation, dropdown menu doesn't */ +.ui.modal.transition, +.ui.dimmer.transition { + animation-name: fadein; + animation-duration: 100ms; + animation-timing-function: ease-in-out; +} From 9aa4e897e7c7e9e5345d3cc873f921713ee11de8 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 1 Jun 2026 14:05:09 -0700 Subject: [PATCH 05/88] chore(deps): update tool dependencies (#37965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/air-verse/air](https://redirect.github.com/air-verse/air) | `v1.65.2` → `v1.65.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fair-verse%2fair/v1.65.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fair-verse%2fair/v1.65.2/v1.65.3?slim=true) | | [github.com/editorconfig-checker/editorconfig-checker/v3](https://redirect.github.com/editorconfig-checker/editorconfig-checker) | `v3.6.1` → `v3.7.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2feditorconfig-checker%2feditorconfig-checker%2fv3/v3.7.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2feditorconfig-checker%2feditorconfig-checker%2fv3/v3.6.1/v3.7.0?slim=true) | --- ### Release Notes
air-verse/air (github.com/air-verse/air) ### [`v1.65.3`](https://redirect.github.com/air-verse/air/releases/tag/v1.65.3) [Compare Source](https://redirect.github.com/air-verse/air/compare/v1.65.2...v1.65.3) ##### What's Changed - Extend stale workflow timeout by [@​xiantang](https://redirect.github.com/xiantang) in [#​903](https://redirect.github.com/air-verse/air/pull/903) - Increase stale workflow operation limit by [@​xiantang](https://redirect.github.com/xiantang) in [#​904](https://redirect.github.com/air-verse/air/pull/904) - Add review guidelines for coding agents by [@​xiantang](https://redirect.github.com/xiantang) in [#​905](https://redirect.github.com/air-verse/air/pull/905) - Add configurable color output mode by [@​xiantang](https://redirect.github.com/xiantang) in [#​907](https://redirect.github.com/air-verse/air/pull/907) - fix: rewatch files after atomic saves by [@​xiantang](https://redirect.github.com/xiantang) in [#​908](https://redirect.github.com/air-verse/air/pull/908) - follow-up: fix watcher recovery after atomic saves by [@​xiantang](https://redirect.github.com/xiantang) in [#​909](https://redirect.github.com/air-verse/air/pull/909) - Accept .config/air.toml by [@​bersace](https://redirect.github.com/bersace) in [#​716](https://redirect.github.com/air-verse/air/pull/716) - fix: keep built binary after app shutdown by [@​mariusvniekerk](https://redirect.github.com/mariusvniekerk) in [#​911](https://redirect.github.com/air-verse/air/pull/911) ##### New Contributors - [@​bersace](https://redirect.github.com/bersace) made their first contribution in [#​716](https://redirect.github.com/air-verse/air/pull/716) **Full Changelog**:
editorconfig-checker/editorconfig-checker (github.com/editorconfig-checker/editorconfig-checker/v3) ### [`v3.7.0`](https://redirect.github.com/editorconfig-checker/editorconfig-checker/releases/tag/v3.7.0) [Compare Source](https://redirect.github.com/editorconfig-checker/editorconfig-checker/compare/v3.6.1...v3.7.0) ##### Features - **files:** expand glob patterns in passed-file args ([#​190](https://redirect.github.com/editorconfig-checker/editorconfig-checker/issues/190)) ([#​558](https://redirect.github.com/editorconfig-checker/editorconfig-checker/issues/558)) ([4c0f326](https://redirect.github.com/editorconfig-checker/editorconfig-checker/commit/4c0f326cfa71fb0dd80c0c71b1844b2550ed799e)) ##### Bug Fixes - **cli:** auto-enable no-color when output format is github-actions ([#​557](https://redirect.github.com/editorconfig-checker/editorconfig-checker/issues/557)) ([9f4014c](https://redirect.github.com/editorconfig-checker/editorconfig-checker/commit/9f4014ce0944f601472e5cbfaec31f711890c780)) - detect binary files before decoding to prevent false text ([#​550](https://redirect.github.com/editorconfig-checker/editorconfig-checker/issues/550)) ([f47b30c](https://redirect.github.com/editorconfig-checker/editorconfig-checker/commit/f47b30c96713107bc4fe0b7a05e79a293c4874dd))
--- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: silverwind Co-authored-by: Lunny Xiao --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b8cea6b891c..a74752b8d71 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ COMMA := , XGO_VERSION := go-1.26.x -AIR_PACKAGE ?= github.com/air-verse/air@v1.65.2 # renovate: datasource=go -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go +AIR_PACKAGE ?= github.com/air-verse/air@v1.65.3 # renovate: datasource=go +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.7.0 # renovate: datasource=go GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 # renovate: datasource=go From ab2a72fe0420208a67039c3c98bfb08704d84e18 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 1 Jun 2026 16:32:32 -0700 Subject: [PATCH 06/88] fix(deps): update module github.com/google/go-github/v87 to v88 (#37971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [github.com/google/go-github/v87](https://redirect.github.com/google/go-github) | `v87.0.0` → `v88.0.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgoogle%2fgo-github%2fv87/v88.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgoogle%2fgo-github%2fv87/v87.0.0/v88.0.0?slim=true) | --- ### Release Notes
google/go-github (github.com/google/go-github/v87) ### [`v88.0.0`](https://redirect.github.com/google/go-github/releases/tag/v88.0.0) [Compare Source](https://redirect.github.com/google/go-github/compare/v87.0.0...v88.0.0) This release contains the following breaking API changes: - refactor!: Change app installation `Find*` methods to `Get*` ([#​4243](https://redirect.github.com/google/go-github/issues/4243)) BREAKING CHANGE: App installation methods are renamed from `Find*` to `Get*`. ...and the following additional changes: - chore: Bump version of go-github to v88.0.0 ([#​4245](https://redirect.github.com/google/go-github/issues/4245)) - chore: Update `openapi_operations.yaml` ([#​4242](https://redirect.github.com/google/go-github/issues/4242)) - feat: Add support for setting client URLs ([#​4240](https://redirect.github.com/google/go-github/issues/4240)) - refactor: Add constants for API versions ([#​4236](https://redirect.github.com/google/go-github/issues/4236)) - docs: Formatting and punctuation changes ([#​4235](https://redirect.github.com/google/go-github/issues/4235)) - feat: Add `GetParentIssue` for sub-issues ([#​4232](https://redirect.github.com/google/go-github/issues/4232)) - chore: Bump go-github from v86 to v87 in /scrape ([#​4234](https://redirect.github.com/google/go-github/issues/4234))
--- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- assets/go-licenses.json | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- services/migrations/error.go | 2 +- services/migrations/github.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index b2912a3ab3c..22dc1696246 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -660,8 +660,8 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/go-github/v87", - "path": "github.com/google/go-github/v87/LICENSE", + "name": "github.com/google/go-github/v88", + "path": "github.com/google/go-github/v88/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { diff --git a/go.mod b/go.mod index 0bac202baba..ead88bf5fb7 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/google/go-github/v87 v87.0.0 + github.com/google/go-github/v88 v88.0.0 github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/pprof v0.0.0-20260507013755-92041b743c96 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 08d3d896b9b..521779a0278 100644 --- a/go.sum +++ b/go.sum @@ -380,8 +380,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v87 v87.0.0 h1:9Ck3dcOxWJyfsN8tzdah4YvmqB/7ZsstMglv/PkOsl0= -github.com/google/go-github/v87 v87.0.0/go.mod h1:hGUoT5pwm/ck5uLL+wroSVQfg8mpe+buxllCcGV4VaM= +github.com/google/go-github/v88 v88.0.0 h1:dZA9IKkPK1eXZj4ypngnpRj5FwdpTv4whix2PrQMP7M= +github.com/google/go-github/v88 v88.0.0/go.mod h1:rufTDgn2N45wjhukLTyxmvc9nilSp3mr3Rgtt6b1MPw= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/services/migrations/error.go b/services/migrations/error.go index d9a6b8f2675..2dd5540f0ec 100644 --- a/services/migrations/error.go +++ b/services/migrations/error.go @@ -7,7 +7,7 @@ package migrations import ( "errors" - "github.com/google/go-github/v87/github" + "github.com/google/go-github/v88/github" ) // ErrRepoNotCreated returns the error that repository not created diff --git a/services/migrations/github.go b/services/migrations/github.go index e31a2fa6057..b8bf6aee453 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -20,7 +20,7 @@ import ( "gitea.dev/modules/proxy" "gitea.dev/modules/structs" - "github.com/google/go-github/v87/github" + "github.com/google/go-github/v88/github" "golang.org/x/oauth2" ) From 798578115b8ed8ca764be7c2c3fe41d01ae0e7fd Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 1 Jun 2026 22:18:20 -0700 Subject: [PATCH 07/88] fix(deps): update npm dependencies, remove nolyfill (#37968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@eslint-community/eslint-plugin-eslint-comments](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments) | [`4.7.1` → `4.7.2`](https://renovatebot.com/diffs/npm/@eslint-community%2feslint-plugin-eslint-comments/4.7.1/4.7.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@eslint-community%2feslint-plugin-eslint-comments/4.7.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@eslint-community%2feslint-plugin-eslint-comments/4.7.1/4.7.2?slim=true) | | [@primer/octicons](https://primer.style/octicons) ([source](https://redirect.github.com/primer/octicons)) | [`19.26.0` → `19.27.0`](https://renovatebot.com/diffs/npm/@primer%2focticons/19.26.0/19.27.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@primer%2focticons/19.27.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@primer%2focticons/19.26.0/19.27.0?slim=true) | | [@typescript-eslint/parser](https://typescript-eslint.io/packages/parser) ([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)) | [`8.59.4` → `8.60.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2fparser/8.59.4/8.60.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2fparser/8.60.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2fparser/8.59.4/8.60.0?slim=true) | | [@vitest/eslint-plugin](https://redirect.github.com/vitest-dev/eslint-plugin-vitest) | [`1.6.17` → `1.6.18`](https://renovatebot.com/diffs/npm/@vitest%2feslint-plugin/1.6.17/1.6.18) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2feslint-plugin/1.6.18?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2feslint-plugin/1.6.17/1.6.18?slim=true) | | [dayjs](https://day.js.org) ([source](https://redirect.github.com/iamkun/dayjs)) | [`1.11.20` → `1.11.21`](https://renovatebot.com/diffs/npm/dayjs/1.11.20/1.11.21) | ![age](https://developer.mend.io/api/mc/badges/age/npm/dayjs/1.11.21?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dayjs/1.11.20/1.11.21?slim=true) | | [katex](https://katex.org) ([source](https://redirect.github.com/KaTeX/KaTeX)) | [`0.16.47` → `0.17.0`](https://renovatebot.com/diffs/npm/katex/0.16.47/0.17.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/katex/0.17.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/katex/0.16.47/0.17.0?slim=true) | | [material-icon-theme](https://redirect.github.com/material-extensions/vscode-material-icon-theme/blob/main/README.md) ([source](https://redirect.github.com/material-extensions/vscode-material-icon-theme)) | [`5.34.0` → `5.35.0`](https://renovatebot.com/diffs/npm/material-icon-theme/5.34.0/5.35.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/material-icon-theme/5.35.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/material-icon-theme/5.34.0/5.35.0?slim=true) | | [pnpm](https://pnpm.io) ([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) | [`11.2.1` → `11.4.0`](https://renovatebot.com/diffs/npm/pnpm/11.2.1/11.4.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.4.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.2.1/11.4.0?slim=true) | | [rolldown-license-plugin](https://redirect.github.com/silverwind/rolldown-license-plugin) | [`3.0.7` → `3.0.8`](https://renovatebot.com/diffs/npm/rolldown-license-plugin/3.0.7/3.0.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/rolldown-license-plugin/3.0.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/rolldown-license-plugin/3.0.7/3.0.8?slim=true) | | [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) ([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)) | [`8.59.4` → `8.60.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.59.4/8.60.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.60.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.59.4/8.60.0?slim=true) | | [updates](https://redirect.github.com/silverwind/updates) | [`17.16.13` → `17.17.2`](https://renovatebot.com/diffs/npm/updates/17.16.13/17.17.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/updates/17.17.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/updates/17.16.13/17.17.2?slim=true) | | [vite](https://vite.dev) ([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite)) | [`8.0.13` → `8.0.14`](https://renovatebot.com/diffs/npm/vite/8.0.13/8.0.14) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vite/8.0.14?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/8.0.13/8.0.14?slim=true) | | [vue](https://vuejs.org/) ([source](https://redirect.github.com/vuejs/core)) | [`3.5.34` → `3.5.35`](https://renovatebot.com/diffs/npm/vue/3.5.34/3.5.35) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.35?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.34/3.5.35?slim=true) | | [vue-tsc](https://redirect.github.com/vuejs/language-tools) ([source](https://redirect.github.com/vuejs/language-tools/tree/HEAD/packages/tsc)) | [`3.3.1` → `3.3.2`](https://renovatebot.com/diffs/npm/vue-tsc/3.3.1/3.3.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-tsc/3.3.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-tsc/3.3.1/3.3.2?slim=true) | --- ### Release Notes
eslint-community/eslint-plugin-eslint-comments (@​eslint-community/eslint-plugin-eslint-comments) ### [`v4.7.2`](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/releases/tag/v4.7.2) [Compare Source](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/compare/v4.7.1...v4.7.2) ##### Bug Fixes - **deps:** pin `modern-monaco` version to 0.4.0 ([#​320](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/issues/320)) ([62a2c3a](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/commit/62a2c3a4ee304a8383f170369c9999198d9bdac8)) - **docs:** use `modern-monaco` instead of `monaco-editor` ([#​311](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/issues/311)) ([42919d0](https://redirect.github.com/eslint-community/eslint-plugin-eslint-comments/commit/42919d06d8a221e061de3ec98e35bf508ea2b5d2))
primer/octicons (@​primer/octicons) ### [`v19.27.0`](https://redirect.github.com/primer/octicons/blob/HEAD/CHANGELOG.md#19270) [Compare Source](https://redirect.github.com/primer/octicons/compare/v19.26.0...v19.27.0) ##### Minor Changes - [#​1203](https://redirect.github.com/primer/octicons/pull/1203) [`a69618e4`](https://redirect.github.com/primer/octicons/commit/a69618e4b64988784c9c0a06bbf809a3fa343642) Thanks [@​ericwbailey](https://redirect.github.com/ericwbailey)! - Add flag icon ##### Patch Changes - [#​1212](https://redirect.github.com/primer/octicons/pull/1212) [`02bd1ef8`](https://redirect.github.com/primer/octicons/commit/02bd1ef8d15abffaa45be8e00c5fbc896e276c54) Thanks [@​ericwbailey](https://redirect.github.com/ericwbailey)! - remove hardcoded fill from flag icon
typescript-eslint/typescript-eslint (@​typescript-eslint/parser) ### [`v8.60.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#8600-2026-05-25) [Compare Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.59.4...v8.60.0) This was a version bump only for parser to align it with other projects, there were no code changes. See [GitHub Releases](https://redirect.github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.0) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
vitest-dev/eslint-plugin-vitest (@​vitest/eslint-plugin) ### [`v1.6.18`](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/releases/tag/v1.6.18) [Compare Source](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.17...v1.6.18) #####    🐞 Bug Fixes - Correct `requiresTypeChecking` metadata for four rules  -  by [@​inglec-arista](https://redirect.github.com/inglec-arista) in [#​905](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/issues/905) [(e06a3)](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/commit/e06a3dc) #####     [View changes on GitHub](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.17...v1.6.18)
iamkun/dayjs (dayjs) ### [`v1.11.21`](https://redirect.github.com/iamkun/dayjs/blob/HEAD/CHANGELOG.md#11121-2026-05-26) [Compare Source](https://redirect.github.com/iamkun/dayjs/compare/v1.11.20...v1.11.21) ##### Bug Fixes - preserve unsupported year tokens in format ([#​3015](https://redirect.github.com/iamkun/dayjs/issues/3015)) ([#​3016](https://redirect.github.com/iamkun/dayjs/issues/3016)) ([8fda602](https://redirect.github.com/iamkun/dayjs/commit/8fda602beac5abbc64230ddc49085aa532320f26))
KaTeX/KaTeX (katex) ### [`v0.17.0`](https://redirect.github.com/KaTeX/KaTeX/blob/HEAD/CHANGELOG.md#0170-2026-05-22) [Compare Source](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.47...v0.17.0) ##### Performance Improvements - simplify `defineFunction` to avoid destructuring, improve typing ([#​4222](https://redirect.github.com/KaTeX/KaTeX/issues/4222)) ([fb604e6](https://redirect.github.com/KaTeX/KaTeX/commit/fb604e6ba63e99809e242d37f9c8359209d55431)) ##### BREAKING CHANGES - The internal API for `__defineFunction` changed: you should no longer wrap properties in `props`. #### [0.16.47](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.46...v0.16.47) (2026-05-16) ##### Bug Fixes - correct size of `[` big delimiter ([#​4217](https://redirect.github.com/KaTeX/KaTeX/issues/4217)) ([7ba0027](https://redirect.github.com/KaTeX/KaTeX/commit/7ba0027d2f04abddd3b215362f867ab8260b09d7)), closes [#​4215](https://redirect.github.com/KaTeX/KaTeX/issues/4215) #### [0.16.46](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.45...v0.16.46) (2026-05-13) ##### Bug Fixes - preserve math font in some styling commands ([#​4214](https://redirect.github.com/KaTeX/KaTeX/issues/4214)) ([e9ee046](https://redirect.github.com/KaTeX/KaTeX/commit/e9ee0464ddb31da9bf9649eeb70e52236e7a974a)), closes [#​4213](https://redirect.github.com/KaTeX/KaTeX/issues/4213) #### [0.16.45](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.44...v0.16.45) (2026-04-05) ##### Bug Fixes - wrap vcenter mpadded in mrow for valid MathML ([#​4193](https://redirect.github.com/KaTeX/KaTeX/issues/4193)) ([ee66b78](https://redirect.github.com/KaTeX/KaTeX/commit/ee66b78d24340edbbd05b08a4a429ce9ed158b25)), closes [#​4078](https://redirect.github.com/KaTeX/KaTeX/issues/4078) #### [0.16.44](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.43...v0.16.44) (2026-03-27) ##### Bug Fixes - remove extra \jot space at bottom of align/gather/etc. ([#​4184](https://redirect.github.com/KaTeX/KaTeX/issues/4184)) ([3870ee9](https://redirect.github.com/KaTeX/KaTeX/commit/3870ee913e27fdde7bce244e4c6c5d63e2b28a62)) #### [0.16.43](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.42...v0.16.43) (2026-03-26) ##### Bug Fixes - use makeEm() consistently to truncate long CSS decimals ([#​4181](https://redirect.github.com/KaTeX/KaTeX/issues/4181)) ([0967dcc](https://redirect.github.com/KaTeX/KaTeX/commit/0967dcc0278f20d4501a93f01c7343c70abb3fcd)) #### [0.16.42](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.41...v0.16.42) (2026-03-24) ##### Features - \underbracket and \overbracket ([#​4147](https://redirect.github.com/KaTeX/KaTeX/issues/4147)) ([5be9abb](https://redirect.github.com/KaTeX/KaTeX/commit/5be9abb0b4d687a2a196b8adf9b5b9deeb60f7bc)) #### [0.16.41](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.40...v0.16.41) (2026-03-24) ##### Bug Fixes - \sout in text mode ([#​4173](https://redirect.github.com/KaTeX/KaTeX/issues/4173)) ([e748578](https://redirect.github.com/KaTeX/KaTeX/commit/e748578b63e07ad30d5e404e60b04e5e794c0a5a)) #### [0.16.40](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.39...v0.16.40) (2026-03-20) ##### Bug Fixes - **css:** specify position: relative for .katex ([#​4170](https://redirect.github.com/KaTeX/KaTeX/issues/4170)) ([020f0d8](https://redirect.github.com/KaTeX/KaTeX/commit/020f0d89567d59229bac5fc8d8f5832a9508a85f)) #### [0.16.39](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.38...v0.16.39) (2026-03-19) ##### Bug Fixes - middle dot in text mode ([#​4169](https://redirect.github.com/KaTeX/KaTeX/issues/4169)) ([edb45b0](https://redirect.github.com/KaTeX/KaTeX/commit/edb45b0b17c7b33349ce5142fe39156da05cb4d8)), closes [#​3641](https://redirect.github.com/KaTeX/KaTeX/issues/3641) #### [0.16.38](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.37...v0.16.38) (2026-03-08) ##### Bug Fixes - accent skew mixed with font specifiers ([#​4159](https://redirect.github.com/KaTeX/KaTeX/issues/4159)) ([aea3375](https://redirect.github.com/KaTeX/KaTeX/commit/aea33758d6c98896017007d0244885301773856a)), closes [#​4121](https://redirect.github.com/KaTeX/KaTeX/issues/4121) #### [0.16.37](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.36...v0.16.37) (2026-03-06) ##### Bug Fixes - negative-width `\hphantom` and symmetric `\smash` ([#​4153](https://redirect.github.com/KaTeX/KaTeX/issues/4153)) ([d4799ca](https://redirect.github.com/KaTeX/KaTeX/commit/d4799cae585d909e2a4e3dedbebdc2f142998ca9)) #### [0.16.36](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.35...v0.16.36) (2026-03-06) ##### Bug Fixes - contrib esm bloat ([#​4157](https://redirect.github.com/KaTeX/KaTeX/issues/4157)) ([2bde1ad](https://redirect.github.com/KaTeX/KaTeX/commit/2bde1adab2a23f61519145923329c915b04d4778)) #### [0.16.35](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.34...v0.16.35) (2026-03-05) ##### Bug Fixes - version number regression ([#​4155](https://redirect.github.com/KaTeX/KaTeX/issues/4155)) ([db26b73](https://redirect.github.com/KaTeX/KaTeX/commit/db26b733805f2d0d71e82596475b313c8706557e)) #### [0.16.34](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.33...v0.16.34) (2026-03-05) ##### Bug Fixes - emoji with variation selector ([#​4151](https://redirect.github.com/KaTeX/KaTeX/issues/4151)) ([c2606e5](https://redirect.github.com/KaTeX/KaTeX/commit/c2606e5db91ae199ee1ff0c8c2f7f9f70fcf589b)) #### [0.16.33](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.32...v0.16.33) (2026-02-23) ##### Bug Fixes - **scss:** forward variables to fonts module ([#​4146](https://redirect.github.com/KaTeX/KaTeX/issues/4146)) ([9349a64](https://redirect.github.com/KaTeX/KaTeX/commit/9349a64a051ca408da713baf061e32ade80ed22a)) #### [0.16.32](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.31...v0.16.32) (2026-02-22) ##### Bug Fixes - italic separation in \mathnormal ([#​4143](https://redirect.github.com/KaTeX/KaTeX/issues/4143)) ([71305a0](https://redirect.github.com/KaTeX/KaTeX/commit/71305a05140ca6203092bfdc14f689168b26ab8c)) #### [0.16.31](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.30...v0.16.31) (2026-02-22) ##### Bug Fixes - `\*frac` sizing ([#​4137](https://redirect.github.com/KaTeX/KaTeX/issues/4137)) ([ef51f18](https://redirect.github.com/KaTeX/KaTeX/commit/ef51f18ded4ab9ba54ba750f2866241c4676c41c)) #### [0.16.30](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.29...v0.16.30) (2026-02-22) ##### Bug Fixes - no line breaks after `\not` ([#​4140](https://redirect.github.com/KaTeX/KaTeX/issues/4140)) ([2d1ba86](https://redirect.github.com/KaTeX/KaTeX/commit/2d1ba86143bd45540d5a773cfa456081318f3f33)) #### [0.16.29](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.28...v0.16.29) (2026-02-22) ##### Bug Fixes - `\imath` and other `\html@mathml` macros in arguments ([#​4139](https://redirect.github.com/KaTeX/KaTeX/issues/4139)) ([a850cce](https://redirect.github.com/KaTeX/KaTeX/commit/a850cce7ccbf95a0b187313d1e54d8d40dfc7273)) #### [0.16.28](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.27...v0.16.28) (2026-01-25) ##### Bug Fixes - **type:** add missing types definition path to package.json ([#​4125](https://redirect.github.com/KaTeX/KaTeX/issues/4125)) ([0ef8921](https://redirect.github.com/KaTeX/KaTeX/commit/0ef8921d189346b0ff8f84a77f7f552349b76893)) #### [0.16.27](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.26...v0.16.27) (2025-12-07) ##### Features - support equals sign and surrounding whitespace in \htmlData attribute values ([#​4112](https://redirect.github.com/KaTeX/KaTeX/issues/4112)) ([c77aaec](https://redirect.github.com/KaTeX/KaTeX/commit/c77aaec00c766f5bb02e332a1dc416b82a65fe8f)) #### [0.16.26](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.25...v0.16.26) (2025-12-07) ##### Bug Fixes - \mathop followed by integral symbol ([6fbad18](https://redirect.github.com/KaTeX/KaTeX/commit/6fbad18857351e4d2a88ed3e3348bd76caad9be3)) #### [0.16.25](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.24...v0.16.25) (2025-10-13) ##### Features - **css:** provide `katex-swap.css` that uses `font-display: swap` ([#​3940](https://redirect.github.com/KaTeX/KaTeX/issues/3940)) ([b3f9ce6](https://redirect.github.com/KaTeX/KaTeX/commit/b3f9ce691e89a52dea7ec8f10cc6ed4ddc8fc161)), closes [#​2242](https://redirect.github.com/KaTeX/KaTeX/issues/2242) #### [0.16.24](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.23...v0.16.24) (2025-10-12) ##### Features - support hex colors with alpha ([#​4090](https://redirect.github.com/KaTeX/KaTeX/issues/4090)) ([8c9b306](https://redirect.github.com/KaTeX/KaTeX/commit/8c9b3063965acc0d6e6a0b6df4d051169de9e1a9)), closes [#​4067](https://redirect.github.com/KaTeX/KaTeX/issues/4067) [#fA6](https://redirect.github.com/KaTeX/KaTeX/issues/fA6) [#fA6f1](https://redirect.github.com/KaTeX/KaTeX/issues/fA6f1) #### [0.16.23](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.22...v0.16.23) (2025-10-03) ##### Bug Fixes - Support `\def` with arguments via `macros` option ([#​4087](https://redirect.github.com/KaTeX/KaTeX/issues/4087)) ([80a8158](https://redirect.github.com/KaTeX/KaTeX/commit/80a815856a8c26d78b3669e9c05fff00efe82247)) #### [0.16.22](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.21...v0.16.22) (2025-04-09) ##### Bug Fixes - \relax in base or exponent of super/subscript ([#​4045](https://redirect.github.com/KaTeX/KaTeX/issues/4045)) ([1f43c84](https://redirect.github.com/KaTeX/KaTeX/commit/1f43c84a175fb689f8c8d1d72b1e8b896a8b43d1)) #### [0.16.21](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.20...v0.16.21) (2025-01-17) ##### Bug Fixes - escape \htmlData attribute name ([57914ad](https://redirect.github.com/KaTeX/KaTeX/commit/57914ad91eff401357f44bf364b136d37eba04f8)) #### [0.16.20](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.19...v0.16.20) (2025-01-12) ##### Bug Fixes - \providecommand does not overwrite existing macro ([#​4000](https://redirect.github.com/KaTeX/KaTeX/issues/4000)) ([6d30fe4](https://redirect.github.com/KaTeX/KaTeX/commit/6d30fe47b06f9da9b836fe518d5cbbecf6a6a3a1)), closes [#​3928](https://redirect.github.com/KaTeX/KaTeX/issues/3928) #### [0.16.19](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.18...v0.16.19) (2024-12-29) ##### Bug Fixes - **types:** improve `strict` function type ([#​4009](https://redirect.github.com/KaTeX/KaTeX/issues/4009)) ([4228b4e](https://redirect.github.com/KaTeX/KaTeX/commit/4228b4eb529b8e35def66cc6e4fa467383b98c86)) #### [0.16.18](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.17...v0.16.18) (2024-12-18) ##### Bug Fixes - Actually publish TypeScript type definitions ([#​4008](https://redirect.github.com/KaTeX/KaTeX/issues/4008)) ([629b873](https://redirect.github.com/KaTeX/KaTeX/commit/629b87354fdfc04a3769f09b69f6bbadebcb9ae8)) #### [0.16.17](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.16...v0.16.17) (2024-12-17) ##### Bug Fixes - MathML combines multidigit numbers with sup/subscript, comma separators, and multicharacter text when outputting to DOM ([#​3999](https://redirect.github.com/KaTeX/KaTeX/issues/3999)) ([7d79e22](https://redirect.github.com/KaTeX/KaTeX/commit/7d79e220f465c42d4334dc95f1c41e333667e168)), closes [#​3995](https://redirect.github.com/KaTeX/KaTeX/issues/3995) #### [0.16.16](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.15...v0.16.16) (2024-12-17) ##### Features - ESM exports, TypeScript types ([#​3992](https://redirect.github.com/KaTeX/KaTeX/issues/3992)) ([ea9c173](https://redirect.github.com/KaTeX/KaTeX/commit/ea9c173a0de953b49b2ce5d131e88b785f5dffa1)) #### [0.16.15](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.14...v0.16.15) (2024-12-09) ##### Features - italic sans-serif in math mode via `\mathsfit` command ([#​3998](https://redirect.github.com/KaTeX/KaTeX/issues/3998)) ([2218901](https://redirect.github.com/KaTeX/KaTeX/commit/22189018b63c9312ec4ad126804514a7390d60b5)) #### [0.16.14](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.13...v0.16.14) (2024-12-08) ##### Features - \dddot and \ddddot support ([#​3834](https://redirect.github.com/KaTeX/KaTeX/issues/3834)) ([bda35cd](https://redirect.github.com/KaTeX/KaTeX/commit/bda35cdb0a6bbbc52dd27c79e4d984688be3b745)), closes [#​2744](https://redirect.github.com/KaTeX/KaTeX/issues/2744) #### [0.16.13](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.12...v0.16.13) (2024-12-08) ##### Bug Fixes - `\vdots` and `\rule` support in text mode ([#​3997](https://redirect.github.com/KaTeX/KaTeX/issues/3997)) ([0e08352](https://redirect.github.com/KaTeX/KaTeX/commit/0e0835262345d991df61a435800a16b069a4d5c7)), closes [#​3990](https://redirect.github.com/KaTeX/KaTeX/issues/3990) #### [0.16.12](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.11...v0.16.12) (2024-12-08) ##### Features - **css:** configurable margin for display math ([#​3638](https://redirect.github.com/KaTeX/KaTeX/issues/3638)) ([3405001](https://redirect.github.com/KaTeX/KaTeX/commit/3405001225b8ee0cf8b35b2e3a6c1fa2191e5fef)) #### [0.16.11](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.10...v0.16.11) (2024-07-02) ##### Features - add \emph ([#​3963](https://redirect.github.com/KaTeX/KaTeX/issues/3963)) ([9f34da4](https://redirect.github.com/KaTeX/KaTeX/commit/9f34da4b3cf228a7af8134c394394d780a089f2b)), closes [#​3566](https://redirect.github.com/KaTeX/KaTeX/issues/3566) #### [0.16.10](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.9...v0.16.10) (2024-03-24) ##### Bug Fixes - \edef bypassing maxExpand via exponential blowup ([e88b4c3](https://redirect.github.com/KaTeX/KaTeX/commit/e88b4c357f978b1bca8edfe3297f0aa309bcbe34)) - escape \includegraphics src and alt ([c5897fc](https://redirect.github.com/KaTeX/KaTeX/commit/c5897fcd1f73da9612a53e6b5544f1d776e17770)) - force protocol to be lowercase for better protocol filtering ([fc5af64](https://redirect.github.com/KaTeX/KaTeX/commit/fc5af64183a3ceb9be9d1c23a275999a728593de)), closes [/datatracker.ietf.org/doc/html/rfc3986#section-3](https://redirect.github.com//datatracker.ietf.org/doc/html/rfc3986/issues/section-3) - maxExpand limit with Unicode sub/superscripts ([085e21b](https://redirect.github.com/KaTeX/KaTeX/commit/085e21b5da05414efefa932570e7201a7c70e5b2)) #### [0.16.9](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.8...v0.16.9) (2023-10-02) ##### Features - Support bold Fraktur ([#​3777](https://redirect.github.com/KaTeX/KaTeX/issues/3777)) ([240d5ae](https://redirect.github.com/KaTeX/KaTeX/commit/240d5aede915e0303929a9328745b1060e12004a)) #### [0.16.8](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.7...v0.16.8) (2023-06-24) ##### Features - expose error length and raw error message on ParseError ([#​3820](https://redirect.github.com/KaTeX/KaTeX/issues/3820)) ([710774a](https://redirect.github.com/KaTeX/KaTeX/commit/710774aaebb38f43b1ec51c159fe9b9520c91424)) #### [0.16.7](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.6...v0.16.7) (2023-04-28) ##### Bug Fixes - **docs/support\_table.md:** delete redundant "varPsi" ([#​3814](https://redirect.github.com/KaTeX/KaTeX/issues/3814)) ([33a1b98](https://redirect.github.com/KaTeX/KaTeX/commit/33a1b98710c880d2d4a67aa0048f027a94b85702)) #### [0.16.6](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.5...v0.16.6) (2023-04-17) ##### Bug Fixes - Support `\let` via `macros` option ([#​3738](https://redirect.github.com/KaTeX/KaTeX/issues/3738)) ([bdb0be2](https://redirect.github.com/KaTeX/KaTeX/commit/bdb0be201794d22adaee05438b07a2830efea9da)), closes [#​3737](https://redirect.github.com/KaTeX/KaTeX/issues/3737) [#​3737](https://redirect.github.com/KaTeX/KaTeX/issues/3737) #### [0.16.5](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.4...v0.16.5) (2023-04-17) ##### Features - \_\_defineFunction API exposing internal defineFunction ([#​3805](https://redirect.github.com/KaTeX/KaTeX/issues/3805)) ([c7b1f84](https://redirect.github.com/KaTeX/KaTeX/commit/c7b1f84b7801a29dffdfa3db0ff35de289db80c0)), closes [#​3756](https://redirect.github.com/KaTeX/KaTeX/issues/3756) #### [0.16.4](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.3...v0.16.4) (2022-12-07) ##### Bug Fixes - space should prevent optional argument to \ ([#​3746](https://redirect.github.com/KaTeX/KaTeX/issues/3746)) ([a0deb34](https://redirect.github.com/KaTeX/KaTeX/commit/a0deb3410fd92340556fc4c9edb8ab586077e5bf)), closes [#​3745](https://redirect.github.com/KaTeX/KaTeX/issues/3745) #### [0.16.3](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.2...v0.16.3) (2022-10-22) ##### Bug Fixes - \hline after \cr ([#​3735](https://redirect.github.com/KaTeX/KaTeX/issues/3735)) ([ebf6bf5](https://redirect.github.com/KaTeX/KaTeX/commit/ebf6bf5b50a98ac6c5aca1896c0a6ba985c1c91c)), closes [#​3734](https://redirect.github.com/KaTeX/KaTeX/issues/3734) #### [0.16.2](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.1...v0.16.2) (2022-08-29) ##### Bug Fixes - **auto-render:** concatenate content of successive text nodes ([#​3422](https://redirect.github.com/KaTeX/KaTeX/issues/3422)) ([4d3fdd8](https://redirect.github.com/KaTeX/KaTeX/commit/4d3fdd8647a1c320dc7bcb9c9ea2af81379f700d)) - Implement \pmb via CSS text-shadow ([#​3505](https://redirect.github.com/KaTeX/KaTeX/issues/3505)) ([176552a](https://redirect.github.com/KaTeX/KaTeX/commit/176552a69183d71425b491d4cc2fa1d462a1246a)) #### [0.16.1](https://redirect.github.com/KaTeX/KaTeX/compare/v0.16.0...v0.16.1) (2022-08-28) ##### Bug Fixes - Use SVGs for some stacked delims ([#​3686](https://redirect.github.com/KaTeX/KaTeX/issues/3686)) ([8a65a2e](https://redirect.github.com/KaTeX/KaTeX/commit/8a65a2e1fd69ffeee2fac62229f9f05ebf6afd45))
material-extensions/vscode-material-icon-theme (material-icon-theme) ### [`v5.35.0`](https://redirect.github.com/material-extensions/vscode-material-icon-theme/blob/HEAD/CHANGELOG.md#v5350) [Compare Source](https://redirect.github.com/material-extensions/vscode-material-icon-theme/compare/v5.34.0...v5.35.0) [compare changes](https://redirect.github.com/material-extensions/vscode-material-icon-theme/compare/v5.34.0...v5.35.0) ##### 🚀 Enhancements - Add CAD file extensions to 3d icon mapping ([#​3436](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3436)) - Add tsdown icon ([#​3418](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3418)) - Add new icons for mrpack ([#​3439](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3439)) - Add support for vercel.ts icon (typed Vercel configuration) ([#​3441](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3441)) - Support jxl image file type ([#​3444](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3444)) - Add uiua file icon ([#​3408](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3408)) - Add folder associations for rust/cargo projects ([#​3447](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3447)) - **icon:** Add zed folder icon ([#​3442](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3442)) - **icon:** Add redis icon ([#​3450](https://redirect.github.com/material-extensions/vscode-material-icon-theme/pull/3450)) - Add more unit tests for writefile helper function ([9e4c98aa](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/9e4c98aa)) - Include language IDs into the file icons ([c9a9d2ed](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/c9a9d2ed)) - Update dependencies ([d7274c71](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/d7274c71)) ##### 🩹 Fixes - Add rootDir to tsconfig.declarations.json for TypeScript 6 ([4f7f49e9](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/4f7f49e9)) - Correct typos in CONTRIBUTING.md ([4de4acf7](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/4de4acf7)) ##### 💅 Refactors - **core:** Rewrite toTitleCase for clarity and add tests ([33c0e614](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/33c0e614)) - Remove duplicate toTitleCase, consolidate imports ([e247951d](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/e247951d)) ##### 🏡 Chore - Improve release process ([b959b483](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/b959b483)) ##### ✅ Tests - **core:** Add comprehensive tests for object helpers ([57f476c5](https://redirect.github.com/material-extensions/vscode-material-icon-theme/commit/57f476c5)) ##### ❤️ Contributors - Philipp Kief ([@​PKief](https://redirect.github.com/PKief)) - Sayan Shankhari ([@​SayanShankhari](https://redirect.github.com/SayanShankhari)) - Tymon Marek ([@​TymonMarek](https://redirect.github.com/TymonMarek)) - Unteksi-ozar ([@​Unteksi-ozar](https://redirect.github.com/Unteksi-ozar)) - 锐冰 SharpIce ([@​SharpIceX](https://redirect.github.com/SharpIceX)) - El Mahdi Bennajah ([@​bennajah](https://redirect.github.com/bennajah)) - Glitch714 ([@​glitchplaysgames714](https://redirect.github.com/glitchplaysgames714)) - Andrin Haldner ([@​AHaldner](https://redirect.github.com/AHaldner)) - Kaden Gruizenga ([@​kgruiz](https://redirect.github.com/kgruiz))
pnpm/pnpm (pnpm) ### [`v11.4.0`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1140) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.3.0...v11.4.0) ##### Minor Changes - Treat tarball-integrity mismatches against the lockfile as a hard failure by default. Previously, `pnpm install` (non-frozen) would log `ERR_PNPM_TARBALL_INTEGRITY`, silently re-resolve from the registry, and overwrite the locked integrity — which meant a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile. `pnpm install` now exits with `ERR_PNPM_TARBALL_INTEGRITY` and a hint pointing at the new opt-in flag. The only opt-in is **`pnpm install --update-checksums`** — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable. `--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. - `pnpm runtime set ` now saves the runtime to `devEngines.runtime` by default instead of `engines.runtime`. Pass `--save-prod` (or `-P`) to save it to `engines.runtime` instead [#​11948](https://redirect.github.com/pnpm/pnpm/issues/11948). ##### Patch Changes - Fix a credential disclosure issue where an unscoped `_authToken` (or `_auth`, or `username` + `_password`, or `tokenHelper`) defined in one source — `~/.npmrc`, `~/.config/pnpm/auth.ini`, a workspace `.npmrc`, CLI flags, etc. — would be sent as an `Authorization` header to whichever registry a different (potentially untrusted) source named. The same fix extends to client TLS credentials (`cert`, `key`) so they aren't presented to a registry their author didn't choose. pnpm now rewrites each unscoped per-registry setting (`_authToken`, `_auth`, `username`, `_password`, `tokenHelper`, `cert`, `key`) to its URL-scoped form at load time, using the `registry=` value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding `registry=` therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. `ca`/`cafile` are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally. Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since `npm@9`, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped (e.g. `//registry.example.com/:_authToken=...` or `//registry.example.com/:cert=...`). `@pnpm/network.auth-header`: removed the `defaultRegistry` parameter from `createGetAuthHeaderByURI` and `getAuthHeadersFromCreds`. Now that credentials are URL-scoped at load time, the merged `configByUri` never contains the empty-string "default registry" placeholder slot, so re-keying it onto the merged default registry is no longer needed. - Fix `pnpm deploy` crashing with `ENOENT: ... lstat '/node_modules'` when `configDependencies` declares pacquet (`pacquet` or `@pnpm/pacquet`). The deploy directory never installs config dependencies, so the install engine they designate isn't on disk to invoke; the nested install now skips them. - Reject git resolutions whose `commit` field is not a 40-character hexadecimal SHA before invoking `git`. A malicious lockfile could otherwise smuggle a value such as `--upload-pack=` through `git fetch` / `git checkout`, which on SSH or local-file transports executes the supplied command. - Limit concurrent project manifest reads while listing large workspaces to avoid `EMFILE` errors. - Reject patch files whose `diff --git` headers reference paths outside the patched package directory. Previously a malicious `.patch` file added via a pull request could write, delete, or rename arbitrary files reachable by the user running `pnpm install`. - Improve the log message that pnpm prints after auto-adding entries to `minimumReleaseAgeExclude` when `minimumReleaseAge` is set without `minimumReleaseAgeStrict`. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to set `minimumReleaseAgeStrict` to `true` if they want these updates gated behind a prompt instead [#​11747](https://redirect.github.com/pnpm/pnpm/issues/11747). - Reject dependency aliases that contain path-traversal segments (such as `@x/../../../../../.git/hooks`) when reading them from a package manifest or symlinking them into `node_modules`. A malicious registry package could otherwise use a transitive dependency key to make `pnpm install` create symlinks at attacker-chosen paths outside the intended `node_modules` directory. - Reject `pnpm-lock.yaml` entries whose remote tarball `resolution:` block is missing the `integrity` field. Previously the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes, so an attacker who could both alter the lockfile (e.g. via a pull request that strips `integrity:`) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under `--frozen-lockfile`. pnpm now fails closed at lockfile-read time with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. Git-hosted tarballs (`gitHosted: true` or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `file:` tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes. - Validate `devEngines.runtime` and `engines.runtime` version ranges for `node`, `deno`, and `bun` when `onFail` is set to `error` or `warn`. Previously these settings only had an effect with `onFail: 'download'` — the `error` and `warn` modes silently did nothing [#​11818](https://redirect.github.com/pnpm/pnpm/issues/11818). Violations now throw `ERR_PNPM_BAD_RUNTIME_VERSION`. - Require provenance before treating trusted publisher metadata as the strongest trust evidence. ### [`v11.3.0`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1130) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.2.2...v11.3.0) ##### Minor Changes - Added `pnpm stage` with `publish`, `list`, `view`, `approve`, `reject`, and `download` subcommands for npm staged publishing. - Added a new setting `trustLockfile`. When `true`, `pnpm install` skips the supply-chain verification pass that re-applies `minimumReleaseAge` / `trustPolicy='no-downgrade'` to every entry in the loaded lockfile. The install treats the lockfile as already-trusted — useful for closed-source projects where every commit comes from a trusted author. Defaults to `false`; verification stays on by default. Set in `pnpm-workspace.yaml`. Also cut the memory footprint of the verification pass itself: the per-(registry, name) trust-meta cache previously retained the full packument — dependency graphs, scripts, README, and per-version manifests — for the entire install. On large workspaces (`~4k` lockfile entries with `minimumReleaseAge` + `trustPolicy: no-downgrade` enabled) this could OOM CI runners with a 2GB heap cap. The cache now stores only the fields the trust check actually reads (`time`, per-version `_npmUser.trustedPublisher`, `dist.attestations.provenance`). The abbreviated-metadata cache is similarly projected to just the package-level `modified` field and the set of currently-listed version names. Fixes [#​11860](https://redirect.github.com/pnpm/pnpm/issues/11860). - Implemented `pnpm pkg` command natively, following `npm pkg` standards. - Implemented `pnpm repo` command natively, following `npm repo` standards. - Implemented `pnpm set-script` (alias `ss`) natively. Adds or updates an entry in the `scripts` field of the project manifest, supporting `package.json`, `package.json5`, and `package.yaml` formats. - Add a `skip-manifest-obfuscation` option for `pnpm pack` and `pnpm publish`. When enabled, the original `packageManager` field and publish lifecycle scripts are kept in the packed/published manifest instead of being stripped. The pnpm-specific `pnpm` field continues to be omitted. ##### Patch Changes - Fixed `pnpm dlx` failing with `ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND` when the installed package's CAS slot is missing its `package.json`. Observed in the wild for `pnpm dlx node@runtime:` when the GVS slot was populated without the synthesized manifest runtime archives need (they don't ship a `package.json` of their own, so the synthesized one is the only way it gets there; an existing slot from an earlier code path that skipped the synthesis stays incomplete). The bin link itself is wired up from the resolution and remains valid, so `dlx` now falls back to the scopeless package name when the slot's manifest is unreadable — for single-bin packages (the dlx common case, including every `runtime:` spec) this matches what `manifest.bin` would have named. Multi-bin packages already require `--package= ` to disambiguate and don't enter this code path. - Fixed non-determinism in `pnpm dedupe` and `pnpm install` when a dependency graph contains packages with transitive peer dependencies on each other (e.g. `@aws-sdk/client-sts` and `@aws-sdk/client-sso-oidc`) and `auto-install-peers` is enabled. The lockfile no longer flips between two equally-valid forms across consecutive runs. The root cause was that `resolveDependencies` pushed onto its `pkgAddresses` / `postponedResolutionsQueue` arrays from inside `Promise.all`-spawned callbacks, so completion-order timing leaked into the array order and downstream cyclic-peer suffix assignment. Fixes [#​8155](https://redirect.github.com/pnpm/pnpm/issues/8155). - Fixed a regression introduced by [#​11711](https://redirect.github.com/pnpm/pnpm/pull/11711) where `pnpm add ` (and any other wanted-dependency whose alias can't be parsed from the user-supplied spec, e.g. tarball URLs or `pnpm/test-git-fetch#sha`) was silently dropped from the manifest update and from `pendingBuilds`. The alias-keyed lookup added in that PR couldn't find a `wantedDependency` whose `alias` was `undefined` at parse time but resolved to a package name only after fetching, so the entry never made it into `specsToUpsert`. Restored the original index-based pairing between `directDependencies` and `wantedDependencies`; the catalog-protocol preservation that PR was originally fixing is unaffected because it's driven by `rdd.catalogLookup.userSpecifiedBareSpecifier`, not by the lookup. Fixes the three `rebuilds dependencies` / `rebuilds specific dependencies` / `rebuild with pending option` failures in `building/commands/test/build/index.ts`. - Fixed `pnpm add --config` leaving orphan entries in `pnpm-lock.env.yaml` (the optional subdependencies of the previously resolved version of the updated config dependency). ### [`v11.2.2`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1122) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.2.1...v11.2.2) ##### Patch Changes - When the install engine is delegated to pacquet via `configDependencies`, the user's CLI flags passed to `pnpm install` (e.g. `--no-runtime`, `--prod`, `--dev`, `--no-optional`, `--node-linker`, `--cpu`/`--os`/`--libc`, `--offline`, `--prefer-offline`) are now forwarded to pacquet's `install` subcommand verbatim. Previously pacquet was invoked with a fixed argument list, so flags like `--no-runtime` were silently dropped. Flag forwarding is gated on the command being `install`/`i`; `add`, `update`, and `dedupe` still don't forward (their flag surface doesn't line up with pacquet's `install`). - Fixed `pnpm up` (and `pnpm add` / `pnpm remove`) failing with `pacquet_package_manager::outdated_lockfile` when pacquet is declared in `configDependencies`. pnpm now passes `--ignore-manifest-check` to pacquet so its `--frozen-lockfile` check doesn't fire against the (pre-mutation) `package.json` pnpm hasn't written yet [#​11797](https://redirect.github.com/pnpm/pnpm/issues/11797). Requires a pacquet release that supports the flag — bump `PACQUET_VERSION` in the e2e tests once it ships.
silverwind/rolldown-license-plugin (rolldown-license-plugin) ### [`v3.0.8`](https://redirect.github.com/silverwind/rolldown-license-plugin/releases/tag/3.0.8) [Compare Source](https://redirect.github.com/silverwind/rolldown-license-plugin/compare/3.0.7...3.0.8) - update deps (silverwind) - swap path.join for template concat in I/O hot paths (silverwind) - simplify license sort and allow-branch control flow (silverwind)
typescript-eslint/typescript-eslint (typescript-eslint) ### [`v8.60.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/typescript-eslint/CHANGELOG.md#8600-2026-05-25) [Compare Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.59.4...v8.60.0) This was a version bump only for typescript-eslint to align it with other projects, there were no code changes. See [GitHub Releases](https://redirect.github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.0) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
silverwind/updates (updates) ### [`v17.17.2`](https://redirect.github.com/silverwind/updates/releases/tag/17.17.2) [Compare Source](https://redirect.github.com/silverwind/updates/compare/17.17.1...17.17.2) - Read github env tokens lazily instead of at import (silverwind) ### [`v17.17.1`](https://redirect.github.com/silverwind/updates/releases/tag/17.17.1) [Compare Source](https://redirect.github.com/silverwind/updates/compare/17.17.0...17.17.1) - Scope GitHub token fallback to GitHub hosts only (silverwind) ### [`v17.17.0`](https://redirect.github.com/silverwind/updates/releases/tag/17.17.0) [Compare Source](https://redirect.github.com/silverwind/updates/compare/17.16.13...17.17.0) - update deps (silverwind) - Add per-package `overrides` config option ([#​140](https://redirect.github.com/silverwind/updates/issues/140)) (silverwind) - fix three bugs in range/tag handling (silverwind)
vitejs/vite (vite) ### [`v8.0.14`](https://redirect.github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-8014-2026-05-21-small) [Compare Source](https://redirect.github.com/vitejs/vite/compare/v8.0.13...v8.0.14) ##### Features - update rolldown to 1.0.2 ([#​22484](https://redirect.github.com/vitejs/vite/issues/22484)) ([96efc88](https://redirect.github.com/vitejs/vite/commit/96efc88570b6a6ddf1a910f106920cbac07b3cf0)) ##### Bug Fixes - **deps:** update all non-major dependencies ([#​22471](https://redirect.github.com/vitejs/vite/issues/22471)) ([98b8163](https://redirect.github.com/vitejs/vite/commit/98b81632139d51820f82036e58d6fbbf122b77b3)) - **dev:** handle errors when sending messages to vite server ([#​22450](https://redirect.github.com/vitejs/vite/issues/22450)) ([e8e9a34](https://redirect.github.com/vitejs/vite/commit/e8e9a34dcf2540139de558a10187630884d10217)) - **html:** handle trailing slash paths in transformIndexHtml ([#​22480](https://redirect.github.com/vitejs/vite/issues/22480)) ([5d94d1b](https://redirect.github.com/vitejs/vite/commit/5d94d1bffdb2a15de9341194d89baec86ce1f693)) - **optimizer:** pass oxc jsx options to transformSync in dependency scan ([#​22342](https://redirect.github.com/vitejs/vite/issues/22342)) ([b3132da](https://redirect.github.com/vitejs/vite/commit/b3132dacea9c6e0cf526cd9f0f09d850f577c262)) ##### Miscellaneous Chores - **deps:** update rolldown-related dependencies ([#​22470](https://redirect.github.com/vitejs/vite/issues/22470)) ([7cb728e](https://redirect.github.com/vitejs/vite/commit/7cb728eb629cc677661f1bc52a044ffc0b87fc7f)) - remove irrelevant commits from changelog ([2c69495](https://redirect.github.com/vitejs/vite/commit/2c69495f250edf01132d4a20128de19dbe836086)) ##### Code Refactoring - **glob:** do not rewrite import path for absolute base ([#​22310](https://redirect.github.com/vitejs/vite/issues/22310)) ([0ae2844](https://redirect.github.com/vitejs/vite/commit/0ae2844ab6d6d1ccf78a2975b8132769fc35b302)) ##### Tests - **css:** sass does not use main field ([#​22449](https://redirect.github.com/vitejs/vite/issues/22449)) ([ebf39a0](https://redirect.github.com/vitejs/vite/commit/ebf39a04329ddc6ba765e006a5d463680a952270))
vuejs/core (vue) ### [`v3.5.35`](https://redirect.github.com/vuejs/core/blob/HEAD/CHANGELOG.md#3535-2026-05-27) [Compare Source](https://redirect.github.com/vuejs/core/compare/v3.5.34...v3.5.35) ##### Bug Fixes - **compiler-core:** avoid double processing v-for keys with v-memo ([#​14861](https://redirect.github.com/vuejs/core/issues/14861)) ([34a0ded](https://redirect.github.com/vuejs/core/commit/34a0ded4d27289a8f227462bd35b6341a4b51831)), closes [#​14859](https://redirect.github.com/vuejs/core/issues/14859) - **compiler-sfc:** resolve top-level exports from files registered as global types ([#​14805](https://redirect.github.com/vuejs/core/issues/14805)) ([3d077f2](https://redirect.github.com/vuejs/core/commit/3d077f26e33510f2ba001d14142ba76a1414dfff)), closes [nuxt/nuxt#33694](https://redirect.github.com/nuxt/nuxt/issues/33694) - **runtime-core:** avoid repeated hydration mismatch checks ([#​14857](https://redirect.github.com/vuejs/core/issues/14857)) ([170fc95](https://redirect.github.com/vuejs/core/commit/170fc95eb64b97024dcb3df770557065e2919aa8)), closes [#​14855](https://redirect.github.com/vuejs/core/issues/14855) - **runtime-core:** skip idle persisted transition hooks in keep-alive moves ([#​14865](https://redirect.github.com/vuejs/core/issues/14865)) ([80fc139](https://redirect.github.com/vuejs/core/commit/80fc139f90513943f1d0da20d353feec8a9ec894)), closes [#​14031](https://redirect.github.com/vuejs/core/issues/14031) - **server-renderer:** propagate sync errors from `ssrRenderSuspense` ([#​14804](https://redirect.github.com/vuejs/core/issues/14804)) ([4760997](https://redirect.github.com/vuejs/core/commit/47609975e294fbcc8017b6d68c9be38fa5508f36)), closes [nuxt/nuxt#28162](https://redirect.github.com/nuxt/nuxt/issues/28162) - **teleport:** skip child unmount when pending mount discarded ([#​14876](https://redirect.github.com/vuejs/core/issues/14876)) ([#​14877](https://redirect.github.com/vuejs/core/issues/14877)) ([584beb1](https://redirect.github.com/vuejs/core/commit/584beb1262d1247d41ed3b463c485c57022fa922)) ##### Performance Improvements - **reactivity:** skip type checks for cached proxies ([#​14860](https://redirect.github.com/vuejs/core/issues/14860)) ([5734fe9](https://redirect.github.com/vuejs/core/commit/5734fe97f6e42d7abb1893c8bc38a17f7deb00b1)) - **runtime-dom:** optimize array event handler dispatch ([#​14828](https://redirect.github.com/vuejs/core/issues/14828)) ([bb18dc8](https://redirect.github.com/vuejs/core/commit/bb18dc8e567ce22f1e5dfbc6b16c1003b48c2785)) - **server-renderer:** avoid materializing iterables in ssrRenderList ([#​14821](https://redirect.github.com/vuejs/core/issues/14821)) ([1b7a2cc](https://redirect.github.com/vuejs/core/commit/1b7a2cc15c501a4b1e4be61874879381af59b74f))
vuejs/language-tools (vue-tsc) ### [`v3.3.2`](https://redirect.github.com/vuejs/language-tools/blob/HEAD/CHANGELOG.md#332-2026-05-25) [Compare Source](https://redirect.github.com/vuejs/language-tools/compare/v3.3.1...v3.3.2) ##### language-core - **feat:** preserve literal types for inline `v-for` sources ([#​6067](https://redirect.github.com/vuejs/language-tools/issues/6067)) - Thanks to [@​kkesidis](https://redirect.github.com/kkesidis)! - **fix:** align `v-bind` shorthand identifier skipping with interpolation - Thanks to [@​KazariEX](https://redirect.github.com/KazariEX)! ##### vscode - **feat:** transform tsserver content ([#​6062](https://redirect.github.com/vuejs/language-tools/issues/6062)) - Thanks to [@​KazariEX](https://redirect.github.com/KazariEX)! - **fix:** do not mark trailing slash in capitalized self-closing tags as invalid ([#​6065](https://redirect.github.com/vuejs/language-tools/issues/6065)) - Thanks to [@​suisanka](https://redirect.github.com/suisanka)!
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --------- Co-authored-by: Lunny Xiao Co-authored-by: silverwind --- .github/workflows/cron-renovate.yml | 2 +- Makefile | 8 - package.json | 29 +- pnpm-lock.yaml | 3532 ++++++++++++++++++------ pnpm-workspace.yaml | 23 - public/assets/img/svg/octicon-flag.svg | 1 + renovate.json5 | 2 +- tools/migrate-nolyfills.ts | 33 - 8 files changed, 2628 insertions(+), 1002 deletions(-) create mode 100644 public/assets/img/svg/octicon-flag.svg delete mode 100644 tools/migrate-nolyfills.ts diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index 2c6cc8aedcd..a50af530f20 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -28,5 +28,5 @@ jobs: token: ${{ secrets.RENOVATE_TOKEN }} env: RENOVATE_BINARY_SOURCE: install # auto-install go/node toolchains needed by post-upgrade tasks. - RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg nolyfill)$"]' + RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^make (tidy|svg)$"]' RENOVATE_REPOSITORIES: '["go-gitea/gitea"]' diff --git a/Makefile b/Makefile index a74752b8d71..2b19075a484 100644 --- a/Makefile +++ b/Makefile @@ -608,14 +608,6 @@ update-js: node_modules ## update js dependencies rm -rf node_modules pnpm-lock.yaml pnpm install @touch node_modules - $(MAKE) --no-print-directory nolyfill - -.PHONY: nolyfill -nolyfill: node_modules ## apply nolyfill overrides to package.json and relock - pnpm exec nolyfill install - node tools/migrate-nolyfills.ts - pnpm install - @touch node_modules .PHONY: update-py update-py: node_modules ## update py dependencies diff --git a/package.json b/package.json index c56dd03b083..8a915ea795c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.2.1", + "packageManager": "pnpm@11.4.0", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" @@ -28,7 +28,7 @@ "@lezer/highlight": "1.2.3", "@mcaptcha/vanilla-glue": "0.1.0-rc2", "@mermaid-js/layout-elk": "0.2.1", - "@primer/octicons": "19.26.0", + "@primer/octicons": "19.27.0", "@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-lang-nix": "6.0.1", "@replit/codemirror-lang-svelte": "6.0.0", @@ -45,19 +45,19 @@ "colord": "2.9.3", "compare-versions": "6.1.1", "cropperjs": "1.6.2", - "dayjs": "1.11.20", + "dayjs": "1.11.21", "easymde": "2.21.0", "esbuild": "0.28.0", "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.1.1", - "katex": "0.16.47", + "katex": "0.17.0", "mermaid": "11.15.0", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", "postcss": "8.5.15", - "rolldown-license-plugin": "3.0.7", + "rolldown-license-plugin": "3.0.8", "sortablejs": "1.15.7", "swagger-ui-dist": "5.32.6", "tailwindcss": "3.4.19", @@ -67,14 +67,14 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.13", + "vite": "8.0.14", "vite-string-plugin": "2.0.4", - "vue": "3.5.34", + "vue": "3.5.35", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.3" }, "devDependencies": { - "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", + "@eslint-community/eslint-plugin-eslint-comments": "4.7.2", "@eslint/json": "1.2.0", "@playwright/test": "1.60.0", "@stylistic/eslint-plugin": "5.10.0", @@ -89,9 +89,9 @@ "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/parser": "8.60.0", "@vitejs/plugin-vue": "6.0.7", - "@vitest/eslint-plugin": "1.6.17", + "@vitest/eslint-plugin": "1.6.18", "eslint": "10.4.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", @@ -109,8 +109,7 @@ "happy-dom": "20.9.0", "jiti": "2.7.0", "markdownlint-cli": "0.48.0", - "material-icon-theme": "5.34.0", - "nolyfill": "1.0.44", + "material-icon-theme": "5.35.0", "postcss-html": "1.8.1", "spectral-cli-bundle": "1.0.8", "stylelint": "17.12.0", @@ -120,9 +119,9 @@ "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "6.0.3", - "typescript-eslint": "8.59.4", - "updates": "17.16.13", + "typescript-eslint": "8.60.0", + "updates": "17.17.2", "vitest": "4.1.7", - "vue-tsc": "3.3.1" + "vue-tsc": "3.3.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e96e6a6174b..11c6ebf5add 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,28 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - array-includes: npm:@nolyfill/array-includes@^1 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1 - es-aggregate-error: npm:@nolyfill/es-aggregate-error@^1 - hasown: npm:@nolyfill/hasown@^1 - is-core-module: npm:@nolyfill/is-core-module@^1 - object.assign: npm:@nolyfill/object.assign@^1 - object.fromentries: npm:@nolyfill/object.fromentries@^1 - object.groupby: npm:@nolyfill/object.groupby@^1 - object.values: npm:@nolyfill/object.values@^1 - safe-buffer: npm:@nolyfill/safe-buffer@^1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1 - safer-buffer: npm:@nolyfill/safer-buffer@^1 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 - object-keys: npm:@nolyfill/object-keys@^1 - object.entries: npm:@nolyfill/object.entries@^1 - abab: npm:@nolyfill/abab@^1 - es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 - importers: .: @@ -97,8 +75,8 @@ importers: specifier: 0.2.1 version: 0.2.1(mermaid@11.15.0) '@primer/octicons': - specifier: 19.26.0 - version: 19.26.0 + specifier: 19.27.0 + version: 19.27.0 '@replit/codemirror-indentation-markers': specifier: 6.5.3 version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) @@ -116,7 +94,7 @@ importers: version: 2.6.2 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -128,7 +106,7 @@ importers: version: 4.5.1 chartjs-adapter-dayjs-4: specifier: 1.0.4 - version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.20) + version: 1.0.4(chart.js@4.5.1)(dayjs@1.11.21) chartjs-plugin-zoom: specifier: 2.2.0 version: 2.2.0(chart.js@4.5.1) @@ -148,8 +126,8 @@ importers: specifier: 1.6.2 version: 1.6.2 dayjs: - specifier: 1.11.20 - version: 1.11.20 + specifier: 1.11.21 + version: 1.11.21 easymde: specifier: 2.21.0 version: 2.21.0 @@ -166,8 +144,8 @@ importers: specifier: 4.1.1 version: 4.1.1 katex: - specifier: 0.16.47 - version: 0.16.47 + specifier: 0.17.0 + version: 0.17.0 mermaid: specifier: 11.15.0 version: 11.15.0 @@ -184,8 +162,8 @@ importers: specifier: 8.5.15 version: 8.5.15 rolldown-license-plugin: - specifier: 3.0.7 - version: 3.0.7(rolldown@1.0.1) + specifier: 3.0.8 + version: 3.0.8(rolldown@1.0.2) sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -214,24 +192,24 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.13 - version: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + specifier: 8.0.14 + version: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) vite-string-plugin: specifier: 2.0.4 - version: 2.0.4(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 2.0.4(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue: - specifier: 3.5.34 - version: 3.5.34(typescript@6.0.3) + specifier: 3.5.35 + version: 3.5.35(typescript@6.0.3) vue-bar-graph: specifier: 2.2.0 version: 2.2.0(typescript@6.0.3) vue-chartjs: specifier: 5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.35(typescript@6.0.3)) devDependencies: '@eslint-community/eslint-plugin-eslint-comments': - specifier: 4.7.1 - version: 4.7.1(eslint@10.4.0(jiti@2.7.0)) + specifier: 4.7.2 + version: 4.7.2(eslint@10.4.0(jiti@2.7.0)) '@eslint/json': specifier: 1.2.0 version: 1.2.0 @@ -275,17 +253,17 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.59.4 - version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.0 + version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.17 - version: 1.6.17(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) + specifier: 1.6.18 + version: 1.6.18(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) eslint: specifier: 10.4.0 version: 10.4.0(jiti@2.7.0) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-array-func: specifier: 5.1.1 version: 5.1.1(eslint@10.4.0(jiti@2.7.0)) @@ -297,7 +275,7 @@ importers: version: 6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + version: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-playwright: specifier: 2.10.4 version: 2.10.4(eslint@10.4.0(jiti@2.7.0)) @@ -312,7 +290,7 @@ importers: version: 64.0.0(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-vue: specifier: 10.9.1 - version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) + version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) eslint-plugin-vue-scoped-css: specifier: 3.1.0 version: 3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) @@ -332,11 +310,8 @@ importers: specifier: 0.48.0 version: 0.48.0 material-icon-theme: - specifier: 5.34.0 - version: 5.34.0 - nolyfill: - specifier: 1.0.44 - version: 1.0.44 + specifier: 5.35.0 + version: 5.35.0 postcss-html: specifier: 1.8.1 version: 1.8.1 @@ -365,17 +340,17 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.59.4 - version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.0 + version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) updates: - specifier: 17.16.13 - version: 17.16.13 + specifier: 17.17.2 + version: 17.17.2 vitest: specifier: 4.1.7 - version: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue-tsc: - specifier: 3.3.1 - version: 3.3.1(typescript@6.0.3) + specifier: 3.3.2 + version: 3.3.2(typescript@6.0.3) packages: @@ -386,36 +361,36 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@cacheable/memory@2.0.8': - resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + '@cacheable/memory@2.0.9': + resolution: {integrity: sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==} '@cacheable/utils@2.4.1': resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} @@ -561,8 +536,8 @@ packages: '@codemirror/view@6.43.0': resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} - '@csstools/css-calc@3.2.0': - resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -574,8 +549,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3': - resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -773,8 +748,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-plugin-eslint-comments@4.7.1': - resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} + '@eslint-community/eslint-plugin-eslint-comments@4.7.2': + resolution: {integrity: sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -834,8 +809,8 @@ packages: resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.1': - resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@github/browserslist-config@1.0.0': @@ -880,8 +855,8 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.1.1': - resolution: {integrity: sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==} + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} @@ -927,8 +902,8 @@ packages: '@lezer/common@1.5.2': resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@lezer/cpp@1.1.5': - resolution: {integrity: sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==} + '@lezer/cpp@1.1.6': + resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==} '@lezer/css@1.3.3': resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} @@ -954,14 +929,14 @@ packages: '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} - '@lezer/markdown@1.6.3': - resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + '@lezer/markdown@1.6.4': + resolution: {integrity: sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==} '@lezer/php@1.0.5': resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} - '@lezer/python@1.1.18': - resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} + '@lezer/python@1.1.19': + resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==} '@lezer/rust@1.0.2': resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} @@ -992,9 +967,6 @@ packages: '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -1013,90 +985,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/abab@1.0.44': - resolution: {integrity: sha512-ZQFi9RSKHKnXM4lLtazYx7otLcqEpQ6sSzs7yzb6zlcimyFIv3CV4s2G+CaA3GnPDTmUFugCn2wWap4jynGQow==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array-includes@1.0.44': - resolution: {integrity: sha512-IVEqpEgFbLaU0hUoMwJYXNSdi6lq+FxHdxd8xTKDLxh8k6u5YNGz4Bo6bT46l7p0x8PbJmHViBtngqhvE528fA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.findlastindex@1.0.44': - resolution: {integrity: sha512-BLeHS3SulsR3iFxxETL9q21lArV2KS7lh2wcUnhue1ppx19xah1W7MdFxepyeGbM3Umk9S90snfboXAds5HkTg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.flat@1.0.44': - resolution: {integrity: sha512-HnOqOT4te0l+XU9UKhy3ry+pc+ZRNsUJFR7omMEtjXf4+dq6oXmIBk7vR35+hSTk4ldjwm/27jwV3ZIGp3l4IQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/array.prototype.flatmap@1.0.44': - resolution: {integrity: sha512-P6OsaEUrpBJ9NdNekFDQVM9LOFHPDKSJzwOWRBaC6LqREX+4lkZT2Q+to78R6aG6atuOQsxBVqPjMGCKjWdvyQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/es-set-tostringtag@1.0.44': - resolution: {integrity: sha512-Qfiv/3wI+mKSPCgU8Fg/7Auu4os00St4GwyLqCCZ/oBD4W00itWUkl9Rq1MCGS+VXYDnTobvrc6AvPagwnT2pg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/hasown@1.0.44': - resolution: {integrity: sha512-GA/21lkTr2PAQuT6jGnhLuBD5IFd/AEhBXJ/tf33+/bVxPxg+5ejKx9jGQGnyV/P0eSmdup5E+s8b2HL6lOrwQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object-keys@1.0.44': - resolution: {integrity: sha512-k59MxUv8AFPd+GzZlGBhBaW6zu6t7f2euhctz7vdT3WsE3LVPANgE7uAN71IIpxaIn+bBEX1Xesf/rTwWqVO0Q==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.assign@1.0.44': - resolution: {integrity: sha512-cZoXq09YZXDgkxRMAP/TTb3kAsWm7p5OyBugWDe4fOfxf0XRI55mgDSkuyq41sV1qW1zVC5aSsKEh1hQo1KOvA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.entries@1.0.44': - resolution: {integrity: sha512-RCxO6EH9YbvxQWGYLKOd7MjNi7vKzPkXv1VDWNsy1C8BksQxXNPQrddlu3INi1O2fexk82WXpCCeaCtpU/y21w==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.fromentries@1.0.44': - resolution: {integrity: sha512-/LrsCtpLmByZ6GwP/NeXULSgMyNsVr5d6FlgQy1HZatAiBc8c+WZ1VmFkK19ZLXCNNXBedXDultrp0x4Nz+QQw==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.groupby@1.0.44': - resolution: {integrity: sha512-jCt/8pN+10mlbeg0ZESpVVaqn5qqpv6kpjM+GDfEP7cXGDSPlIjtvfYWRZK4k4Gftkhhgqkzvcrr8z1wuNO1TQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/object.values@1.0.44': - resolution: {integrity: sha512-bwIpVzFMudUC0ofnvdSDB/OyGUizcU+r32ZZ0QTMbN03gUttMtdCFDekuSYT0XGFgufTQyZ4ONBnAeb3DFCPGQ==} - engines: {node: '>=12.4.0'} - - '@nolyfill/safe-regex-test@1.0.44': - resolution: {integrity: sha512-Q6veatd1NebtD8Sre6zjvO35QzG21IskMVOOEbePFcNO9noanNJgsqHeOCr0c5yZz6Z0DAizLg2gIZWokJSkXw==} - engines: {node: '>=12.4.0'} - - '@nolyfill/safer-buffer@1.0.44': - resolution: {integrity: sha512-Ouw1fMwjAy1V4MpnDASfu1DCPgkP0nNFteiiWbFoEGSqa7Vnmkb6if2c522N2WcMk+RuaaabQbC1F1D4/kTXcg==} - engines: {node: '>=12.4.0'} - - '@nolyfill/shared@1.0.44': - resolution: {integrity: sha512-NI1zxDh4LYL7PYlKKCwojjuc5CEZslywrOTKBNyodjmWjRiZ4AlCMs3Gp+zDoPQPNkYCSQp/luNojHmJWWfCbw==} - - '@nolyfill/string.prototype.includes@1.0.44': - resolution: {integrity: sha512-d1t7rnoAYyoap0X3a/gCnusCvxzK6v7uMFzW8k0mI2WtAK8HiKuzaQUwAriyVPh63GsvQCqvXx8Y5gtdh4LjSA==} - engines: {node: '>=12.4.0'} - - '@nolyfill/string.prototype.trimend@1.0.44': - resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} - engines: {node: '>=12.4.0'} - - '@oxc-project/types@0.130.0': - resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} '@playwright/test@1.60.0': resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} @@ -1106,8 +1003,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.26.0': - resolution: {integrity: sha512-K+ewEvXf9w+65i4OeLDXOhd1yT5qdMIpU/miqmjTuaGufZCxFn4131SIh0CfrinmLFnL9Ow60kk3hxx0Y4oIAQ==} + '@primer/octicons@19.27.0': + resolution: {integrity: sha512-7xC6D89f9IcoDezeKTGETbgRAoXJnbZlakavqYzD4Wo+uTC6212k0fTE/dLV8WCDOwfp//WyONftdaFRdI1VdQ==} '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} @@ -1157,97 +1054,97 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.1': - resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.1': - resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.1': - resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.1': - resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': - resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.1': - resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.1': - resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.1': - resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.1': - resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1306,8 +1203,8 @@ packages: peerDependencies: stylelint: ^17.6.0 - '@swc/helpers@0.5.21': - resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tootallnate/once@2.0.1': resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} @@ -1424,8 +1321,8 @@ packages: '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -1511,165 +1408,234 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.59.4': - resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} + '@typescript-eslint/eslint-plugin@8.60.0': + resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.4 + '@typescript-eslint/parser': ^8.60.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.4': - resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.0': + resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.4': - resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} + '@typescript-eslint/project-service@8.60.0': + resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.4': - resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.59.4': - resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.4': - resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} + '@typescript-eslint/scope-manager@8.60.0': + resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.0': + resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.0': + resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.59.4': - resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.59.4': - resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/utils@8.59.4': - resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.59.4': - resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} + '@typescript-eslint/types@8.60.0': + resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.0': + resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.0': + resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.0': + resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] os: [android] - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} cpu: [arm64] os: [android] - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} cpu: [arm64] os: [darwin] - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} cpu: [x64] os: [darwin] - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} cpu: [x64] os: [freebsd] - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} cpu: [arm] os: [linux] - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] libc: [musl] - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] libc: [glibc] - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] libc: [musl] - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} cpu: [arm64] os: [win32] - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} cpu: [ia32] os: [win32] - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} cpu: [x64] os: [win32] @@ -1683,8 +1649,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.17': - resolution: {integrity: sha512-sIVY9ZeVcXyPxFCNRkIt8Yw4keKIcUyp9/8qnmuomPwE+ST1htw5sZsbqdUMTiah9SmCg1JYoK9RqdDtPeNYYg==} + '@vitest/eslint-plugin@1.6.18': + resolution: {integrity: sha512-J6U4X0jH3NwTuYouvrJn6I8ypTOU+GhKEjyVwpoPnDuc23usa/xi/R0caWLBbNp3xLy3/rL1YkuJuneTMVV4Mg==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -1737,37 +1703,41 @@ packages: '@volar/typescript@2.4.28': resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} - '@vue/compiler-core@3.5.34': - resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} - '@vue/compiler-dom@3.5.34': - resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} - '@vue/compiler-sfc@3.5.34': - resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} - '@vue/compiler-ssr@3.5.34': - resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} - '@vue/language-core@3.3.1': - resolution: {integrity: sha512-NP8g6V7x81NVOXbLupUvYY6i6LqUkjkVowe2epRedmpgaFCOdjgWHE/rQBvEJ4r7koAYODIjGeBWEdt6n7jYXQ==} + '@vue/language-core@3.3.2': + resolution: {integrity: sha512-CLwjSfHlPLhjd2qhuS3tTFtnOIWHXAM5u4X1DxmzlQ8j5bmOYlKCsSusOP7jCRJnlVg0mCTQtHU3vwFvopZGoQ==} - '@vue/reactivity@3.5.34': - resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} - '@vue/runtime-core@3.5.34': - resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} - '@vue/runtime-dom@3.5.34': - resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} - '@vue/server-renderer@3.5.34': - resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} peerDependencies: - vue: 3.5.34 + vue: 3.5.35 - '@vue/shared@3.5.34': - resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -1799,6 +1769,14 @@ packages: alien-signals@3.2.1: resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + ansi-escapes@1.4.0: + resolution: {integrity: sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==} + engines: {node: '>=0.10.0'} + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1807,6 +1785,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1835,9 +1817,40 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + asciinema-player@3.15.1: resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1849,11 +1862,25 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axe-core@4.11.4: - resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axe-core@4.12.0: + resolution: {integrity: sha512-FTavr/7Ba0IptwGOPxnQvdyW2tAsdLBMTBXz7rKH6xJ2skpyxpBxyHkDdBs4lf69yRqYpkqCdfhnwS8YULGOmg==} engines: {node: '>=4'} axobject-query@4.1.0: @@ -1870,23 +1897,33 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.27: - resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + biome@0.3.3: + resolution: {integrity: sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ==} + hasBin: true + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -1905,16 +1942,28 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - builtin-modules@5.1.0: - resolution: {integrity: sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==} + builtin-modules@5.2.0: + resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cacheable@2.3.4: - resolution: {integrity: sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==} + cacheable@2.3.5: + resolution: {integrity: sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1924,13 +1973,20 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001791: - resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1985,9 +2041,20 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cli-cursor@1.0.2: + resolution: {integrity: sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==} + engines: {node: '>=0.10.0'} + + cli-width@1.1.1: + resolution: {integrity: sha512-eMU2akIeEIkCxGXUNmDnJq1KzOIiPnJ+rKqRe6hcxE3vIOPvpMrBYOn/Bl7zNlYJj/zQxXquAnozHUCf9Whnsg==} + clippie@4.2.0: resolution: {integrity: sha512-NcWaVzqChJ69+foFhwJXta3KXWNDJlpicxcfZG5udobyszOSBDhmFubKv1b/1nIZiVAsPoKqME2iV1SITZqFoQ==} + code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + codemirror-lang-elixir@4.0.1: resolution: {integrity: sha512-z6W/XB4b7TZrp9EZYBGVq93vQfvKbff+1iM8YZaVErL0dguBAeLmVRlEv1NuDZHOP1qjJ3NwyibkUkNWn7q9VQ==} @@ -2019,6 +2086,9 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -2031,8 +2101,8 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - comment-parser@1.4.6: - resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} + comment-parser@1.4.7: + resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} engines: {node: '>= 12.0.0'} compare-versions@6.1.1: @@ -2041,18 +2111,22 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-js@3.32.2: resolution: {integrity: sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==} + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -2129,8 +2203,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.3: - resolution: {integrity: sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==} + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -2278,12 +2352,28 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -2319,6 +2409,14 @@ packages: resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} engines: {node: '>=0.10.0'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -2365,17 +2463,30 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.2: - resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==} + dompurify@3.4.7: + resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + earlgrey-runtime@0.1.2: + resolution: {integrity: sha512-T4qoScXi5TwALDv8nlGTvOuCT8jXcKcxtO8qVdqv46IA2GHJfQzwoBPbkOmORnyhu3A98cVVuhWLsM2CzPljJg==} + easymde@2.21.0: resolution: {integrity: sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==} - electron-to-chromium@1.5.349: - resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + editor@1.0.0: + resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==} + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} @@ -2405,6 +2516,14 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -2412,8 +2531,24 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-toolkit@1.46.1: - resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} @@ -2472,8 +2607,8 @@ packages: eslint-plugin-import-x: optional: true - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + eslint-module-utils@2.13.0: + resolution: {integrity: sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -2571,8 +2706,8 @@ packages: peerDependencies: eslint: '>=8.40.0' - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2707,10 +2842,21 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exit-hook@1.1.1: + resolution: {integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==} + engines: {node: '>=0.10.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2727,8 +2873,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} @@ -2752,6 +2898,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@1.7.0: + resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} + engines: {node: '>=0.10.0'} + file-entry-cache@11.1.3: resolution: {integrity: sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==} @@ -2781,10 +2931,31 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + fs-extra@0.26.7: + resolution: {integrity: sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q==} + + fs-promise@0.5.0: + resolution: {integrity: sha512-Y+4F4ujhEcayCJt6JmzcOun9MYGQwz+bVUiuBmTkJImhBHKpBvmVPZR9wtfiF7k3ffwAOAuurygQe+cPLSFQhw==} + deprecated: Use mz or fs-extra^3.0 with Promise Support + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2795,16 +2966,45 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - get-east-asian-width@1.5.0: - resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2813,6 +3013,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2833,6 +3037,10 @@ packages: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@16.2.0: resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} @@ -2840,6 +3048,10 @@ packages: globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2854,6 +3066,23 @@ packages: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2862,10 +3091,29 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -2887,6 +3135,10 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -2924,6 +3176,13 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2931,6 +3190,16 @@ packages: resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + inquirer-promise@0.0.3: + resolution: {integrity: sha512-82CQX586JAV9GAgU9yXZsMDs+NorjA0nLhkfFx9+PReyOnuoHRbHrC1Z90sS95bFJI1Tm1gzMObuE0HabzkJpg==} + + inquirer@0.11.4: + resolution: {integrity: sha512-QR+2TW90jnKk9LUUtbcA3yQXKt2rDEKMh6+BAZQIeumtzHexnwVLdPakSslGijXYLJCzFv7GMXbFCn0pA00EUw==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -2944,13 +3213,29 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} @@ -2961,6 +3246,22 @@ packages: is-bun-module@2.0.0: resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -2968,10 +3269,22 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2979,6 +3292,18 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2990,12 +3315,57 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + is-valid-element-name@1.0.0: resolution: {integrity: sha512-GZITEJY2LkSjQfaIPBha7eyZv+ge0PhBR7KITeCCWvy7VBQrCUdFkvpI+HrAPQjVtVjy1LvlEkqQTHckoszruw==} + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + jest-environment-jsdom@29.7.0: resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3041,6 +3411,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + jsdoc-type-pratt-parser@7.2.0: resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} engines: {node: '>=20.0.0'} @@ -3071,9 +3444,15 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -3081,10 +3460,17 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + jsx-ast-utils-x@0.1.0: resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3093,10 +3479,17 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + kaiser@0.0.4: + resolution: {integrity: sha512-m8ju+rmBqvclZmyrOXgGGhOYSjKJK6RN1NhqEltemY87UqZOxEkizg9TOy1vQSyJ01Wx6SAPuuN0iO2Mgislvw==} + katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} hasBin: true + katex@0.17.0: + resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3114,6 +3507,9 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + klaw@1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -3215,8 +3611,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -3243,6 +3639,12 @@ packages: lodash.upperfirst@4.3.1: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@3.10.1: + resolution: {integrity: sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3269,10 +3671,14 @@ packages: engines: {node: '>= 12'} hasBin: true - material-icon-theme@5.34.0: - resolution: {integrity: sha512-m+ZgtdtJMkfOwxpfUH8FqyJbfnAJuxMBNv4XRvz3m808Is42RbeYc/5W0bk5qGJJLxzF5Cupj6Or52aQISwz3A==} + material-icon-theme@5.35.0: + resolution: {integrity: sha512-ptU3rjmZjPCeLRog0HcJ1HtnoVgOslc350WHk1oIvX7fVFE4NBAF7GL3QvCCEjtd1TSSDoxmS4dWsv6bTB1x9g==} engines: {vscode: ^1.55.0} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mathml-tag-names@4.0.0: resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==} @@ -3393,9 +3799,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3405,6 +3808,9 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mute-stream@0.0.5: + resolution: {integrity: sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -3446,13 +3852,9 @@ packages: encoding: optional: true - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - - nolyfill@1.0.44: - resolution: {integrity: sha512-PoggwVLiJUn0MnodpftsiC7EuknW5+6v62ntTOQ6T6l7g2r6aoaOwgk0tQW2BxGLYw9bF298LL8jDFTmEFuzlA==} - engines: {node: '>=12.4.0'} - hasBin: true + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -3461,9 +3863,16 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3472,9 +3881,44 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@1.1.0: + resolution: {integrity: sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==} + engines: {node: '>=0.10.0'} + online-3d-viewer@0.18.0: resolution: {integrity: sha512-y7ZlV/zkakNUyjqcXz6XecA7vXgLEUnaAey9tyx8o6/wcdV64RfjXAQOjGXGY2JOZoDi4Cg1ic9icSWMWAvRQA==} @@ -3482,6 +3926,14 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3517,6 +3969,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3533,6 +3989,9 @@ packages: perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3552,9 +4011,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - playwright-core@1.60.0: resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} @@ -3575,6 +4031,10 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss-html@1.8.1: resolution: {integrity: sha512-OLF6P7qctfAWayOhLpcVnTGqVeJzu2W3WpIYelfz2+JV5oGxfkcEvweN9U4XpeqE0P98dcD9ssusGwlF0TK0uQ==} engines: {node: ^12 || >=14} @@ -3670,10 +4130,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qified@0.9.1: - resolution: {integrity: sha512-n7mar4T0xQ+39dE2vGTAlbxUEpndwPANH0kDef1/MYsB8Bba9wshkybIRx74qgcvKQPEWErf9AqAdYjhzY2Ilg==} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} + qs@6.5.5: + resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} + engines: {node: '>=0.6'} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -3690,10 +4154,20 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readline2@1.0.1: + resolution: {integrity: sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g==} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerator-runtime@0.9.6: + resolution: {integrity: sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw==} + regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3702,6 +4176,10 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + regjsparser@0.13.1: resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} hasBin: true @@ -3710,6 +4188,16 @@ packages: resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} engines: {node: '>= 0.8.0'} + request-promise@3.0.0: + resolution: {integrity: sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw==} + engines: {node: '>=0.10.0'} + deprecated: request-promise has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3729,31 +4217,43 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.6: - resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} engines: {node: '>= 0.4'} hasBin: true + restore-cursor@1.0.1: + resolution: {integrity: sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==} + engines: {node: '>=0.10.0'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown-license-plugin@3.0.7: - resolution: {integrity: sha512-jBgDoLE/UWPB1VWzERbpWGuHA0+YBDflFHX889Mayg4Z8Re27kFh7GKbuaEt4f3VK1xpJ4PGVcUoT3V2+fAybw==} + rolldown-license-plugin@3.0.8: + resolution: {integrity: sha512-q4nvGtimBIxValBXzkri+0jP2Sdf6PLztYykn7vCu0nVkJEzGsVRfcH/1X6qm0S8//4/gFt/XxwJqpaM6uSdJA==} peerDependencies: rolldown: '*' - rolldown@1.0.1: - resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + run-async@0.1.0: + resolution: {integrity: sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw==} + run-con@1.3.2: resolution: {integrity: sha512-CcfE+mYiTcKEzg0IqS08+efdnH0oJ3zV0wSUFBNrMHMuxCtXvBCLzCJHatwuXDcu/RlhjTziTo/a1ruQik6/Yg==} hasBin: true @@ -3764,6 +4264,27 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rx-lite@3.1.2: + resolution: {integrity: sha512-1I1+G2gteLB8Tkt8YI1sJvSIfa0lWuRtC8GjvtyPBcLSF5jBCCJJqKrpER5JU5r6Bhe+i9/pK3VMuUcXu0kdwQ==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -3780,21 +4301,33 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true - seroval-plugins@1.5.3: - resolution: {integrity: sha512-LhVh4KjjkKmCxOUjoaUwtqbDjyMfnA535yEmmGDuwZcIYtw8ns6tZmeszNTECeUg/3sJpnEjsz/KhQrcPXPw1Q==} + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.5.3: - resolution: {integrity: sha512-BXe0x4buEeYiIKaRUnth1WqCILQ3k4O67KP/B4pC3pVz0Mv2c96ngA9QDREUYxWY1sb2RZVRqwI9RcpVMyHCVw==} + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3803,6 +4336,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3826,8 +4375,8 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} - solid-js@1.9.12: - resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} solid-transition-group@0.2.3: resolution: {integrity: sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==} @@ -3851,6 +4400,11 @@ packages: engines: {node: '>=20'} hasBin: true + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -3865,6 +4419,14 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3877,6 +4439,26 @@ packages: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3944,6 +4526,10 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3980,8 +4566,8 @@ packages: resolution: {integrity: sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==} engines: {node: '>=14'} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} table@6.9.0: @@ -4007,15 +4593,18 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -4032,6 +4621,10 @@ packages: toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} @@ -4065,6 +4658,12 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4073,8 +4672,24 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - typescript-eslint@8.59.4: - resolution: {integrity: sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.60.0: + resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4090,18 +4705,19 @@ packages: engines: {node: '>=14.17'} hasBin: true - typo-js@1.3.1: - resolution: {integrity: sha512-elJkpCL6Z77Ghw0Lv0lGnhBAjSTOQ5FhiVOCfOuxhaoTT2xtLVbqikYItK5HHchzPbHEUFAcjOH669T2ZzeCbg==} + typo-js@1.3.2: + resolution: {integrity: sha512-Z1YkJ7IIYNrFeOxAlHUercY4Q2I+PhYD/3VkWpJGy/Oqudy3bFpNcQxnv6Oa9fTSXCHPGz1eDoX1bZYm2Z891A==} uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uint8-to-base64@0.2.1: resolution: {integrity: sha512-uO/84GaoDUfiAxpa8EksjVLE77A9Kc7ZTziN4zRpq4de9yLaLcZn3jx1/sVjyupsywcVX6RKWbqLe7gUNyzH+Q==} + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -4113,8 +4729,12 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + untildify@3.0.3: + resolution: {integrity: sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==} + engines: {node: '>=4'} update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} @@ -4122,8 +4742,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.16.13: - resolution: {integrity: sha512-Od8Ea50dJZWp3dKnibQSQ08Wp1Z3qhn3i0zMBOB47qPltSlGjZFVDR+XjfSEzQIE/Grp2z8OQ/G4YuNiH3X8xA==} + updates@17.17.2: + resolution: {integrity: sha512-gOwGrBYBvHVS+OiaUFRuilrmf/P8eYVmKUrkq7W3fvYUgpBsNKgfZo/CPhLKBa7xEFiEuMwMxTkiKsd9mn8pEw==} engines: {node: '>=22'} hasBin: true @@ -4133,23 +4753,36 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + user-home@2.0.0: + resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==} + engines: {node: '>=0.10.0'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + vite-string-plugin@2.0.4: resolution: {integrity: sha512-uahQl15I6hsHGzbjCmllVoKZBmZavXAp8GXBmq5omNQM52wgBv7e//98A6cONbq0Of6F/5uJ30OjF5ggNY6reQ==} peerDependencies: vite: '*' - vite@8.0.13: - resolution: {integrity: sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==} + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4250,14 +4883,14 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-tsc@3.3.1: - resolution: {integrity: sha512-webBP3jhlxzhELZ2g+11KJ6pg5OVY1xWhWrj7N/yQMi1CrtxJnW+tUACyRVeDK0cQNLP2Va5HNYK8pe+7c+msw==} + vue-tsc@3.3.2: + resolution: {integrity: sha512-n7nQoA3YWW/eiDR8jMiv/uJvlg0uLGs+YgUrsTrf9EZaYSt3tuvMZb5V8+7Mvh/EH5pnY/hoVdgfjH+XcK+wwA==} hasBin: true peerDependencies: typescript: '>=5.0.0' - vue@3.5.34: - resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -4294,6 +4927,22 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.21: + resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -4312,12 +4961,15 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4352,32 +5004,32 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.1.2 + tinyexec: 1.2.4 - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/parser@7.29.3': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@braintree/sanitize-url@7.1.2': {} - '@cacheable/memory@2.0.8': + '@cacheable/memory@2.0.9': dependencies: '@cacheable/utils': 2.4.1 '@keyv/bigmap': 1.3.1(keyv@5.6.0) @@ -4475,7 +5127,7 @@ snapshots: '@codemirror/lang-cpp@6.0.3': dependencies: '@codemirror/language': 6.12.3 - '@lezer/cpp': 1.1.5 + '@lezer/cpp': 1.1.6 '@codemirror/lang-css@6.3.1': dependencies: @@ -4563,7 +5215,7 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 '@lezer/common': 1.5.2 - '@lezer/markdown': 1.6.3 + '@lezer/markdown': 1.6.4 '@codemirror/lang-php@6.0.2': dependencies: @@ -4579,7 +5231,7 @@ snapshots: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 - '@lezer/python': 1.1.18 + '@lezer/python': 1.1.19 '@codemirror/lang-rust@6.0.2': dependencies: @@ -4700,7 +5352,7 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -4709,7 +5361,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -4730,7 +5382,7 @@ snapshots: '@deltablot/dropzone@7.4.3': dependencies: - '@swc/helpers': 0.5.21 + '@swc/helpers': 0.5.23 '@emnapi/core@1.10.0': dependencies: @@ -4826,7 +5478,7 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.0(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 eslint: 10.4.0(jiti@2.7.0) @@ -4895,7 +5547,7 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@eslint/plugin-kit@0.7.1': + '@eslint/plugin-kit@0.7.2': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 @@ -4933,11 +5585,11 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@3.1.1': + '@iconify/utils@3.1.3': dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.2 + import-meta-resolve: 4.2.0 '@jest/environment@29.7.0': dependencies: @@ -4994,7 +5646,7 @@ snapshots: '@lezer/common@1.5.2': {} - '@lezer/cpp@1.1.5': + '@lezer/cpp@1.1.6': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5044,7 +5696,7 @@ snapshots: dependencies: '@lezer/common': 1.5.2 - '@lezer/markdown@1.6.3': + '@lezer/markdown@1.6.4': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5055,7 +5707,7 @@ snapshots: '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 - '@lezer/python@1.1.18': + '@lezer/python@1.1.19': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -5109,13 +5761,6 @@ snapshots: dependencies: '@chevrotain/types': 11.1.2 - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -5135,73 +5780,11 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nolyfill/abab@1.0.44': {} - - '@nolyfill/array-includes@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.findlastindex@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.flat@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/array.prototype.flatmap@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/es-set-tostringtag@1.0.44': {} - - '@nolyfill/hasown@1.0.44': {} - - '@nolyfill/is-core-module@1.0.39': {} - - '@nolyfill/object-keys@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.assign@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.entries@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.fromentries@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.groupby@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/object.values@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/safe-regex-test@1.0.44': {} - - '@nolyfill/safer-buffer@1.0.44': {} - - '@nolyfill/shared@1.0.44': {} - - '@nolyfill/string.prototype.includes@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@nolyfill/string.prototype.trimend@1.0.44': - dependencies: - '@nolyfill/shared': 1.0.44 - - '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.132.0': {} '@package-json/types@0.0.12': {} - '@pkgr/core@0.2.9': {} + '@pkgr/core@0.3.6': {} '@playwright/test@1.60.0': dependencies: @@ -5209,7 +5792,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.26.0': + '@primer/octicons@19.27.0': dependencies: object-assign: 4.1.1 @@ -5255,53 +5838,53 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.1': + '@rolldown/binding-android-arm64@1.0.2': optional: true - '@rolldown/binding-darwin-arm64@1.0.1': + '@rolldown/binding-darwin-arm64@1.0.2': optional: true - '@rolldown/binding-darwin-x64@1.0.1': + '@rolldown/binding-darwin-x64@1.0.2': optional: true - '@rolldown/binding-freebsd-x64@1.0.1': + '@rolldown/binding-freebsd-x64@1.0.2': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.1': + '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.1': + '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.1': + '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.1': + '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.1': + '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true - '@rolldown/binding-linux-x64-musl@1.0.1': + '@rolldown/binding-linux-x64-musl@1.0.2': optional: true - '@rolldown/binding-openharmony-arm64@1.0.1': + '@rolldown/binding-openharmony-arm64@1.0.2': optional: true - '@rolldown/binding-wasm32-wasi@1.0.1': + '@rolldown/binding-wasm32-wasi@1.0.2': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.1': + '@rolldown/binding-win32-arm64-msvc@1.0.2': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.1': + '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -5327,25 +5910,25 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@solid-primitives/refs@1.1.3(solid-js@1.9.12)': + '@solid-primitives/refs@1.1.3(solid-js@1.9.13)': dependencies: - '@solid-primitives/utils': 6.4.0(solid-js@1.9.12) - solid-js: 1.9.12 + '@solid-primitives/utils': 6.4.0(solid-js@1.9.13) + solid-js: 1.9.13 - '@solid-primitives/transition-group@1.1.2(solid-js@1.9.12)': + '@solid-primitives/transition-group@1.1.2(solid-js@1.9.13)': dependencies: - solid-js: 1.9.12 + solid-js: 1.9.13 - '@solid-primitives/utils@6.4.0(solid-js@1.9.12)': + '@solid-primitives/utils@6.4.0(solid-js@1.9.13)': dependencies: - solid-js: 1.9.12 + solid-js: 1.9.13 '@standard-schema/spec@1.1.0': {} '@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/types': 8.60.1 eslint: 10.4.0(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 @@ -5363,7 +5946,7 @@ snapshots: style-search: 0.1.0 stylelint: 17.12.0(typescript@6.0.3) - '@swc/helpers@0.5.21': + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -5508,7 +6091,7 @@ snapshots: '@types/esrecurse@4.3.1': {} - '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} '@types/geojson@7946.0.16': {} @@ -5558,7 +6141,7 @@ snapshots: '@types/tern@0.23.9': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/throttle-debounce@5.0.2': {} @@ -5583,14 +6166,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 eslint: 10.4.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 @@ -5599,14 +6182,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/type-utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.0 eslint: 10.4.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 @@ -5615,66 +6198,130 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.0(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.0(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.4(typescript@5.9.3)': + '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': + '@typescript-eslint/project-service@8.60.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) - '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) + '@typescript-eslint/types': 8.60.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.4': + '@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/tsconfig-utils@8.59.4(typescript@5.9.3)': + '@typescript-eslint/project-service@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.60.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -5682,11 +6329,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) @@ -5694,122 +6341,217 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.59.4': {} - - '@typescript-eslint/typescript-estree@8.59.4(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.4(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3) - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + eslint: 10.4.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/visitor-keys': 8.59.4 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 10.4.0(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + optional: true + + '@typescript-eslint/types@8.60.0': {} + + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.1 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.60.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/visitor-keys': 8.60.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.60.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) eslint: 10.4.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/types': 8.59.4 - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.0 + '@typescript-eslint/types': 8.60.0 + '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) eslint: 10.4.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.4': + '@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.4 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + eslint: 10.4.0(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + eslint: 10.4.0(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.0': + dependencies: + '@typescript-eslint/types': 8.60.0 eslint-visitor-keys: 5.0.1 - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': + '@typescript-eslint/visitor-keys@8.60.1': dependencies: - '@napi-rs/wasm-runtime': 0.2.12 + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': optional: true - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + '@unrs/resolver-binding-android-arm64@1.12.2': optional: true - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + '@unrs/resolver-binding-darwin-arm64@1.12.2': optional: true - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true '@upsetjs/venn.js@2.0.0': @@ -5817,21 +6559,21 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) - vue: 3.5.34(typescript@6.0.3) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vue: 3.5.35(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.17(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': + '@vitest/eslint-plugin@1.6.18(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': dependencies: - '@typescript-eslint/scope-manager': 8.59.4 - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.4.0(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 - vitest: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -5844,13 +6586,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': + '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) '@vitest/pretty-format@4.1.7': dependencies: @@ -5888,69 +6630,71 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 - '@vue/compiler-core@3.5.34': + '@vue/compiler-core@3.5.35': dependencies: - '@babel/parser': 7.29.3 - '@vue/shared': 3.5.34 + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.35 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.34': + '@vue/compiler-dom@3.5.35': dependencies: - '@vue/compiler-core': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/compiler-sfc@3.5.34': + '@vue/compiler-sfc@3.5.35': dependencies: - '@babel/parser': 7.29.3 - '@vue/compiler-core': 3.5.34 - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.15 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.34': + '@vue/compiler-ssr@3.5.35': dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/language-core@3.3.1': + '@vue/language-core@3.3.2': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 picomatch: 4.0.4 - '@vue/reactivity@3.5.34': + '@vue/reactivity@3.5.35': dependencies: - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.35 - '@vue/runtime-core@3.5.34': + '@vue/runtime-core@3.5.35': dependencies: - '@vue/reactivity': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 - '@vue/runtime-dom@3.5.34': + '@vue/runtime-dom@3.5.35': dependencies: - '@vue/reactivity': 3.5.34 - '@vue/runtime-core': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 csstype: 3.2.3 - '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))': + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@6.0.3))': dependencies: - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 - vue: 3.5.34(typescript@6.0.3) + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@6.0.3) - '@vue/shared@3.5.34': {} + '@vue/shared@3.5.35': {} + + abab@2.0.6: {} acorn-globals@7.0.1: dependencies: @@ -5983,16 +6727,22 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 + fast-uri: 3.1.2 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 alien-signals@3.2.1: {} + ansi-escapes@1.4.0: {} + + ansi-regex@2.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} + ansi-styles@2.2.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6014,11 +6764,67 @@ snapshots: aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + asciinema-player@3.15.1: dependencies: - '@babel/runtime': 7.29.2 - solid-js: 1.9.12 - solid-transition-group: 0.2.3(solid-js@1.9.12) + '@babel/runtime': 7.29.7 + solid-js: 1.9.13 + solid-transition-group: 0.2.3(solid-js@1.9.13) + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} assertion-error@2.0.1: {} @@ -6026,9 +6832,19 @@ snapshots: astral-regex@2.0.0: {} + async-function@1.0.0: {} + asynckit@0.4.0: {} - axe-core@4.11.4: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + axe-core@4.12.0: {} axobject-query@4.1.0: {} @@ -6038,18 +6854,36 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.27: {} + baseline-browser-mapping@2.10.33: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 binary-extensions@2.3.0: {} + biome@0.3.3: + dependencies: + bluebird: 3.7.2 + chalk: 1.1.3 + commander: 2.20.3 + editor: 1.0.0 + fs-promise: 0.5.0 + inquirer-promise: 0.0.3 + request-promise: 3.0.0 + untildify: 3.0.3 + user-home: 2.0.0 + + bluebird@3.7.2: {} + boolbase@1.0.0: {} - brace-expansion@1.1.14: + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -6059,10 +6893,10 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.27 - caniuse-lite: 1.0.30001791 - electron-to-chromium: 1.5.349 - node-releases: 2.0.38 + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer@5.7.1: @@ -6072,26 +6906,53 @@ snapshots: builtin-modules@3.3.0: {} - builtin-modules@5.1.0: {} + builtin-modules@5.2.0: {} bytes@3.1.2: {} - cacheable@2.3.4: + cacheable@2.3.5: dependencies: - '@cacheable/memory': 2.0.8 + '@cacheable/memory': 2.0.9 '@cacheable/utils': 2.4.1 hookified: 1.15.1 keyv: 5.6.0 - qified: 0.9.1 + qified: 0.10.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 callsites@3.1.0: {} camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001791: {} + caniuse-lite@1.0.30001793: {} + + caseless@0.12.0: {} chai@6.2.2: {} + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6109,10 +6970,10 @@ snapshots: dependencies: '@kurkle/color': 0.3.4 - chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.20): + chartjs-adapter-dayjs-4@1.0.4(chart.js@4.5.1)(dayjs@1.11.21): dependencies: chart.js: 4.5.1 - dayjs: 1.11.20 + dayjs: 1.11.21 chartjs-plugin-zoom@2.2.0(chart.js@4.5.1): dependencies: @@ -6144,8 +7005,16 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cli-cursor@1.0.2: + dependencies: + restore-cursor: 1.0.1 + + cli-width@1.1.1: {} + clippie@4.2.0: {} + code-point-at@1.1.0: {} + codemirror-lang-elixir@4.0.1: dependencies: '@codemirror/language': 6.12.3 @@ -6153,7 +7022,7 @@ snapshots: codemirror-spell-checker@1.1.2: dependencies: - typo-js: 1.3.1 + typo-js: 1.3.2 codemirror@5.65.21: {} @@ -6173,28 +7042,32 @@ snapshots: commander@14.0.3: {} + commander@2.20.3: {} + commander@4.1.1: {} commander@7.2.0: {} commander@8.3.0: {} - comment-parser@1.4.6: {} + comment-parser@1.4.7: {} compare-versions@6.1.1: {} concat-map@0.0.1: {} - confbox@0.1.8: {} - convert-source-map@2.0.0: {} core-js-compat@3.49.0: dependencies: browserslist: 4.28.2 + core-js@2.6.12: {} + core-js@3.32.2: {} + core-util-is@1.0.2: {} + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -6260,17 +7133,17 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.3): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.3 + cytoscape: 3.33.4 - cytoscape-fcose@2.2.0(cytoscape@3.33.3): + cytoscape-fcose@2.2.0(cytoscape@3.33.4): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.3 + cytoscape: 3.33.4 - cytoscape@3.33.3: {} + cytoscape@3.33.4: {} d3-array@2.12.1: dependencies: @@ -6446,13 +7319,35 @@ snapshots: damerau-levenshtein@1.0.8: {} + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + data-urls@3.0.2: dependencies: - abab: '@nolyfill/abab@1.0.44' + abab: 2.0.6 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - dayjs@1.11.20: {} + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.21: {} debug@3.2.7: dependencies: @@ -6477,6 +7372,18 @@ snapshots: kind-of: 3.2.2 rename-keys: 1.2.0 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 @@ -6517,7 +7424,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.2: + dompurify@3.4.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6527,6 +7434,19 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + earlgrey-runtime@0.1.2: + dependencies: + core-js: 2.6.12 + kaiser: 0.0.4 + lodash: 4.18.1 + regenerator-runtime: 0.9.6 + easymde@2.21.0: dependencies: '@types/codemirror': 5.60.17 @@ -6535,7 +7455,14 @@ snapshots: codemirror-spell-checker: 1.1.2 marked: 4.3.0 - electron-to-chromium@1.5.349: {} + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + editor@1.0.0: {} + + electron-to-chromium@1.5.364: {} elkjs@0.9.3: {} @@ -6555,11 +7482,91 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.21 + + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-module-lexer@2.1.0: {} - es-toolkit@1.46.1: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.47.0: {} esbuild@0.28.0: optionalDependencies: @@ -6610,45 +7617,45 @@ snapshots: dependencies: eslint: 10.4.0(jiti@2.7.0) - eslint-import-context@0.1.9(unrs-resolver@1.11.1): + eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.14.0 stable-hash-x: 0.2.0 optionalDependencies: - unrs-resolver: 1.11.1 + unrs-resolver: 1.12.2 eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: '@nolyfill/is-core-module@1.0.39' - resolve: 2.0.0-next.6 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)): dependencies: debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.4.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -6685,8 +7692,8 @@ snapshots: '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) aria-query: 5.3.2 eslint: 10.4.0(jiti@2.7.0) eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) @@ -6694,17 +7701,17 @@ snapshots: eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-filenames: 1.3.2(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-i18n-text: 1.0.1(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.0(jiti@2.7.0)) eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 prettier: 3.8.3 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + typescript-eslint: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript @@ -6715,49 +7722,49 @@ snapshots: dependencies: eslint: 10.4.0(jiti@2.7.0) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.59.4 - comment-parser: 1.4.6 + '@typescript-eslint/types': 8.60.1 + comment-parser: 1.4.7 debug: 4.4.3 eslint: 10.4.0(jiti@2.7.0) - eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + eslint-import-context: 0.1.9(unrs-resolver@1.12.2) is-glob: 4.0.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.1 stable-hash-x: 0.2.0 - unrs-resolver: 1.11.1 + unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 - array-includes: '@nolyfill/array-includes@1.0.44' - array.prototype.findlastindex: '@nolyfill/array.prototype.findlastindex@1.0.44' - array.prototype.flat: '@nolyfill/array.prototype.flat@1.0.44' - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 eslint: 10.4.0(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - hasown: '@nolyfill/hasown@1.0.44' - is-core-module: '@nolyfill/is-core-module@1.0.39' + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) + hasown: 2.0.4 + is-core-module: 2.16.2 is-glob: 4.0.3 minimatch: 3.1.5 - object.fromentries: '@nolyfill/object.fromentries@1.0.44' - object.groupby: '@nolyfill/object.groupby@1.0.44' - object.values: '@nolyfill/object.values@1.0.44' + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 semver: 6.3.1 - string.prototype.trimend: '@nolyfill/string.prototype.trimend@1.0.44' + string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -6766,21 +7773,21 @@ snapshots: eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.0(jiti@2.7.0)): dependencies: aria-query: 5.3.2 - array-includes: '@nolyfill/array-includes@1.0.44' - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.4 + axe-core: 4.12.0 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 10.4.0(jiti@2.7.0) - hasown: '@nolyfill/hasown@1.0.44' + hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 minimatch: 3.1.5 - object.fromentries: '@nolyfill/object.fromentries@1.0.44' - safe-regex-test: '@nolyfill/safe-regex-test@1.0.44' - string.prototype.includes: '@nolyfill/string.prototype.includes@1.0.44' + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 eslint-plugin-no-only-tests@3.4.0: {} @@ -6789,12 +7796,12 @@ snapshots: eslint: 10.4.0(jiti@2.7.0) globals: 17.6.0 - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): dependencies: eslint: 10.4.0(jiti@2.7.0) prettier: 3.8.3 prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 + synckit: 0.11.13 optionalDependencies: eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) @@ -6802,7 +7809,7 @@ snapshots: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 - comment-parser: 1.4.6 + comment-parser: 1.4.7 eslint: 10.4.0(jiti@2.7.0) jsdoc-type-pratt-parser: 7.2.0 refa: 0.12.1 @@ -6821,13 +7828,13 @@ snapshots: lodash.merge: 4.6.2 minimatch: 10.2.5 scslre: 0.3.0 - semver: 7.8.0 + semver: 7.8.1 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 eslint-plugin-unicorn@64.0.0(eslint@10.4.0(jiti@2.7.0)): dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) change-case: 5.4.4 ci-info: 4.4.0 @@ -6842,32 +7849,32 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.1 - semver: 7.8.0 + semver: 7.8.1 strip-indent: 4.1.1 eslint-plugin-vue-scoped-css@3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - es-toolkit: 1.46.1 + es-toolkit: 1.47.0 eslint: 10.4.0(jiti@2.7.0) postcss: 8.5.15 postcss-safe-parser: 7.0.1(postcss@8.5.15) postcss-selector-parser: 7.1.1 vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): + eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) eslint: 10.4.0(jiti@2.7.0) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 - semver: 7.8.0 + semver: 7.8.1 vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-wc@3.1.0(eslint@10.4.0(jiti@2.7.0)): dependencies: @@ -6880,7 +7887,7 @@ snapshots: eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 @@ -6897,11 +7904,11 @@ snapshots: '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.1 + '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 debug: 4.4.3 @@ -6955,7 +7962,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 esutils@2.0.3: {} @@ -6963,8 +7970,14 @@ snapshots: events@3.3.0: {} + exit-hook@1.1.1: {} + expect-type@1.3.0: {} + extend@3.0.2: {} + + extsprintf@1.3.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6981,7 +7994,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} + fast-uri@3.1.2: {} fastest-levenshtein@1.0.16: {} @@ -7001,6 +8014,11 @@ snapshots: fflate@0.8.2: {} + figures@1.7.0: + dependencies: + escape-string-regexp: 1.0.5 + object-assign: 4.1.1 + file-entry-cache@11.1.3: dependencies: flat-cache: 6.1.22 @@ -7027,34 +8045,106 @@ snapshots: flat-cache@6.1.22: dependencies: - cacheable: 2.3.4 + cacheable: 2.3.5 flatted: 3.4.2 hookified: 1.15.1 flatted@3.4.2: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 - es-set-tostringtag: '@nolyfill/es-set-tostringtag@1.0.44' - hasown: '@nolyfill/hasown@1.0.44' + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 mime-types: 2.1.35 + fs-extra@0.26.7: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + klaw: 1.3.1 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + + fs-promise@0.5.0: + dependencies: + any-promise: 1.3.0 + fs-extra: 0.26.7 + mz: 2.7.0 + thenify-all: 1.6.0 + + fs.realpath@1.0.0: {} + fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.4 + is-callable: 1.2.7 + functional-red-black-tree@1.0.1: {} - get-east-asian-width@1.5.0: {} + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7063,6 +8153,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -7079,6 +8178,11 @@ snapshots: globals@17.6.0: {} + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + globby@16.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -7090,6 +8194,8 @@ snapshots: globjoin@0.1.4: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} hachure-fill@0.5.2: {} @@ -7103,19 +8209,50 @@ snapshots: '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.20.0 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.15.0 + har-schema: 2.0.0 + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-bigints@1.1.0: {} + has-flag@4.0.0: {} has-flag@5.0.1: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hashery@1.5.1: dependencies: hookified: 1.15.1 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hookified@1.15.1: {} hookified@2.2.0: {} @@ -7141,6 +8278,12 @@ snapshots: transitivePeerDependencies: - supports-color + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -7150,7 +8293,7 @@ snapshots: iconv-lite@0.6.3: dependencies: - safer-buffer: '@nolyfill/safer-buffer@1.0.44' + safer-buffer: 2.1.2 idiomorph@0.7.4: {} @@ -7171,10 +8314,44 @@ snapshots: indent-string@5.0.0: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + ini@1.3.8: {} ini@4.1.3: {} + inquirer-promise@0.0.3: + dependencies: + earlgrey-runtime: 0.1.2 + inquirer: 0.11.4 + + inquirer@0.11.4: + dependencies: + ansi-escapes: 1.4.0 + ansi-regex: 2.1.1 + chalk: 1.1.3 + cli-cursor: 1.0.2 + cli-width: 1.1.1 + figures: 1.7.0 + lodash: 3.10.1 + readline2: 1.0.1 + run-async: 0.1.0 + rx-lite: 3.1.2 + string-width: 1.0.2 + strip-ansi: 3.0.1 + through: 2.3.8 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.0 + internmap@1.0.1: {} internmap@2.0.3: {} @@ -7186,46 +8363,156 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-buffer@1.1.6: {} is-builtin-module@5.0.0: dependencies: - builtin-modules: 5.1.0 + builtin-modules: 5.2.0 is-bun-module@2.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.1 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 is-decimal@2.0.1: {} is-extglob@2.1.1: {} + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@1.0.0: + dependencies: + number-is-nan: 1.0.1 + is-fullwidth-code-point@3.0.0: {} + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-number@7.0.0: {} is-path-inside@4.0.0: {} is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.21 + + is-typedarray@1.0.0: {} + is-valid-element-name@1.0.0: dependencies: is-potential-custom-element-name: 1.0.1 + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + isexe@2.0.0: {} + isstream@0.1.2: {} + jest-environment-jsdom@29.7.0: dependencies: '@jest/environment': 29.7.0 @@ -7243,7 +8530,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -7284,11 +8571,13 @@ snapshots: dependencies: argparse: 2.0.1 + jsbn@0.1.1: {} + jsdoc-type-pratt-parser@7.2.0: {} jsdom@20.0.3: dependencies: - abab: '@nolyfill/abab@1.0.44' + abab: 2.0.6 acorn: 8.16.0 acorn-globals: 7.0.1 cssom: 0.5.0 @@ -7312,7 +8601,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.20.0 + ws: 8.21.0 xml-name-validator: 4.0.0 transitivePeerDependencies: - bufferutil @@ -7329,29 +8618,52 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} + json5@1.0.2: dependencies: minimist: 1.2.8 jsonc-parser@3.3.1: {} + jsonfile@2.4.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonpointer@5.0.1: {} + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + jsx-ast-utils-x@0.1.0: {} jsx-ast-utils@3.3.5: dependencies: - array-includes: '@nolyfill/array-includes@1.0.44' - array.prototype.flat: '@nolyfill/array.prototype.flat@1.0.44' - object.assign: '@nolyfill/object.assign@1.0.44' - object.values: '@nolyfill/object.values@1.0.44' + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + kaiser@0.0.4: + dependencies: + earlgrey-runtime: 0.1.2 katex@0.16.47: dependencies: commander: 8.3.0 + katex@0.17.0: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -7368,6 +8680,10 @@ snapshots: kind-of@6.0.3: {} + klaw@1.3.1: + optionalDependencies: + graceful-fs: 4.2.11 + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -7441,7 +8757,7 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: + linkify-it@5.0.1: dependencies: uc.micro: 2.1.0 @@ -7463,6 +8779,10 @@ snapshots: lodash.upperfirst@4.3.1: {} + lodash@3.10.1: {} + + lodash@4.18.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7471,7 +8791,7 @@ snapshots: dependencies: argparse: 2.0.1 entities: 4.5.0 - linkify-it: 5.0.0 + linkify-it: 5.0.1 mdurl: 2.0.0 punycode.js: 2.3.1 uc.micro: 2.1.0 @@ -7489,7 +8809,7 @@ snapshots: minimatch: 10.2.5 run-con: 1.3.2 smol-toml: 1.6.1 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 transitivePeerDependencies: - supports-color @@ -7511,13 +8831,16 @@ snapshots: marked@4.3.0: {} - material-icon-theme@5.34.0: + material-icon-theme@5.35.0: dependencies: + biome: 0.3.3 chroma-js: 3.2.0 events: 3.3.0 fast-deep-equal: 3.1.3 svgson: 5.3.1 + math-intrinsics@1.1.0: {} + mathml-tag-names@4.0.0: {} mdn-data@2.0.28: {} @@ -7533,26 +8856,26 @@ snapshots: mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.1 + '@iconify/utils': 3.1.3 '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.3 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.3) - cytoscape-fcose: 2.2.0(cytoscape@3.33.3) + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 - dayjs: 1.11.20 - dompurify: 3.4.2 - es-toolkit: 1.46.1 + dayjs: 1.11.21 + dompurify: 3.4.7 + es-toolkit: 1.47.0 katex: 0.16.47 khroma: 2.1.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.2.0 - uuid: 11.1.1 + uuid: 14.0.0 micromark-core-commonmark@2.0.3: dependencies: @@ -7739,27 +9062,22 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 1.1.15 minimist@1.2.8: {} - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.4 - moo@0.5.3: {} ms@2.1.3: {} muggle-string@0.4.1: {} + mute-stream@0.0.5: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -7776,9 +9094,9 @@ snapshots: node-exports-info@1.6.0: dependencies: - array.prototype.flatmap: '@nolyfill/array.prototype.flatmap@1.0.44' + array.prototype.flatmap: 1.3.3 es-errors: 1.3.0 - object.entries: '@nolyfill/object.entries@1.0.44' + object.entries: 1.1.9 semver: 6.3.1 node-fetch@2.6.13: @@ -7789,9 +9107,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.38: {} - - nolyfill@1.0.44: {} + node-releases@2.0.46: {} normalize-path@3.0.0: {} @@ -7799,14 +9115,64 @@ snapshots: dependencies: boolbase: 1.0.0 + number-is-nan@1.0.1: {} + nwsapi@2.2.23: {} + oauth-sign@0.9.0: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@1.1.0: {} + online-3d-viewer@0.18.0: dependencies: '@simonwep/pickr': 1.9.0 @@ -7822,6 +9188,14 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + os-homedir@1.0.2: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -7848,7 +9222,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -7863,6 +9237,8 @@ snapshots: path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -7873,6 +9249,8 @@ snapshots: perfect-debounce@2.1.0: {} + performance-now@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -7883,12 +9261,6 @@ snapshots: pirates@4.0.7: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - playwright-core@1.60.0: {} playwright@1.60.0: @@ -7906,6 +9278,8 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 + possible-typed-array-names@1.1.0: {} + postcss-html@1.8.1: dependencies: htmlparser2: 8.0.2 @@ -7985,10 +9359,12 @@ snapshots: punycode@2.3.1: {} - qified@0.9.1: + qified@0.10.1: dependencies: hookified: 2.2.0 + qs@6.5.5: {} + querystringify@2.2.0: {} queue-microtask@1.2.3: {} @@ -8003,10 +9379,29 @@ snapshots: dependencies: picomatch: 2.3.2 + readline2@1.0.1: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + mute-stream: 0.0.5 + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerator-runtime@0.9.6: {} + regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -8014,12 +9409,50 @@ snapshots: regexp-tree@0.1.27: {} + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + regjsparser@0.13.1: dependencies: jsesc: 3.1.0 rename-keys@1.2.0: {} + request-promise@3.0.0: + dependencies: + bluebird: 3.7.2 + lodash: 4.18.1 + request: 2.88.2 + + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.5 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + require-from-string@2.0.2: {} requires-port@1.0.0: {} @@ -8031,47 +9464,56 @@ snapshots: resolve@1.22.12: dependencies: es-errors: 1.3.0 - is-core-module: '@nolyfill/is-core-module@1.0.39' + is-core-module: 2.16.2 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: + resolve@2.0.0-next.7: dependencies: es-errors: 1.3.0 - is-core-module: '@nolyfill/is-core-module@1.0.39' + is-core-module: 2.16.2 node-exports-info: 1.6.0 - object-keys: '@nolyfill/object-keys@1.0.44' + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@1.0.1: + dependencies: + exit-hook: 1.1.1 + onetime: 1.1.0 + reusify@1.1.0: {} + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + robust-predicates@3.0.3: {} - rolldown-license-plugin@3.0.7(rolldown@1.0.1): + rolldown-license-plugin@3.0.8(rolldown@1.0.2): dependencies: - rolldown: 1.0.1 + rolldown: 1.0.2 - rolldown@1.0.1: + rolldown@1.0.2: dependencies: - '@oxc-project/types': 0.130.0 + '@oxc-project/types': 0.132.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.1 - '@rolldown/binding-darwin-arm64': 1.0.1 - '@rolldown/binding-darwin-x64': 1.0.1 - '@rolldown/binding-freebsd-x64': 1.0.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 - '@rolldown/binding-linux-arm64-gnu': 1.0.1 - '@rolldown/binding-linux-arm64-musl': 1.0.1 - '@rolldown/binding-linux-ppc64-gnu': 1.0.1 - '@rolldown/binding-linux-s390x-gnu': 1.0.1 - '@rolldown/binding-linux-x64-gnu': 1.0.1 - '@rolldown/binding-linux-x64-musl': 1.0.1 - '@rolldown/binding-openharmony-arm64': 1.0.1 - '@rolldown/binding-wasm32-wasi': 1.0.1 - '@rolldown/binding-win32-arm64-msvc': 1.0.1 - '@rolldown/binding-win32-x64-msvc': 1.0.1 + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 roughjs@4.6.6: dependencies: @@ -8080,6 +9522,10 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 + run-async@0.1.0: + dependencies: + once: 1.4.0 + run-con@1.3.2: dependencies: deep-extend: 0.6.0 @@ -8093,6 +9539,31 @@ snapshots: rw@1.3.3: {} + rx-lite@3.1.2: {} + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + sax@1.6.0: {} saxes@6.0.0: @@ -8107,13 +9578,35 @@ snapshots: semver@6.3.1: {} - semver@7.8.0: {} + semver@7.8.1: {} - seroval-plugins@1.5.3(seroval@1.5.3): + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: - seroval: 1.5.3 + seroval: 1.5.4 - seroval@1.5.3: {} + seroval@1.5.4: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 shebang-command@2.0.0: dependencies: @@ -8121,6 +9614,34 @@ snapshots: shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -8137,17 +9658,17 @@ snapshots: smol-toml@1.6.1: {} - solid-js@1.9.12: + solid-js@1.9.13: dependencies: csstype: 3.2.3 - seroval: 1.5.3 - seroval-plugins: 1.5.3(seroval@1.5.3) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) - solid-transition-group@0.2.3(solid-js@1.9.12): + solid-transition-group@0.2.3(solid-js@1.9.13): dependencies: - '@solid-primitives/refs': 1.1.3(solid-js@1.9.12) - '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.12) - solid-js: 1.9.12 + '@solid-primitives/refs': 1.1.3(solid-js@1.9.13) + '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.13) + solid-js: 1.9.13 sortablejs@1.15.7: {} @@ -8160,6 +9681,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + stable-hash-x@0.2.0: {} stack-utils@2.0.6: @@ -8170,6 +9703,17 @@ snapshots: std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@1.0.2: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -8178,14 +9722,47 @@ snapshots: string-width@8.1.0: dependencies: - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 string-width@8.2.1: dependencies: - get-east-asian-width: 1.5.0 + get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8224,9 +9801,9 @@ snapshots: stylelint@17.12.0(typescript@6.0.3): dependencies: - '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -8272,11 +9849,13 @@ snapshots: lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-interface-checker: 0.1.13 supports-color@10.2.2: {} + supports-color@2.0.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8320,9 +9899,9 @@ snapshots: transitivePeerDependencies: - encoding - synckit@0.11.12: + synckit@0.11.13: dependencies: - '@pkgr/core': 0.2.9 + '@pkgr/core': 0.3.6 table@6.9.0: dependencies: @@ -8372,11 +9951,13 @@ snapshots: throttle-debounce@5.0.2: {} + through@2.3.8: {} + tinybench@2.9.0: {} - tinyexec@1.1.2: {} + tinyexec@1.2.4: {} - tinyglobby@0.2.16: + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -8393,6 +9974,11 @@ snapshots: toastify-js@1.12.0: {} + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + tough-cookie@4.1.4: dependencies: psl: 1.15.0 @@ -8429,29 +10015,68 @@ snapshots: tslib@2.8.1: {} + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 type-detect@4.0.8: {} - typescript-eslint@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3): + typed-array-buffer@1.0.3: dependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) eslint: 10.4.0(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript-eslint@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) eslint: 10.4.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: @@ -8461,43 +10086,53 @@ snapshots: typescript@6.0.3: {} - typo-js@1.3.1: {} + typo-js@1.3.2: {} uc.micro@2.1.0: {} - ufo@1.6.4: {} - uint8-to-base64@0.2.1: {} + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + undici-types@7.24.6: {} unicorn-magic@0.4.0: {} universalify@0.2.0: {} - unrs-resolver@1.11.1: + unrs-resolver@1.12.2: dependencies: napi-postinstall: 0.3.4 optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + untildify@3.0.3: {} update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: @@ -8505,7 +10140,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.16.13: {} + updates@17.17.2: {} uri-js@4.4.1: dependencies: @@ -8516,33 +10151,45 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + user-home@2.0.0: + dependencies: + os-homedir: 1.0.2 + util-deprecate@1.0.2: {} - uuid@11.1.1: {} + uuid@14.0.0: {} + + uuid@3.4.0: {} vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.4(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + verror@1.10.0: dependencies: - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 - vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): + vite-string-plugin@2.0.4(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + dependencies: + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + + vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.1 - tinyglobby: 0.2.16 + rolldown: 1.0.2 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.1 esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -8556,10 +10203,10 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.16 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.13(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 @@ -8572,14 +10219,14 @@ snapshots: vue-bar-graph@2.2.0(typescript@6.0.3): dependencies: - vue: 3.5.34(typescript@6.0.3) + vue: 3.5.35(typescript@6.0.3) transitivePeerDependencies: - typescript - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.34(typescript@6.0.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.35(typescript@6.0.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.34(typescript@6.0.3) + vue: 3.5.35(typescript@6.0.3) vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)): dependencies: @@ -8589,23 +10236,23 @@ snapshots: eslint-visitor-keys: 5.0.1 espree: 11.2.0 esquery: 1.7.0 - semver: 7.8.0 + semver: 7.8.1 transitivePeerDependencies: - supports-color - vue-tsc@3.3.1(typescript@6.0.3): + vue-tsc@3.3.2(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.1 + '@vue/language-core': 3.3.2 typescript: 6.0.3 - vue@3.5.34(typescript@6.0.3): + vue@3.5.35(typescript@6.0.3): dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-sfc': 3.5.34 - '@vue/runtime-dom': 3.5.34 - '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3)) - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@6.0.3)) + '@vue/shared': 3.5.35 optionalDependencies: typescript: 6.0.3 @@ -8635,6 +10282,47 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.21 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.21: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -8650,11 +10338,13 @@ snapshots: word-wrap@1.2.5: {} + wrappy@1.0.2: {} + write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 - ws@8.20.0: {} + ws@8.21.0: {} xml-lexer@0.2.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c8f7c63fef..c16cfb76cba 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,3 @@ -packages: [ . ] # workaround for https://github.com/SukkaW/nolyfill/issues/119 savePrefix: '' dedupePeerDependents: false updateNotifier: false @@ -8,28 +7,6 @@ peerDependencyRules: allowedVersions: eslint-plugin-github>eslint: '>=9' -overrides: - array-includes: npm:@nolyfill/array-includes@^1 - array.prototype.findlastindex: npm:@nolyfill/array.prototype.findlastindex@^1 - array.prototype.flat: npm:@nolyfill/array.prototype.flat@^1 - array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1 - es-aggregate-error: npm:@nolyfill/es-aggregate-error@^1 - hasown: npm:@nolyfill/hasown@^1 - is-core-module: npm:@nolyfill/is-core-module@^1 - object.assign: npm:@nolyfill/object.assign@^1 - object.fromentries: npm:@nolyfill/object.fromentries@^1 - object.groupby: npm:@nolyfill/object.groupby@^1 - object.values: npm:@nolyfill/object.values@^1 - safe-buffer: npm:@nolyfill/safe-buffer@^1 - safe-regex-test: npm:@nolyfill/safe-regex-test@^1 - safer-buffer: npm:@nolyfill/safer-buffer@^1 - string.prototype.includes: npm:@nolyfill/string.prototype.includes@^1 - string.prototype.trimend: npm:@nolyfill/string.prototype.trimend@^1 - object-keys: npm:@nolyfill/object-keys@^1 - object.entries: npm:@nolyfill/object.entries@^1 - abab: npm:@nolyfill/abab@^1 - es-set-tostringtag: npm:@nolyfill/es-set-tostringtag@^1 - allowBuilds: '@scarf/scarf': false core-js: false diff --git a/public/assets/img/svg/octicon-flag.svg b/public/assets/img/svg/octicon-flag.svg new file mode 100644 index 00000000000..1d757dca08d --- /dev/null +++ b/public/assets/img/svg/octicon-flag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index 6f86c61e515..bbfbb8d78a0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -95,7 +95,7 @@ "matchManagers": ["npm"], "postUpdateOptions": ["pnpmDedupe"], "postUpgradeTasks": { - "commands": ["make svg nolyfill"], + "commands": ["make svg"], "fileFilters": ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "public/assets/img/svg/**"], "executionMode": "branch", }, diff --git a/tools/migrate-nolyfills.ts b/tools/migrate-nolyfills.ts deleted file mode 100644 index 2e5f635272a..00000000000 --- a/tools/migrate-nolyfills.ts +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -// nolyfill writes overrides to package.json#pnpm.overrides which pnpm v11 ignores. -// This moves them to pnpm-workspace.yaml until SukkaW/nolyfill#119 is fixed. -import {readFileSync, writeFileSync} from 'node:fs'; -import {exit} from 'node:process'; -import {fileURLToPath} from 'node:url'; -import {dump} from 'js-yaml'; - -const packagePath = fileURLToPath(new URL('../package.json', import.meta.url)); -const workspacePath = fileURLToPath(new URL('../pnpm-workspace.yaml', import.meta.url)); - -const packageJson: {pnpm?: {overrides?: Record}} = JSON.parse(readFileSync(packagePath, 'utf8')); -const overrides = packageJson.pnpm?.overrides; - -if (!overrides || !Object.keys(overrides).length) { - exit(0); -} - -const block = dump({overrides}, {lineWidth: -1, quotingType: "'"}); -const workspace = readFileSync(workspacePath, 'utf8'); -const overridesRegex = /^overrides:[^\n]*(?:\n(?:[ \t][^\n]*|[ \t]*(?=\n[ \t])))*\n?/m; - -if (!overridesRegex.test(workspace)) { - console.error(`No 'overrides:' block found in pnpm-workspace.yaml`); - exit(1); -} - -writeFileSync(workspacePath, workspace.replace(overridesRegex, block)); - -const pnpm = packageJson.pnpm!; -delete pnpm.overrides; -if (!Object.keys(pnpm).length) delete packageJson.pnpm; -writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); From 9619d93e3b81e9f8fe77f01fe8ec21f1cc343bde Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 1 Jun 2026 22:53:44 -0700 Subject: [PATCH 08/88] chore(deps): update action dependencies (#37964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | Pending | |---|---|---|---|---| | [aws-actions/configure-aws-credentials](https://redirect.github.com/aws-actions/configure-aws-credentials) | action | patch | `v6.1.1` → `v6.1.2` | `v6.1.3` | | [docker/build-push-action](https://redirect.github.com/docker/build-push-action) | action | minor | `v7.1.0` → `v7.2.0` | | | [docker/login-action](https://redirect.github.com/docker/login-action) | action | minor | `v4.1.0` → `v4.2.0` | | | [docker/metadata-action](https://redirect.github.com/docker/metadata-action) | action | minor | `v6.0.0` → `v6.1.0` | | | [docker/setup-buildx-action](https://redirect.github.com/docker/setup-buildx-action) | action | minor | `v4.0.0` → `v4.1.0` | | | [docker/setup-qemu-action](https://redirect.github.com/docker/setup-qemu-action) | action | minor | `v4.0.0` → `v4.1.0` | | | redis | service | digest | `48e78eb` → `e74c9b9` | | --- ### Release Notes
aws-actions/configure-aws-credentials (aws-actions/configure-aws-credentials) ### [`v6.1.2`](https://redirect.github.com/aws-actions/configure-aws-credentials/releases/tag/v6.1.2) [Compare Source](https://redirect.github.com/aws-actions/configure-aws-credentials/compare/v6.1.1...v6.1.2) ##### Bug Fixes - additional filesystem checks ([#​1799](https://redirect.github.com/aws-actions/configure-aws-credentials/issues/1799)) ([c39f282](https://redirect.github.com/aws-actions/configure-aws-credentials/commit/c39f282697aca8a78c522ecf1f7da9899a31432c))
docker/build-push-action (docker/build-push-action) ### [`v7.2.0`](https://redirect.github.com/docker/build-push-action/releases/tag/v7.2.0) [Compare Source](https://redirect.github.com/docker/build-push-action/compare/v7.1.0...v7.2.0) - Bump [@​actions/core](https://redirect.github.com/actions/core) from 3.0.0 to 3.0.1 in [#​1525](https://redirect.github.com/docker/build-push-action/pull/1525) - Bump [@​docker/actions-toolkit](https://redirect.github.com/docker/actions-toolkit) from 0.87.0 to 0.90.0 in [#​1517](https://redirect.github.com/docker/build-push-action/pull/1517) - Bump brace-expansion from 2.0.2 to 5.0.6 in [#​1534](https://redirect.github.com/docker/build-push-action/pull/1534) - Bump fast-xml-builder from 1.1.4 to 1.2.0 in [#​1529](https://redirect.github.com/docker/build-push-action/pull/1529) - Bump fast-xml-parser from 5.5.7 to 5.8.0 in [#​1521](https://redirect.github.com/docker/build-push-action/pull/1521) - Bump postcss from 8.5.6 to 8.5.10 in [#​1526](https://redirect.github.com/docker/build-push-action/pull/1526) - Bump tar from 6.2.1 to 7.5.15 in [#​1533](https://redirect.github.com/docker/build-push-action/pull/1533) **Full Changelog**:
docker/login-action (docker/login-action) ### [`v4.2.0`](https://redirect.github.com/docker/login-action/releases/tag/v4.2.0) [Compare Source](https://redirect.github.com/docker/login-action/compare/v4.1.0...v4.2.0) - Bump [@​actions/core](https://redirect.github.com/actions/core) from 3.0.0 to 3.0.1 in [#​976](https://redirect.github.com/docker/login-action/pull/976) - Bump [@​aws-sdk/client-ecr](https://redirect.github.com/aws-sdk/client-ecr) and [@​aws-sdk/client-ecr-public](https://redirect.github.com/aws-sdk/client-ecr-public) to 3.1050.0 in [#​960](https://redirect.github.com/docker/login-action/pull/960) - Bump [@​docker/actions-toolkit](https://redirect.github.com/docker/actions-toolkit) from 0.86.0 to 0.90.0 in [#​970](https://redirect.github.com/docker/login-action/pull/970) - Bump brace-expansion from 2.0.1 to 5.0.6 in [#​993](https://redirect.github.com/docker/login-action/pull/993) - Bump fast-xml-builder from 1.1.4 to 1.2.0 in [#​985](https://redirect.github.com/docker/login-action/pull/985) - Bump fast-xml-parser from 5.3.6 to 5.8.0 in [#​963](https://redirect.github.com/docker/login-action/pull/963) - Bump http-proxy-agent and https-proxy-agent to 9.0.0 in [#​961](https://redirect.github.com/docker/login-action/pull/961) - Bump postcss from 8.5.6 to 8.5.10 in [#​979](https://redirect.github.com/docker/login-action/pull/979) - Bump tar from 6.2.1 to 7.5.15 in [#​991](https://redirect.github.com/docker/login-action/pull/991) - Bump vite from 7.3.1 to 7.3.3 in [#​986](https://redirect.github.com/docker/login-action/pull/986) **Full Changelog**:
docker/metadata-action (docker/metadata-action) ### [`v6.1.0`](https://redirect.github.com/docker/metadata-action/releases/tag/v6.1.0) [Compare Source](https://redirect.github.com/docker/metadata-action/compare/v6...v6.1.0) - Bump [@​docker/actions-toolkit](https://redirect.github.com/docker/actions-toolkit) from 0.79.0 to 0.90.0 in [#​613](https://redirect.github.com/docker/metadata-action/pull/613) - Bump brace-expansion from 1.1.12 to 5.0.6 in [#​658](https://redirect.github.com/docker/metadata-action/pull/658) [#​630](https://redirect.github.com/docker/metadata-action/pull/630) - Bump csv-parse from 6.1.0 to 6.2.1 in [#​617](https://redirect.github.com/docker/metadata-action/pull/617) - Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​620](https://redirect.github.com/docker/metadata-action/pull/620) - Bump flatted from 3.3.3 to 3.4.2 in [#​623](https://redirect.github.com/docker/metadata-action/pull/623) - Bump glob from 10.3.15 to 10.5.0 in [#​621](https://redirect.github.com/docker/metadata-action/pull/621) - Bump handlebars from 4.7.8 to 4.7.9 in [#​629](https://redirect.github.com/docker/metadata-action/pull/629) - Bump lodash from 4.17.23 to 4.18.1 in [#​639](https://redirect.github.com/docker/metadata-action/pull/639) - Bump moment-timezone from 0.6.0 to 0.6.1 in [#​619](https://redirect.github.com/docker/metadata-action/pull/619) - Bump picomatch from 4.0.3 to 4.0.4 in [#​626](https://redirect.github.com/docker/metadata-action/pull/626) - Bump postcss from 8.5.6 to 8.5.10 in [#​649](https://redirect.github.com/docker/metadata-action/pull/649) - Bump tar from 6.2.1 to 7.5.15 in [#​657](https://redirect.github.com/docker/metadata-action/pull/657) - Bump undici from 6.23.0 to 6.25.0 in [#​614](https://redirect.github.com/docker/metadata-action/pull/614) - Bump vite from 7.3.1 to 7.3.2 in [#​637](https://redirect.github.com/docker/metadata-action/pull/637) **Full Changelog**:
docker/setup-buildx-action (docker/setup-buildx-action) ### [`v4.1.0`](https://redirect.github.com/docker/setup-buildx-action/releases/tag/v4.1.0) [Compare Source](https://redirect.github.com/docker/setup-buildx-action/compare/v4...v4.1.0) - Bump [@​docker/actions-toolkit](https://redirect.github.com/docker/actions-toolkit) from 0.79.0 to 0.90.0 in [#​489](https://redirect.github.com/docker/setup-buildx-action/pull/489) - Bump brace-expansion from 1.1.12 to 5.0.6 in [#​547](https://redirect.github.com/docker/setup-buildx-action/pull/547) [#​508](https://redirect.github.com/docker/setup-buildx-action/pull/508) - Bump fast-xml-builder from 1.0.0 to 1.2.0 in [#​540](https://redirect.github.com/docker/setup-buildx-action/pull/540) - Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​496](https://redirect.github.com/docker/setup-buildx-action/pull/496) - Bump flatted from 3.3.3 to 3.4.2 in [#​499](https://redirect.github.com/docker/setup-buildx-action/pull/499) - Bump glob from 10.3.12 to 13.0.6 in [#​495](https://redirect.github.com/docker/setup-buildx-action/pull/495) - Bump handlebars from 4.7.8 to 4.7.9 in [#​504](https://redirect.github.com/docker/setup-buildx-action/pull/504) - Bump lodash from 4.17.23 to 4.18.1 in [#​523](https://redirect.github.com/docker/setup-buildx-action/pull/523) - Bump picomatch from 4.0.3 to 4.0.4 in [#​503](https://redirect.github.com/docker/setup-buildx-action/pull/503) - Bump postcss from 8.5.6 to 8.5.10 in [#​537](https://redirect.github.com/docker/setup-buildx-action/pull/537) - Bump tar from 6.2.1 to 7.5.15 in [#​545](https://redirect.github.com/docker/setup-buildx-action/pull/545) - Bump undici from 6.23.0 to 6.25.0 in [#​492](https://redirect.github.com/docker/setup-buildx-action/pull/492) - Bump vite from 7.3.1 to 7.3.2 in [#​520](https://redirect.github.com/docker/setup-buildx-action/pull/520) **Full Changelog**:
docker/setup-qemu-action (docker/setup-qemu-action) ### [`v4.1.0`](https://redirect.github.com/docker/setup-qemu-action/releases/tag/v4.1.0) [Compare Source](https://redirect.github.com/docker/setup-qemu-action/compare/v4...v4.1.0) - Add `reset` input to uninstall current emulators by [@​crazy-max](https://redirect.github.com/crazy-max) in [#​21](https://redirect.github.com/docker/setup-qemu-action/pull/21) - Bump [@​docker/actions-toolkit](https://redirect.github.com/docker/actions-toolkit) from 0.77.0 to 0.91.0 in [#​250](https://redirect.github.com/docker/setup-qemu-action/pull/250) [#​247](https://redirect.github.com/docker/setup-qemu-action/pull/247) - Bump brace-expansion from 1.1.12 to 1.1.15 in [#​265](https://redirect.github.com/docker/setup-qemu-action/pull/265) - Bump fast-xml-builder from 1.0.0 to 1.2.0 in [#​286](https://redirect.github.com/docker/setup-qemu-action/pull/286) - Bump fast-xml-parser from 5.4.2 to 5.8.0 in [#​255](https://redirect.github.com/docker/setup-qemu-action/pull/255) - Bump flatted from 3.3.3 to 3.4.2 in [#​257](https://redirect.github.com/docker/setup-qemu-action/pull/257) - Bump glob from 10.3.15 to 10.5.0 in [#​254](https://redirect.github.com/docker/setup-qemu-action/pull/254) - Bump handlebars from 4.7.8 to 4.7.9 in [#​262](https://redirect.github.com/docker/setup-qemu-action/pull/262) - Bump lodash from 4.17.23 to 4.18.1 in [#​273](https://redirect.github.com/docker/setup-qemu-action/pull/273) - Bump postcss from 8.5.6 to 8.5.10 in [#​285](https://redirect.github.com/docker/setup-qemu-action/pull/285) - Bump tar from 6.2.1 to 7.5.15 in [#​287](https://redirect.github.com/docker/setup-qemu-action/pull/287) - Bump tmp from 0.2.5 to 0.2.6 in [#​291](https://redirect.github.com/docker/setup-qemu-action/pull/291) - Bump undici from 6.23.0 to 6.26.0 in [#​251](https://redirect.github.com/docker/setup-qemu-action/pull/251) - Bump vite from 7.3.1 to 7.3.2 in [#​271](https://redirect.github.com/docker/setup-qemu-action/pull/271) **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: Lunny Xiao Co-authored-by: silverwind --- .github/actions/docker-dryrun/action.yml | 8 ++++---- .github/workflows/pull-db-tests.yml | 2 +- .github/workflows/release-nightly.yml | 18 +++++++++--------- .github/workflows/release-tag-rc.yml | 18 +++++++++--------- .github/workflows/release-tag-version.yml | 18 +++++++++--------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/actions/docker-dryrun/action.yml b/.github/actions/docker-dryrun/action.yml index d280ea26ce7..e9cd88d46f6 100644 --- a/.github/actions/docker-dryrun/action.yml +++ b/.github/actions/docker-dryrun/action.yml @@ -9,10 +9,10 @@ inputs: runs: using: composite steps: - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Build regular image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: ${{ inputs.platform }} @@ -20,7 +20,7 @@ runs: file: Dockerfile cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful - name: Build rootless image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: ${{ inputs.platform }} diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 641a3cacb8b..cbf86247ce1 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -131,7 +131,7 @@ jobs: ports: - "7700:7700" redis: - image: redis:latest@sha256:48e78eb9d1e1adcfb10184b2cc3c7fc5ed21e5a3be08875f239257d194bab8c9 + image: redis:latest@sha256:e74c9b933d78e2829583d88f92793f4524752a15ac59c8baff2dd5ed000b7432 options: >- # wait until redis has started --health-cmd "redis-cli ping" --health-interval 5s diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 82ebf79a61e..d1561329431 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -57,7 +57,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -79,8 +79,8 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Get cleaned branch name id: clean_name env: @@ -88,7 +88,7 @@ jobs: run: | REF_NAME=$(echo "$REF" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta with: images: |- @@ -98,7 +98,7 @@ jobs: type=raw,value=${{ steps.clean_name.outputs.branch }} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -112,18 +112,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular docker image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -133,7 +133,7 @@ jobs: cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index b246d87b9ad..3e7655027c4 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -58,7 +58,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -90,9 +90,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta with: images: |- @@ -105,7 +105,7 @@ jobs: type=semver,pattern={{version}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -121,18 +121,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -140,7 +140,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 9eb910c8e36..66a2984def3 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -61,7 +61,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -93,9 +93,9 @@ jobs: # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta with: images: |- @@ -112,7 +112,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 + - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 id: meta_rootless with: images: |- @@ -133,18 +133,18 @@ jobs: annotations: | org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: build regular container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 @@ -152,7 +152,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} annotations: ${{ steps.meta.outputs.annotations }} - name: build rootless container image - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 From 79810ba2e37a5b5b7840a7737a877fc7f1ea7c38 Mon Sep 17 00:00:00 2001 From: puni9869 <80308335+puni9869@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:38:23 +0530 Subject: [PATCH 09/88] fix: use committer time where ever possible as default (#37969) Fix https://github.com/go-gitea/gitea/issues/37857 --------- Co-authored-by: wxiaoguang --- MAINTAINERS | 2 +- routers/web/repo/wiki.go | 6 +++--- templates/repo/commit_page.tmpl | 3 +-- templates/repo/wiki/revision.tmpl | 4 ++-- templates/repo/wiki/view.tmpl | 4 ++-- tests/integration/repo_commits_test.go | 22 ++++++++++++++++++++++ 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/MAINTAINERS b/MAINTAINERS index 26192eff447..03ff6999f33 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -64,4 +64,4 @@ metiftikci (@metiftikci) Christopher Homberger (@ChristopherHX) Tobias Balle-Petersen (@tobiasbp) TheFox (@TheFox0x7) -Nicolas (@bircni) \ No newline at end of file +Nicolas (@bircni) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 4d9cd21c132..dcb0c25829f 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -496,7 +496,7 @@ func Wiki(ctx *context.Context) { ctx.ServerError("GetCommitByPath", err) return } - ctx.Data["Author"] = lastCommit.Author + ctx.Data["Committer"] = lastCommit.Committer ctx.HTML(http.StatusOK, tplWikiView) } @@ -528,7 +528,7 @@ func WikiRevision(ctx *context.Context) { ctx.ServerError("GetCommitByPath", err) return } - ctx.Data["Author"] = lastCommit.Author + ctx.Data["Committer"] = lastCommit.Committer ctx.HTML(http.StatusOK, tplWikiRevision) } @@ -587,7 +587,7 @@ func WikiPages(ctx *context.Context) { Name: displayName, SubURL: wiki_service.WebPathToURLPath(wikiName), GitEntryName: entry.Entry.Name(), - UpdatedUnix: timeutil.TimeStamp(entry.Commit.Author.When.Unix()), + UpdatedUnix: timeutil.TimeStamp(entry.Commit.Committer.When.Unix()), }) } ctx.Data["Pages"] = pages diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 975cd303ec4..9618eda7612 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -139,8 +139,7 @@ {{.Commit.Author.Name}} {{end}} - - {{DateUtils.TimeSince .Commit.Author.When}} + {{DateUtils.TimeSince .Commit.Committer.When}}
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index c59047a6404..a9df43ea468 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -9,8 +9,8 @@
{{$title}}
- {{$timeSince := DateUtils.TimeSince .Author.When}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} + {{$timeSince := DateUtils.TimeSince .Committer.When}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Committer.Name $timeSince}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 4c7ef364d29..967a8814c96 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -37,8 +37,8 @@
{{$title}}
- {{$timeSince := DateUtils.TimeSince .Author.When}} - {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}} + {{$timeSince := DateUtils.TimeSince .Committer.When}} + {{ctx.Locale.Tr "repo.wiki.last_commit_info" .Committer.Name $timeSince}}
diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index ab7119769f8..9335ef70656 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -69,6 +69,28 @@ func TestRepoCommits(t *testing.T) { assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text())) }) + t.Run("CommitPageUsesCommitterDate", func(t *testing.T) { + const ( + commitID = "5099b81332712fe655e34e8dd63574f503f61811" + expectedCommitterTime = "2017-08-06T19:56:13+02:00" + authorTime = "2017-08-06T19:55:01+02:00" + ) + + req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + var commitListTime string + doc.doc.Find("#commits-table tbody tr").EachWithBreak(func(_ int, row *goquery.Selection) bool { + if path.Base(row.Find(".commit-id-short").AttrOr("href", "")) != commitID { + return true + } + commitListTime = row.Find("td").Eq(3).Find("relative-time").AttrOr("datetime", "") + return false + }) + require.Equal(t, expectedCommitterTime, commitListTime) + }) + t.Run("LastCommitNonExistingCommiter", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2") resp := session.MakeRequest(t, req, http.StatusOK) From fbaaac9c147fc3d40b5ac44dd6aaac28637a2d28 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 4 Jun 2026 00:12:02 +0800 Subject: [PATCH 10/88] fix: remove "no-transfrom" from the cache-control header (#37985) Cloudflare has officially removed the "auto-minify" feature https://community.cloudflare.com/t/655677, so we don't need such option anymore. Fix #34521 --- modules/httpcache/httpcache.go | 17 +++++------------ modules/httpcache/httpcache_test.go | 2 +- modules/httplib/serve.go | 5 ++--- routers/common/errpage.go | 2 +- services/context/api.go | 2 +- services/context/context.go | 2 +- 6 files changed, 11 insertions(+), 19 deletions(-) diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index f55e13ce594..785363c29ca 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -15,9 +15,8 @@ import ( ) type CacheControlOptions struct { - IsPublic bool - MaxAge time.Duration - NoTransform bool + IsPublic bool + MaxAge time.Duration } // SetCacheControlInHeader sets suitable cache-control headers in the response @@ -38,25 +37,19 @@ func SetCacheControlInHeader(h http.Header, opts *CacheControlOptions) { directives = append(directives, "max-age=0", publicPrivate, "must-revalidate") h.Set("X-Gitea-Debug", fmt.Sprintf("RUN_MODE=%v, MaxAge=%s", setting.RunMode, opts.MaxAge)) } - - if opts.NoTransform { - directives = append(directives, "no-transform") - } h.Set("Cache-Control", strings.Join(directives, ", ")) } func CacheControlForPublicStatic() *CacheControlOptions { return &CacheControlOptions{ - IsPublic: true, - MaxAge: setting.StaticCacheTime, - NoTransform: true, + IsPublic: true, + MaxAge: setting.StaticCacheTime, } } func CacheControlForPrivateStatic() *CacheControlOptions { return &CacheControlOptions{ - MaxAge: setting.StaticCacheTime, - NoTransform: true, + MaxAge: setting.StaticCacheTime, } } diff --git a/modules/httpcache/httpcache_test.go b/modules/httpcache/httpcache_test.go index f9625981aaf..d715cac4a24 100644 --- a/modules/httpcache/httpcache_test.go +++ b/modules/httpcache/httpcache_test.go @@ -18,7 +18,7 @@ func TestHandleGenericETagCache(t *testing.T) { matchedEtag := `"matched-etag"` lastModifiedTime := new(time.Date(2021, time.January, 2, 15, 4, 5, 0, time.FixedZone("test-zone", 8*3600))) lastModified := lastModifiedTime.UTC().Format(http.TimeFormat) - cacheControl := "max-age=0, private, must-revalidate, no-transform" + cacheControl := "max-age=0, private, must-revalidate" type testCase struct { name string reqHeaders map[string]string diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index cf2e049cb7a..e2da9e85fa7 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -94,9 +94,8 @@ func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { } httpcache.SetCacheControlInHeader(header, &httpcache.CacheControlOptions{ - IsPublic: opts.CacheIsPublic, - MaxAge: opts.CacheDuration, - NoTransform: true, + IsPublic: opts.CacheIsPublic, + MaxAge: opts.CacheDuration, }) if !opts.LastModified.IsZero() { diff --git a/routers/common/errpage.go b/routers/common/errpage.go index bb89070aaab..1426b05ef93 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -32,7 +32,7 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in } } - httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(w.Header(), &httpcache.CacheControlOptions{}) tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req)) w.WriteHeader(respCode) diff --git a/services/context/api.go b/services/context/api.go index 97a97ecc550..05b6490826a 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -242,7 +242,7 @@ func APIContexter() func(http.Handler) http.Handler { } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{}) next.ServeHTTP(ctx.Resp, ctx.Req) }) } diff --git a/services/context/context.go b/services/context/context.go index 2875bb46161..c1e438975f6 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -196,7 +196,7 @@ func Contexter() func(next http.Handler) http.Handler { } } - httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true}) + httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{}) ctx.Data["SystemConfig"] = setting.Config() From 623bb81bb95ee5738197955850df308cc1f45528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20G=C3=B3ra?= <108656216+dawidgora@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:30:30 +0200 Subject: [PATCH 11/88] fix(releases): generate notes for initial tag (#37697) Fixes https://github.com/go-gitea/gitea/issues/37286 Automatic release notes for the first release in a repository were empty when there was no previous tag. Before this change, the release notes generator used the tag name to build the changelog link, but reused that state for pull request collection. When `PreviousTag` was empty, the PR collection logic did not scan a useful commit range, so merged pull requests were omitted from the generated notes. This pull request fixes that by decoupling the internal PR collection range from the rendered changelog link: - when a previous tag exists, behavior stays unchanged - when no previous tag exists, release notes collect merged pull requests from the full reachable history up to the target tag - the displayed full changelog link for the first release still uses the existing `/commits/tag/{tag}` format Tests were updated to cover: - generating notes for a repository with no previous tags - including merged pull requests before the first tag - preserving existing behavior when a previous tag exists --- services/release/notes.go | 49 ++++++++++++++++++++++------- services/release/notes_test.go | 56 ++++++++++++++++++++++++++++------ 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/services/release/notes.go b/services/release/notes.go index b8baeb9620a..e62335c98b5 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -10,6 +10,7 @@ import ( "slices" "strings" + "gitea.dev/models/db" issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" @@ -32,18 +33,23 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR return "", err } - if opts.PreviousTag == "" { - // no previous tag, usually due to there is no tag in the repo, use the same content as GitHub - content := fmt.Sprintf("**Full Changelog**: %s/commits/tag/%s\n", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName)) - return content, nil + isFirstRelease, err := isFirstRelease(ctx, repo.ID) + if err != nil { + return "", fmt.Errorf("isFirstRelease: %w", err) } - baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) - if err != nil { + baseCommitID := "" + if opts.PreviousTag != "" { + baseCommit, err := gitRepo.GetCommit(opts.PreviousTag) + if err != nil { + return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) + } + baseCommitID = baseCommit.ID.String() + } else if !isFirstRelease { return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName) } - commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String()) + commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommitID) if err != nil { return "", fmt.Errorf("CommitsBetweenIDs: %w", err) } @@ -58,10 +64,27 @@ func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitR return "", err } - content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors) + fullChangelogURL := "" + if isFirstRelease { + // Keep the first-release changelog link aligned with GitHub, while collecting PRs from full history. + fullChangelogURL = fmt.Sprintf("%s/commits/tag/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName)) + } + + content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors, fullChangelogURL) return content, nil } +func isFirstRelease(ctx context.Context, repoID int64) (bool, error) { + count, err := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ + RepoID: repoID, + IncludeDrafts: false, + }) + if err != nil { + return false, err + } + return count == 0, nil +} + func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) { ref := tagName if !gitRepo.IsTagExist(tagName) { @@ -107,7 +130,7 @@ func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits [ return prs, nil } -func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest) string { +func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest, fullChangelogURL string) string { var builder strings.Builder builder.WriteString("## What's Changed\n") @@ -136,8 +159,12 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, } builder.WriteString("**Full Changelog**: ") - compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) - fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) + if fullChangelogURL != "" { + builder.WriteString(fullChangelogURL) + } else { + compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) + fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) + } builder.WriteByte('\n') return builder.String() } diff --git a/services/release/notes_test.go b/services/release/notes_test.go index 2922da424b5..3fb3b7c553a 100644 --- a/services/release/notes_test.go +++ b/services/release/notes_test.go @@ -21,13 +21,14 @@ import ( func TestGenerateReleaseNotes(t *testing.T) { unittest.PrepareTestEnv(t) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) - require.NoError(t, err) - t.Run("ChangeLogsWithPRs", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + mergedCommit := "90c1019714259b24fb81711d4416ac0f18667dfa" - createMergedPullRequest(t, repo, mergedCommit, 5) + createMergedPullRequest(t, repo, mergedCommit, 5, "Release notes test pull request") content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ TagName: "v1.2.0", @@ -50,16 +51,51 @@ func TestGenerateReleaseNotes(t *testing.T) { }) t.Run("NoPreviousTag", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + + createMergedPullRequest(t, repo, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", 5, "Initial tag PR 1") + createMergedPullRequest(t, repo, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", 4, "Initial tag PR 2") + createMergedPullRequest(t, repo, "5099b81332712fe655e34e8dd63574f503f61811", 8, "Initial tag PR 3") + content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ - TagName: "v1.2.0", - TagTarget: "DefaultBranch", + TagName: "v0.1.0", + TagTarget: repo.DefaultBranch, }) require.NoError(t, err) - assert.Equal(t, "**Full Changelog**: https://try.gitea.io/user2/repo1/commits/tag/v1.2.0\n", content) + + assert.Contains(t, content, "## What's Changed\n") + assert.Contains(t, content, "* Initial tag PR 1 in [#") + assert.Contains(t, content, "* Initial tag PR 2 in [#") + assert.Contains(t, content, "* Initial tag PR 3 in [#") + assert.Contains(t, content, "\n## Contributors\n") + assert.Contains(t, content, "* @user5\n") + assert.Contains(t, content, "* @user4\n") + assert.Contains(t, content, "* @user8\n") + assert.Contains(t, content, "\n## New Contributors\n") + assert.Contains(t, content, "* @user5 made their first contribution in [#") + assert.Contains(t, content, "* @user4 made their first contribution in [#") + assert.Contains(t, content, "* @user8 made their first contribution in [#") + assert.Contains(t, content, "**Full Changelog**: https://try.gitea.io/user2/repo16/commits/tag/v0.1.0\n") + }) + + t.Run("EmptyPreviousTagWithExistingTags", func(t *testing.T) { + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := gitrepo.OpenRepository(t.Context(), repo) + require.NoError(t, err) + t.Cleanup(func() { gitRepo.Close() }) + + _, err = GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{ + TagName: "v1.2.0", + TagTarget: repo.DefaultBranch, + }) + require.Error(t, err) }) } -func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest { +func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64, title string) *issues_model.PullRequest { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID}) issue := &issues_model.Issue{ @@ -67,7 +103,7 @@ func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCom Repo: repo, Poster: user, PosterID: user.ID, - Title: "Release notes test pull request", + Title: title, Content: "content", } From 735e940a6148b7cbe0ee58ed1508856e84a82327 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Wed, 3 Jun 2026 18:50:47 +0200 Subject: [PATCH 12/88] fix(oauth2): not respecting claims before second login (#37874) fixes defect where claims where only applies on login but not during account linking making only the second login take them into account fixes: https://github.com/go-gitea/gitea/issues/32566 --- routers/web/auth/oauth.go | 7 - routers/web/auth/oauth_signin_sync.go | 9 ++ tests/integration/auth_oauth2_test.go | 203 ++++++++++++++++++++++++-- 3 files changed, 196 insertions(+), 23 deletions(-) diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 7bed3523ed8..f7d9c3c34ac 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -188,10 +188,6 @@ func SignInOAuthCallback(ctx *context.Context) { source := authSource.Cfg.(*oauth2.Source) - isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser) - u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue - u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) - linkAccountData := &LinkAccountData{authSource.ID, gothUser} if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled { linkAccountData = nil @@ -373,9 +369,6 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m opts.IsActive = optional.Some(true) } - // Update GroupClaims - opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) - if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { ctx.ServerError("SyncGroupsToTeams", err) diff --git a/routers/web/auth/oauth_signin_sync.go b/routers/web/auth/oauth_signin_sync.go index a939a0e71ec..f5ec66e006a 100644 --- a/routers/web/auth/oauth_signin_sync.go +++ b/routers/web/auth/oauth_signin_sync.go @@ -14,6 +14,7 @@ import ( asymkey_service "gitea.dev/services/asymkey" "gitea.dev/services/auth/source/oauth2" "gitea.dev/services/context" + user_service "gitea.dev/services/user" "github.com/markbates/goth" ) @@ -50,6 +51,14 @@ func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.Us } } + // sync user flags (admin/restricted) + isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) + if isAdmin.Has() || isRestricted.Has() { + if err = user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{IsAdmin: isAdmin, IsRestricted: isRestricted}); err != nil { + log.Error("Unable to sync OAuth2 user admin or restricted status %s: %v", gothUser.Provider, err) + } + } + err = oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u) if err != nil { log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err) diff --git a/tests/integration/auth_oauth2_test.go b/tests/integration/auth_oauth2_test.go index bcd49811494..3bb3d56e7bd 100644 --- a/tests/integration/auth_oauth2_test.go +++ b/tests/integration/auth_oauth2_test.go @@ -54,7 +54,7 @@ func TestMigrateAzureADV2ToOIDC(t *testing.T) { ) // The fake OIDC server issues tokens containing both sub and oid claims, mirroring what Azure AD v2.0 returns. - srv := newFakeOIDCServer(t, subValue, oidValue) + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: subValue, OID: oidValue}) // --- Step 1: Establish the legacy Azure AD V2 state --- // Create an azureadv2 auth source. In production this would have been the source used before the migration. @@ -138,7 +138,7 @@ func TestOIDCIgnoresStaleExternalLoginLinks(t *testing.T) { setup := func(t *testing.T, sourceName, sub, userName, email string) (*auth_model.Source, *user_model.User) { t.Helper() - srv := newFakeOIDCServerWithProfile(t, sub, sub+"-oid", email, "OIDC Test User") + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: sub, OID: sub + "-oid", Email: email, Name: "OIDC Test User"}) addOAuth2Source(t, sourceName, oauth2.Source{ Provider: "openidConnect", ClientID: "test-client-id", @@ -191,14 +191,27 @@ func TestOIDCIgnoresStaleExternalLoginLinks(t *testing.T) { }) } -// newFakeOIDCServer starts an httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow: -func newFakeOIDCServer(t *testing.T, sub, oid string) *httptest.Server { - return newFakeOIDCServerWithProfile(t, sub, oid, sub+"@example.com", "OIDC Test User") +// FakeOIDCConfig holds configuration for the fake OIDC server used in tests. +type FakeOIDCConfig struct { + Sub string + OID string + Email string + Name string + Groups []string } -func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *httptest.Server { +// newFakeOIDCServer starts a httptest.Server that implements the minimum OIDC endpoints needed to complete a sign-in flow +func newFakeOIDCServer(t *testing.T, cfg FakeOIDCConfig) *httptest.Server { t.Helper() + // Set defaults for backward compatibility with existing tests + if cfg.Email == "" { + cfg.Email = cfg.Sub + "@example.com" + } + if cfg.Name == "" { + cfg.Name = "OIDC Test User" + } + var srv *httptest.Server srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -212,11 +225,18 @@ func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *h }) case "/token": // returns an ID token with both "sub" and "oid" claims so tests can verify which one ends up as ExternalID claims := map[string]any{ - "iss": srv.URL, - "aud": "test-client-id", - "exp": time.Now().Add(time.Hour).Unix(), - "sub": sub, - "oid": oid, + "iss": srv.URL, + "aud": "test-client-id", + "exp": time.Now().Add(time.Hour).Unix(), + "sub": cfg.Sub, + "email": cfg.Email, + "name": cfg.Name, + } + if cfg.OID != "" { + claims["oid"] = cfg.OID + } + if cfg.Groups != nil { + claims["groups"] = cfg.Groups } payload, _ := json.Marshal(claims) header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`)) @@ -232,11 +252,18 @@ func newFakeOIDCServerWithProfile(t *testing.T, sub, oid, email, name string) *h }) case "/userinfo": // sub MUST match the id_token sub; goth rejects mismatches. - _ = json.NewEncoder(w).Encode(map[string]any{ - "sub": sub, - "email": email, - "name": name, - }) + response := map[string]any{ + "sub": cfg.Sub, + "email": cfg.Email, + "name": cfg.Name, + } + if cfg.OID != "" { + response["oid"] = cfg.OID + } + if cfg.Groups != nil { + response["groups"] = cfg.Groups + } + _ = json.NewEncoder(w).Encode(response) default: http.NotFound(w, r) } @@ -264,3 +291,147 @@ func doOIDCSignIn(t *testing.T, sourceName string) { callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", sourceName, url.QueryEscape(state)) session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther) } + +// newOIDCSource is a helper function to create a configured OAuth2 source for testing +func newOIDCSource(srv *httptest.Server, withAdmin, withRestricted bool) oauth2.Source { + src := oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration", + GroupClaimName: "groups", + } + if withAdmin { + src.AdminGroup = "admins" + } + if withRestricted { + src.RestrictedGroup = "restricted-users" + } + return src +} + +// TestOAuth2GroupClaimsAppliedOnFirstLogin verifies that group claims from OAuth2/OIDC +// are correctly applied to newly created users on the first login +func TestOAuth2GroupClaimsAppliedOnFirstLogin(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // Enable auto-registration to ensure first login creates user with group claims + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + // Use sub claim as username for deterministic user naming + defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)() + + tt := []struct { + Name string + IsAdmin bool + IsRestricted bool + SourceName string + }{ + { + Name: "user in both admin and restricted groups", + IsAdmin: true, + IsRestricted: true, + SourceName: "test-group-claims", + }, + { + Name: "no groups", + IsAdmin: false, + IsRestricted: false, + SourceName: "test-no-groups", + }, + } + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + // Set up OIDC server with group claims + srv := newFakeOIDCServer(t, FakeOIDCConfig{ + Sub: tc.SourceName, + Email: tc.SourceName + "@example.com", + Name: "Test User", + Groups: []string{"admins", "restricted-users"}, + }) + + // Ensure it's the first login so no user in database + unittest.AssertNotExistsBean(t, &user_model.User{Name: tc.SourceName}) + + addOAuth2Source(t, tc.SourceName, newOIDCSource(srv, tc.IsAdmin, tc.IsRestricted)) + + doOIDCSignIn(t, tc.SourceName) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: tc.SourceName}) + assert.Equal(t, tc.IsAdmin, user.IsAdmin) + assert.Equal(t, tc.IsRestricted, user.IsRestricted) + assert.Equal(t, auth_model.OAuth2, user.LoginType) + }) + } +} + +// TestOAuth2GroupClaimsManualLinking tests that group claims are applied correctly +// when a user goes through the manual linking flow (auto-registration disabled). +func TestOAuth2GroupClaimsManualLinking(t *testing.T) { + defer tests.PrepareTestEnv(t)() + // Disable auto-registration to force manual linking flow + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, false)() + defer test.MockVariableValue(&setting.Service.AllowOnlyInternalRegistration, false)() + + tt := []struct { + Name string + IsAdmin bool + IsRestricted bool + SourceName string + }{ + { + Name: "user in both admin and restricted groups", + IsAdmin: true, + IsRestricted: true, + SourceName: "test-group-claims-manual-linking", + }, + { + Name: "no groups", + IsAdmin: false, + IsRestricted: false, + SourceName: "test-no-groups-manual-linking", + }, + } + + for _, tc := range tt { + t.Run(tc.Name, func(t *testing.T) { + srv := newFakeOIDCServer(t, FakeOIDCConfig{ + Sub: tc.SourceName, + Email: tc.SourceName + "@example.com", + Name: "Manual User", + Groups: []string{"admins", "restricted-users"}, + }) + addOAuth2Source(t, tc.SourceName, newOIDCSource(srv, tc.IsAdmin, tc.IsRestricted)) + unittest.AssertNotExistsBean(t, &user_model.User{Name: tc.SourceName}) + session := emptyTestSession(t) + resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/oauth2/"+tc.SourceName), http.StatusTemporaryRedirect) + + location := resp.Header().Get("Location") + u, err := url.Parse(location) + require.NoError(t, err) + state := u.Query().Get("state") + require.NotEmpty(t, state, "redirect to OIDC provider must include state") + + callbackURL := fmt.Sprintf("/user/oauth2/%s/callback?code=test-code&state=%s", tc.SourceName, url.QueryEscape(state)) + session.MakeRequest(t, NewRequest(t, "GET", callbackURL), http.StatusSeeOther) + + // Submit the form to create a new account + linkAccountResp := session.MakeRequest(t, NewRequest(t, "GET", "/user/link_account"), http.StatusOK) + // Verify we're on the link account page + assert.Contains(t, linkAccountResp.Body.String(), "link_account") + + // Use NewRequestWithValues to POST form data (no CSRF needed in tests) + // Field names are lowercase in HTML forms: user_name, email, password, retype + req := NewRequestWithValues(t, "POST", "/user/link_account_signup", map[string]string{ + "user_name": tc.SourceName, + "email": tc.SourceName + "@example.com", + "password": "", // AllowOnlyExternalRegistration means no password needed + "retype": "", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: tc.SourceName}) + assert.Equal(t, tc.IsAdmin, user.IsAdmin) + assert.Equal(t, tc.IsRestricted, user.IsRestricted) + assert.Equal(t, auth_model.OAuth2, user.LoginType) + }) + } +} From b2748d7654db6ef7c423585b1bb1581d19d42848 Mon Sep 17 00:00:00 2001 From: Thomas Sayen <69324626+Chi-Iroh@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:40:38 +0200 Subject: [PATCH 13/88] feat(ui): add "follow rename" to file commit history list (#34994) Fix #28253 --------- Co-authored-by: wxiaoguang --- modules/git/repo_commit.go | 29 ++++++++++++------ modules/git/repo_commit_test.go | 47 +++++++++++++++++++++++++++-- modules/paginator/paginator.go | 17 +++++------ options/locale/locale_en-US.json | 1 + routers/api/v1/repo/commits.go | 2 +- routers/api/v1/repo/wiki.go | 2 +- routers/web/feed/file.go | 2 +- routers/web/repo/commit.go | 47 +++++++++++++++++++---------- routers/web/repo/wiki.go | 2 +- routers/web/user/home.go | 11 ++++--- routers/web/user/profile.go | 3 +- services/context/pagination.go | 4 +-- services/context/pagination_test.go | 20 ++++++++++++ templates/repo/commits_table.tmpl | 27 +++++++++++------ web_src/css/repo.css | 24 --------------- web_src/js/features/repo-commit.ts | 10 ++++++ web_src/js/index.ts | 3 +- 17 files changed, 169 insertions(+), 82 deletions(-) diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index cbe5053346b..acf2a13b0e1 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -222,17 +222,20 @@ type CommitsByFileAndRangeOptions struct { Page int Since string Until string + + // when using FollowRename, there is no quick way to know the total count, so use hasMore to indicate if there are more commits to load + FollowRename bool } // CommitsByFileAndRange return the commits according revision file and the page -func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - gitCmd := gitcmd.NewCommand("rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). +func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) (commits []*Commit, hasMore bool, _ error) { + limit := setting.Git.CommitsRangeSize + gitCmd := gitcmd.NewCommand("--no-pager", "log"). + AddArguments("--pretty=tformat:%H"). + AddOptionFormat("--max-count=%d", limit+1). AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) - gitCmd.AddDynamicArguments(opts.Revision) - - if opts.Not != "" { - gitCmd.AddOptionValues("--not", opts.Not) + if opts.FollowRename { + gitCmd.AddArguments("--follow") } if opts.Since != "" { gitCmd.AddOptionFormat("--since=%s", opts.Since) @@ -240,9 +243,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) if opts.Until != "" { gitCmd.AddOptionFormat("--until=%s", opts.Until) } + gitCmd.AddDynamicArguments(opts.Revision) + if opts.Not != "" { + gitCmd.AddOptionValues("--not", opts.Not) + } gitCmd.AddDashesAndList(opts.File) - var commits []*Commit stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() defer stdoutReaderClose() err := gitCmd.WithDir(repo.Path). @@ -274,7 +280,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) } }). RunWithStderr(repo.Ctx) - return commits, err + + hasMore = len(commits) > limit + if hasMore { + commits = commits[:limit] + } + return commits, hasMore, err } // FilesCountBetween return the number of files changed between two commits diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 517decf0ca3..aecba042508 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -6,8 +6,10 @@ package git import ( "os" "path/filepath" + "strings" "testing" + "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" "gitea.dev/modules/test" @@ -140,11 +142,52 @@ func TestCommitsByFileAndRange(t *testing.T) { defer bareRepo1.Close() // "foo" has 3 commits in "master" branch - commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) + commits, hasMore, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 1}) require.NoError(t, err) + assert.True(t, hasMore) assert.Len(t, commits, 2) - commits, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) + commits, hasMore, err = bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "foo", Page: 2}) require.NoError(t, err) assert.Len(t, commits, 1) + assert.False(t, hasMore) + + repoFollowRenameDir := filepath.Join(t.TempDir(), "repo.git") + require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoFollowRenameDir).Run(t.Context())) + _, _, runErr := gitcmd.NewCommand("fast-import").WithDir(repoFollowRenameDir).WithStdinBytes([]byte(strings.TrimSpace(` +blob +mark :1 +data 0 + +reset refs/heads/master +commit refs/heads/master +mark :2 +author Chi-Iroh 1778660718 +0200 +committer Chi-Iroh 1778660718 +0200 +data 10 +Add a.txt +M 100644 :1 a.txt + +commit refs/heads/master +mark :3 +author Chi-Iroh 1778660741 +0200 +committer Chi-Iroh 1778660741 +0200 +data 22 +Rename a.txt to b.txt +from :2 +D a.txt +M 100644 :1 b.txt + `))).RunStdString(t.Context()) + require.NoError(t, runErr) + + repoFollowRename, err := OpenRepository(t.Context(), repoFollowRenameDir) + require.NoError(t, err) + defer repoFollowRename.Close() + + commits, _, err = repoFollowRename.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1}) + require.NoError(t, err) + assert.Len(t, commits, 1) + commits, _, err = repoFollowRename.CommitsByFileAndRange(CommitsByFileAndRangeOptions{Revision: "master", File: "b.txt", Page: 1, FollowRename: true}) + require.NoError(t, err) + assert.Len(t, commits, 2) } diff --git a/modules/paginator/paginator.go b/modules/paginator/paginator.go index 942bf7117b6..ee06d70dca5 100644 --- a/modules/paginator/paginator.go +++ b/modules/paginator/paginator.go @@ -37,10 +37,11 @@ type Paginator struct { total int // total rows count, -1 means unknown totalPages int // total pages count, -1 means unknown current int // current page number - curRows int // current page rows count pagingNum int // how many rows in one page numPages int // how many pages to show on the UI + + hasNext *bool // used for total=-1 ("unlimited paging") } // New initialize a new pagination calculation and returns a Paginator as result. @@ -60,15 +61,13 @@ func New(total, pagingNum, current, numPages int) *Paginator { } } -func (p *Paginator) SetCurRows(rows int) { +func (p *Paginator) SetUnlimitedPaging(curRows int, hasNext bool) { // For "unlimited paging", we need to know the rows of current page to determine if there is a next page. - // There is still an edge case: when curRows==pagingNum, then the "next page" will be an empty page. - // Ideally we should query one more row to determine if there is really a next page, but it's impossible in current framework. - p.curRows = rows - if p.total == -1 && p.current == 1 && !p.HasNext() { + p.hasNext = &hasNext + if p.total == -1 && p.current == 1 && !hasNext { // if there is only one page for the "unlimited paging", set total rows/pages count // then the tmpl could decide to hide the nav bar. - p.total = rows + p.total = curRows p.totalPages = util.Iif(p.total == 0, 0, 1) } } @@ -92,8 +91,8 @@ func (p *Paginator) Previous() int { // HasNext returns true if there is a next page relative to current page. func (p *Paginator) HasNext() bool { - if p.total == -1 { - return p.curRows >= p.pagingNum + if p.hasNext != nil { + return *p.hasNext } return p.current*p.pagingNum < p.total } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index fdef5813580..51a97977421 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "Branch \"%s\" already exists in your fork. Please choose a new branch name.", "repo.commits.desc": "Browse source code change history.", "repo.commits.commits": "Commits", + "repo.commits.history_enable_follow_renames": "Include renames", "repo.commits.no_commits": "No commits in common. \"%s\" and \"%s\" have entirely different histories.", "repo.commits.nothing_to_compare": "There are no differences to show.", "repo.commits.search.tooltip": "You can prefix keywords with \"author:\", \"committer:\", \"after:\", or \"before:\", e.g. \"revert author:Alice before:2019-01-13\".", diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 0e66bae4304..620ee037006 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -262,7 +262,7 @@ func GetAllCommits(ctx *context.APIContext) { return } - commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: sha, File: path, diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index e6fdd6afc60..16471044820 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -435,7 +435,7 @@ func ListPageRevisions(ctx *context.APIContext) { page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, _, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go index 122d219b6ca..f1155219286 100644 --- a/routers/web/feed/file.go +++ b/routers/web/feed/file.go @@ -20,7 +20,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string if len(fileName) == 0 { return } - commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, _, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName File: fileName, diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 0760cb4eeda..7cb2f82a903 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -214,37 +214,52 @@ func FileHistory(ctx *context.Context) { return } - commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("FileCommitsCount", err) - return - } else if commitsCount == 0 { - ctx.NotFound(nil) - return - } + followRename := ctx.FormBool("follow-rename") + ctx.Data["ShowFollowRename"] = true + ctx.Data["FollowRenameChecked"] = followRename page := max(ctx.FormInt("page"), 1) - - commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( + commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName - File: ctx.Repo.TreePath, - Page: page, + Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName + File: ctx.Repo.TreePath, + Page: page, + FollowRename: followRename, }) if err != nil { ctx.ServerError("CommitsByFileAndRange", err) return } + + var commitsCount int64 + if followRename { + // there is no quick method to know the total count when "follow rename" + commitsCount = -1 + } else { + commitsCount, err = gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("FileCommitsCount", err) + return + } + } + + if len(commits) == 0 { + ctx.NotFound(nil) + return + } + + ctx.Data["FileTreePath"] = ctx.Repo.TreePath + ctx.Data["CommitCount"] = commitsCount ctx.Data["Commits"], err = processGitCommits(ctx, commits) if err != nil { ctx.ServerError("processGitCommits", err) return } - ctx.Data["FileTreePath"] = ctx.Repo.TreePath - ctx.Data["CommitCount"] = commitsCount - pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5) + if commitsCount == -1 { + pager.WithUnlimitedPaging(len(commits), hasMore) + } pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplCommits) diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index dcb0c25829f..e0881dc9f1d 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -351,7 +351,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiGitRepo.CommitsByFileAndRange( + commitsHistory, _, err := wikiGitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 3573a132414..6951be45ef3 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -111,6 +111,7 @@ func Dashboard(ctx *context.Context) { prepareHeatmapURL(ctx) + pageSize := setting.UI.User.RepoPagingNum feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctxUser, RequestedTeam: ctx.Org.Team, @@ -119,17 +120,17 @@ func Dashboard(ctx *context.Context) { OnlyPerformedBy: false, IncludeDeleted: false, Date: ctx.FormString("date"), - ListOptions: db.ListOptions{ - Page: page, - PageSize: setting.UI.FeedPagingNum, - }, + ListOptions: db.ListOptions{Page: page, PageSize: pageSize}, }) if err != nil { ctx.ServerError("GetFeeds", err) return } - pager := context.NewPagination(count, setting.UI.FeedPagingNum, page, 5).WithCurRows(len(feeds)) + // FIXME: UNLIMITE-PAGING-ONE-MORE-ROW: here is still an edge case: when curRows==pagingNum, then the "next page" will be an empty page. + // Ideally we should query one more row to determine if there is really a next page, but it's impossible in current framework. + pager := context.NewPagination(count, pageSize, page, 5).WithUnlimitedPaging(len(feeds), len(feeds) == pageSize) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.Data["Feeds"] = feeds diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index dfe9a12bc2f..654bd1a5f08 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -312,7 +312,8 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R pager := context.NewPagination(total, pagingNum, page, 5) if tab == "activity" { - pager.WithCurRows(curRows) + // FIXME: UNLIMITE-PAGING-ONE-MORE-ROW: see another comment + pager.WithUnlimitedPaging(curRows, curRows == pagingNum) } pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager diff --git a/services/context/pagination.go b/services/context/pagination.go index a6e83df6a15..ef0f44ab37c 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -33,8 +33,8 @@ func NewPagination(total int64, pagingNum, current, numPages int) *Pagination { return p } -func (p *Pagination) WithCurRows(n int) *Pagination { - p.Paginater.SetCurRows(n) +func (p *Pagination) WithUnlimitedPaging(curRows int, hasNext bool) *Pagination { + p.Paginater.SetUnlimitedPaging(curRows, hasNext) return p } diff --git a/services/context/pagination_test.go b/services/context/pagination_test.go index 0ddef26ca3d..1b15fa7b81d 100644 --- a/services/context/pagination_test.go +++ b/services/context/pagination_test.go @@ -32,4 +32,24 @@ func TestPagination(t *testing.T) { params.Del("foo") v, _ = url.ParseQuery(string(p.GetParams())) assert.Equal(t, params, v) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(0, false) + assert.Zero(t, p.Paginater.TotalPages()) + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(10, false) + assert.Equal(t, 1, p.Paginater.TotalPages()) // first page, no next, so it should know that the total page number is 1 + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 2, 1) + p.WithUnlimitedPaging(10, false) + assert.Equal(t, -1, p.Paginater.TotalPages()) + assert.False(t, p.Paginater.HasNext()) + + p = NewPagination(-1, 1, 1, 1) + p.WithUnlimitedPaging(10, true) + assert.Equal(t, -1, p.Paginater.TotalPages()) + assert.True(t, p.Paginater.HasNext()) } diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 56a4867ff4b..59e87d10c03 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,21 +1,30 @@ -

-
- {{if or .PageIsCommits (gt .CommitCount 0)}} - {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} +
+
+ {{if .Commits}} + {{if gt .CommitCount 0}}{{.CommitCount}}{{end}}{{/* CommitCount is -1 when "follow rename" */}} + {{ctx.Locale.Tr "repo.commits.commits"}} {{else if .IsNothingToCompare}} {{ctx.Locale.Tr "repo.commits.nothing_to_compare"}} {{else}} {{ctx.Locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{end}}
- {{if .IsDiffCompare}} -
+
+ {{if .ShowFollowRename}} +
+ + +
+ {{end}} + {{if .IsDiffCompare}} + - {{end}} -

+ {{end}} + + {{if .PageIsCommits}}
@@ -29,7 +38,7 @@
{{end}} -{{if and .Commits (gt .CommitCount 0)}} +{{if .Commits}} {{template "repo/commits_list" .}} {{end}} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index b47e03b8ce4..b7cb5e1dcd1 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1816,14 +1816,6 @@ tbody.commit-list { box-shadow: 0 0.5rem 1rem var(--color-shadow) !important; } -.commits-table .commits-table-right form { - display: flex; - align-items: center; - gap: 0.75em; - justify-content: center; - flex-wrap: wrap; -} - @media (max-width: 767.98px) { .repository.view.issue .comment-list, .repository.view.issue .comment-list .timeline-item { @@ -1848,22 +1840,6 @@ tbody.commit-list { flex-basis: auto !important; margin-bottom: 0.5rem !important; } - .commits-table { - flex-direction: column; - } - .commits-table .commits-table-left { - align-items: initial !important; - margin-bottom: 6px; - } - .commits-table .commits-table-right form > div:nth-child(1) { - order: 1; /* the "commit search" input */ - } - .commits-table .commits-table-right form > div:nth-child(2) { - order: 3; /* the "search all" checkbox */ - } - .commits-table .commits-table-right form > button:nth-child(3) { - order: 2; /* the "search" button */ - } .commit-table { overflow-x: auto; } diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts index 16f38993749..17130427a62 100644 --- a/web_src/js/features/repo-commit.ts +++ b/web_src/js/features/repo-commit.ts @@ -24,3 +24,13 @@ export function initCommitStatuses() { }); }); } + +export function initCommitFileHistoryFollowRename() { + registerGlobalInitFunc('initCommitHistoryFollowRename', (el: HTMLInputElement) => { + el.addEventListener('change', () => { + const url = new URL(window.location.toString()); + url.searchParams.set('follow-rename', `${el.checked}`); + window.location.assign(url.toString()); + }); + }); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 094caefb7e0..a2994d6912d 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -21,7 +21,7 @@ import {initMarkupContent} from './markup/content.ts'; import {initRepoFileView} from './features/file-view.ts'; import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth.ts'; import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; +import {initRepoEllipsisButton, initCommitStatuses, initCommitFileHistoryFollowRename} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; import {initAdminCommon} from './features/admin/common.ts'; import {initRepoCodeView} from './features/repo-code.ts'; @@ -123,6 +123,7 @@ const initPerformanceTracer = callInitFunctions([ initRepoCodeView, initBranchSelectorTabs, initRepoEllipsisButton, + initCommitFileHistoryFollowRename, initRepoDiffCommitBranchesAndTags, initRepoEditor, initRepoGraphGit, From 792fa5eeba4de5f54b0c31f6b0a869ba2c911396 Mon Sep 17 00:00:00 2001 From: Harsh Mahajan <127186841+HarshMN2345@users.noreply.github.com> Date: Thu, 4 Jun 2026 04:51:48 +0530 Subject: [PATCH 14/88] feat(api): add q parameter to list branches API for server-side filtering (#37982) The GET /repos/{owner}/{repo}/branches endpoint currently has no way to filter branches by name server-side, forcing API consumers to paginate through all branches and filter client-side. The UI already supports branch search (added in [#27055](https://github.com/go-gitea/gitea/pull/27055)). The underlying DB layer has a Keyword field on FindBranchOptions in models/git/branch_list.go that does a LIKE %keyword% SQL filter, it just wasn't wired up to the API handler. This PR exposes a ?q= query parameter on the endpoint that maps to FindBranchOptions.Keyword. Example: ```GET /repos/owner/repo/branches?q=feature ``` Closes #37981 --------- Co-authored-by: wxiaoguang --- routers/api/v1/repo/branch.go | 6 ++++++ templates/swagger/v1_json.tmpl | 6 ++++++ templates/swagger/v1_openapi3_json.tmpl | 8 ++++++++ tests/integration/api_repo_branch_test.go | 17 +++++++++++++++++ tests/integration/integration_test.go | 4 ++++ 5 files changed, 41 insertions(+) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 629945398d8..3afdf9694d1 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -306,6 +306,10 @@ func ListBranches(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: q + // in: query + // description: branch name substring to filter by + // type: string // responses: // "200": // "$ref": "#/responses/BranchList" @@ -314,6 +318,7 @@ func ListBranches(ctx *context.APIContext) { var apiBranches []*api.Branch listOptions := utils.GetListOptions(ctx) + keyword := ctx.FormString("q") if !ctx.Repo.Repository.IsEmpty { if ctx.Repo.GitRepo == nil { @@ -325,6 +330,7 @@ func ListBranches(ctx *context.APIContext) { ListOptions: listOptions, RepoID: ctx.Repo.Repository.ID, IsDeletedBranch: optional.Some(false), + Keyword: keyword, } var err error totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 20c7abd3454..0b3226294f2 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -7158,6 +7158,12 @@ "description": "page size of results", "name": "limit", "in": "query" + }, + { + "type": "string", + "description": "branch name substring to filter by", + "name": "q", + "in": "query" } ], "responses": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 0a15c8eba49..1402f76899a 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -18224,6 +18224,14 @@ "schema": { "type": "integer" } + }, + { + "description": "branch name substring to filter by", + "in": "query", + "name": "q", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/tests/integration/api_repo_branch_test.go b/tests/integration/api_repo_branch_test.go index 188da619fb9..864e351c8d0 100644 --- a/tests/integration/api_repo_branch_test.go +++ b/tests/integration/api_repo_branch_test.go @@ -128,3 +128,20 @@ func TestAPIRepoBranchesMirror(t *testing.T) { assert.NoError(t, err) assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs)) } + +func TestAPIRepoBranchesSearch(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteRepository) + + // "test" matches "test_branch" but not "master" + resp := MakeRequest(t, NewRequestf(t, "GET", "/api/v1/repos/org3/repo3/branches?q=test").AddTokenAuth(token), http.StatusOK) + branches := DecodeJSON(t, resp, []api.Branch{}) + assert.Len(t, branches, 1) + assert.Equal(t, "test_branch", branches[0].Name) + + // no match returns empty list + resp = MakeRequest(t, NewRequestf(t, "GET", "/api/v1/repos/org3/repo3/branches?q=doesnotexist").AddTokenAuth(token), http.StatusOK) + branches = DecodeJSON(t, resp, []api.Branch{}) + assert.Empty(t, branches) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 9c42366832f..6f0928a12e1 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -323,6 +323,10 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { } } +// DecodeJSON decodes then response as JSON into typed variable and return it +// HINT: don't use it on existing variable (reuse existing variable): +// if the existing var already contains some values but the new input doesn't, then it leads to wrong test result in edge cases. +// For slice decoding, use: v := DecodeJSON(t, resp, []T{}) func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) { t.Helper() From aaf4b149fa29a11ec6495d65610031dee7fcfb79 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 06:38:56 -0700 Subject: [PATCH 15/88] chore(deps): upgrade zstd seekable package (#37988) Upgrade `github.com/SaveTheRbtz/zstd-seekable-format-go/pkg` from `v0.8.3` to `v0.10.0`: https://github.com/SaveTheRbtz/zstd-seekable-format-go/releases/tag/pkg%2Fv0.10.0 This keeps Gitea's seekable zstd wrapper on the stable v0.10 API while preserving the existing public `modules/zstd` API. API migration: - update `SeekableWriter` and `SeekableReader` internals for the concrete `*seekable.Writer` and `*seekable.Reader` types introduced by SaveTheRbtz/zstd-seekable-format-go#264 - update generated dependency metadata after `go mod tidy` removed the now-unused `github.com/google/btree` transitive dependency - no Gitea call sites needed changes because `modules/zstd` still exposes the same constructors and interfaces Validation: - `go test ./modules/zstd` - `make --always-make checks-backend` --------- Co-authored-by: Giteabot --- assets/go-licenses.json | 5 ----- go.mod | 3 +-- go.sum | 6 ++---- modules/zstd/zstd.go | 4 ++-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 22dc1696246..506ab3c9ee6 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -649,11 +649,6 @@ "path": "github.com/golang/snappy/LICENSE", "licenseText": "Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/google/btree", - "path": "github.com/google/btree/LICENSE", - "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/google/flatbuffers", "path": "github.com/google/flatbuffers/LICENSE", diff --git a/go.mod b/go.mod index ead88bf5fb7..e1b54b8aac5 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 github.com/ProtonMail/go-crypto v1.4.1 github.com/PuerkitoBio/goquery v1.12.0 - github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3 + github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 github.com/alecthomas/chroma/v2 v2.25.0 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.14 @@ -195,7 +195,6 @@ require ( github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-tpm v0.9.8 // indirect diff --git a/go.sum b/go.sum index 521779a0278..6a97d37ced9 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59 github.com/RoaringBitmap/roaring/v2 v2.16.0/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3 h1:ikrUPushSxbpr9mmDhSgz1KyUTChjwkDrxY/jzv2OTQ= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.3/go.mod h1:bnXbvnI9Mfqdj4L3Y9aCsB1A/ztuYLNRzgVKsJFgR/U= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0 h1:LvK7+C6qgz8BPnmn7xGekf8vTkcqTvyBBHaLYIMxx0g= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.10.0/go.mod h1:I28hc9eaiqKCoOB+9Wh/P1IOScfm0xCgyWC6DAo0lmo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= @@ -367,8 +367,6 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= diff --git a/modules/zstd/zstd.go b/modules/zstd/zstd.go index d2249447d62..00cf2ea82e0 100644 --- a/modules/zstd/zstd.go +++ b/modules/zstd/zstd.go @@ -60,7 +60,7 @@ func (r *Reader) Close() error { type SeekableWriter struct { buf []byte n int - w seekable.Writer + w *seekable.Writer } var _ io.WriteCloser = (*SeekableWriter)(nil) @@ -114,7 +114,7 @@ func (w *SeekableWriter) Close() error { } type SeekableReader struct { - r seekable.Reader + r *seekable.Reader c func() error } From dac41a124fd34820a3c8caf3b3592ba62cd514ff Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 4 Jun 2026 21:56:16 +0800 Subject: [PATCH 16/88] fix!: raise git required version to 2.13 (#37996) format `lstrip=2` is only supported in git >= 2.13 https://git-scm.com/docs/git-for-each-ref/2.13.7 ref: #37994 Co-authored-by: Giteabot --- modules/git/config.go | 6 ++-- modules/git/git.go | 9 +----- modules/git/remote.go | 8 +----- modules/git/repo_commit.go | 51 +++++++-------------------------- modules/git/repo_commit_test.go | 3 +- 5 files changed, 16 insertions(+), 61 deletions(-) diff --git a/modules/git/config.go b/modules/git/config.go index b1ca18c9882..e6a2ac88173 100644 --- a/modules/git/config.go +++ b/modules/git/config.go @@ -46,10 +46,8 @@ func syncGitConfig(ctx context.Context) (err error) { return err } - if DefaultFeatures().CheckVersionAtLeast("2.10") { - if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil { - return err - } + if err := configSet(ctx, "receive.advertisePushOptions", "true"); err != nil { + return err } if DefaultFeatures().CheckVersionAtLeast("2.18") { diff --git a/modules/git/git.go b/modules/git/git.go index 5359781968f..0c2deb0281e 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -22,7 +22,7 @@ import ( "github.com/hashicorp/go-version" ) -const RequiredVersion = "2.6.0" // the minimum Git version required +const RequiredVersion = "2.13.0" // the minimum Git version required type Features struct { gitVersion *version.Version @@ -173,13 +173,6 @@ func InitFull() (err error) { if err = InitSimple(); err != nil { return err } - - if setting.LFS.StartServer { - if !DefaultFeatures().CheckVersionAtLeast("2.1.2") { - return errors.New("LFS server support requires Git >= 2.1.2") - } - } - return syncGitConfig(context.Background()) } diff --git a/modules/git/remote.go b/modules/git/remote.go index f215cd36713..b4fe6cfd100 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -15,13 +15,7 @@ import ( // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { - var cmd *gitcmd.Command - if DefaultFeatures().CheckVersionAtLeast("2.7") { - cmd = gitcmd.NewCommand("remote", "get-url").AddDynamicArguments(remoteName) - } else { - cmd = gitcmd.NewCommand("config", "--get").AddDynamicArguments("remote." + remoteName + ".url") - } - + cmd := gitcmd.NewCommand("remote", "get-url").AddDynamicArguments(remoteName) result, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return "", err diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index acf2a13b0e1..b955f2709b3 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -7,7 +7,6 @@ package git import ( "bytes" "io" - "os" "strconv" "strings" @@ -409,7 +408,7 @@ func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) commits := make([]*Commit, 0, len(formattedLog)) for _, commit := range formattedLog { - branches, err := repo.getBranches(os.Environ(), commit.ID.String(), 2) + branches, err := repo.getBranches(nil, commit.ID.String(), 2) if err != nil { return nil, err } @@ -433,46 +432,17 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, } func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) { - if DefaultFeatures().CheckVersionAtLeast("2.7.0") { - stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). - AddOptionFormat("--count=%d", limit). - AddOptionValues("--contains", commitID, BranchPrefix). - WithDir(repo.Path). - WithEnv(env). - RunStdString(repo.Ctx) - if err != nil { - return nil, err - } - - branches := strings.Fields(stdout) - return branches, nil - } - - stdout, _, err := gitcmd.NewCommand("branch"). + stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). + AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID). - WithDir(repo.Path). + AddArguments(BranchPrefix). WithEnv(env). + WithDir(repo.Path). RunStdString(repo.Ctx) if err != nil { return nil, err } - - refs := strings.Split(stdout, "\n") - - var maxNum int - if len(refs) > limit { - maxNum = limit - } else { - maxNum = len(refs) - 1 - } - - branches := make([]string, maxNum) - for i, ref := range refs[:maxNum] { - parts := strings.Fields(ref) - - branches[i] = parts[len(parts)-1] - } - return branches, nil + return strings.Fields(stdout), nil } // GetCommitsFromIDs get commits from commit IDs @@ -515,16 +485,17 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'}) - // check the commits one by one until we find a commit contained by another branch + // check the commits one by one until we find a commit contained by another branch, // and we think this commit is the divergence point - for commitID := range parts { - branches, err := repo.getBranches(env, string(commitID), 2) + for part := range parts { + commitID := string(part) + branches, err := repo.getBranches(env, commitID, 2) if err != nil { return "", err } for _, b := range branches { if b != branch { - return string(commitID), nil + return commitID, nil } } } diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index aecba042508..fc905d24257 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -4,7 +4,6 @@ package git import ( - "os" "path/filepath" "strings" "testing" @@ -38,7 +37,7 @@ func TestRepository_GetCommitBranches(t *testing.T) { for _, testCase := range testCases { commit, err := bareRepo1.GetCommit(testCase.CommitID) assert.NoError(t, err) - branches, err := bareRepo1.getBranches(os.Environ(), commit.ID.String(), 2) + branches, err := bareRepo1.getBranches(nil, commit.ID.String(), 2) assert.NoError(t, err) assert.Equal(t, testCase.ExpectedBranches, branches) } From 7a26d5a2ae7f708d535623ccbe201ecd3e121d5d Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 5 Jun 2026 01:18:00 +0000 Subject: [PATCH 17/88] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 3e3fd8746f3..9bff2c24be8 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "Tá brainse \"%s\" ann cheana féin i do fhorc. Roghnaigh ainm brainse nua le do thoil.", "repo.commits.desc": "Brabhsáil stair athraithe cód foinse.", "repo.commits.commits": "Tiomáintí", + "repo.commits.history_enable_follow_renames": "Cuir athainmneacha san áireamh", "repo.commits.no_commits": "Níl aon ghealltanas i gcoiteann. Tá stair iomlán difriúil ag \"%s\" agus \"%s\".", "repo.commits.nothing_to_compare": "Níl aon difríochtaí le taispeáint.", "repo.commits.search.tooltip": "Is féidir eochairfhocail a réamhfhostú le “údar:”, “committer:”, “after:”, nó “before:”, e.g. \"fill an t-údar:Alice roimh: 2019-01-13\".", From aa63d1583dc6ca69887a2abc9a176ec16695df61 Mon Sep 17 00:00:00 2001 From: bircni Date: Fri, 5 Jun 2026 20:10:25 +0200 Subject: [PATCH 18/88] fix(actions): return 404 when job log blob is missing (#38003) - When the `action_task` row exists but the underlying dbfs/storage blob is gone, `OpenLogs` returns a wrapped `os.ErrNotExist` which surfaces as a 500 on the job logs endpoints. - Translate it to the same `util.NewNotExistErrorf` shape already used for unknown job ids / expired logs, so both the API (`/api/v1/repos/.../actions/jobs//logs`) and the web download handler return a clean 404 instead. Fixes #37990. --- routers/common/actions.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/routers/common/actions.go b/routers/common/actions.go index 6fc5da7b13c..4c3c2129282 100644 --- a/routers/common/actions.go +++ b/routers/common/actions.go @@ -4,7 +4,9 @@ package common import ( + "errors" "fmt" + "io/fs" "strings" actions_model "gitea.dev/models/actions" @@ -51,6 +53,9 @@ func DownloadActionsRunJobLogs(ctx *context.Base, ctxRepo *repo_model.Repository reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return util.NewNotExistErrorf("logs not found") + } return fmt.Errorf("OpenLogs: %w", err) } defer reader.Close() From 3659b5acc215785d7bc7529c78b8431f573aae8f Mon Sep 17 00:00:00 2001 From: bircni Date: Fri, 5 Jun 2026 23:33:40 +0200 Subject: [PATCH 19/88] ci(workflows): add AgentScan workflow to flag possible AI-assisted PRs (#37962) This PR adds an automated AgentScan workflow to help detect and handle pull requests that appear to be created or authored primarily by automated agents. - If a PR is classified as `automation` or community-flagged, the workflow: - Adds the `possible bot` label, - Posts a policy comment linking to the repository AI Contribution Policy (`CONTRIBUTING.md#ai-contribution-policy`) and listing required disclosures and checks, - Optionally closes the PR if classification indicates an automated/unwelcome submission. --- .github/workflows/agent-scan.yml | 113 +++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/agent-scan.yml diff --git a/.github/workflows/agent-scan.yml b/.github/workflows/agent-scan.yml new file mode 100644 index 00000000000..2c84face622 --- /dev/null +++ b/.github/workflows/agent-scan.yml @@ -0,0 +1,113 @@ +name: AgentScan + +on: + # jobs only use pinned actions and never checkout code + pull_request_target: # zizmor: ignore[dangerous-triggers] + types: [opened, reopened, synchronize, edited] + +concurrency: + group: agent-scan-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + issues: write + pull-requests: write + +jobs: + agentscan: + runs-on: ubuntu-latest + steps: + - name: AgentScan + id: agentscan + uses: MatteoGabriele/agentscan-action@0a0c88109b5153dff2805f969f5060441efb7b65 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + skip-members: "dependabot[bot],renovate[bot], giteabot (backports)" + agent-scan-comment: false + + - name: Handle flagged PR + if: contains(fromJSON('["automation","mixed"]'), steps.agentscan.outputs.classification) || steps.agentscan.outputs.community-flagged == 'true' + env: + CLASSIFICATION: ${{ steps.agentscan.outputs.classification }} + COMMUNITY_FLAGGED: ${{ steps.agentscan.outputs.community-flagged }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 + with: + script: | + const core = require('@actions/core'); + const prNumber = context.payload.pull_request.number; + const classification = process.env.CLASSIFICATION; + const communityFlagged = process.env.COMMUNITY_FLAGGED === 'true'; + const shouldClose = classification === 'automation' || communityFlagged; + + const issue = context.payload.pull_request; + const labels = issue.labels?.map(l => l.name) || []; + + if (!labels.includes('possible bot')) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: ['possible bot'], + }); + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const alreadyCommented = comments.some(c => c.user.type === 'Bot' && c.body.includes('AI Contribution Policy')); + + if (!alreadyCommented) { + const closingNote = shouldClose + ? "We're closing this for now as the account looks automated. If we got that wrong, please just reopen the PR and we'll take another look." + : 'If this was flagged in error, we apologise! 😳 Just let us know. 🙏'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: [ + "We've flagged this pull request as potentially AI-assisted.", + '', + 'Gitea welcomes the thoughtful use of AI tools, but contributors must use them responsibly and clearly disclose any assistance. Please follow the AI Contribution Policy in `CONTRIBUTING.md` and update this PR accordingly:', + '', + 'Maintainers may close PRs that do not disclose AI assistance, appear to be low-quality AI-generated content, or where the contributor cannot explain the changes.', + '', + 'See: https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#ai-contribution-policy', + '', + closingNote, + ].join('\n'), + }); + } else { + core.info('Possible-bot comment already exists - skipping comment.'); + } + + if (shouldClose && issue.state === 'open' && !alreadyCommented) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed', + title: '🚨 unwelcome pr from bot 🚨', + }); + } + + const actionTaken = [ + 'Added `possible bot` label', + alreadyCommented ? null : 'posted policy comment', + shouldClose && !alreadyCommented ? 'closed PR' : null, + ].filter(Boolean).join(', '); + + core.summary + .addHeading('AgentScan: Possible Bot Flag', 2) + .addTable([ + [{ data: 'Property', header: true }, { data: 'Value', header: true }], + ['Pull Request', `#${prNumber}`], + ['Classification', classification], + ['Community flagged', String(communityFlagged)], + ['Action', actionTaken || 'No action (already handled)'], + ]) + .write(); From 4088d7e241d21de8a720debecd2c95cb86a9dc4f Mon Sep 17 00:00:00 2001 From: bircni Date: Sat, 6 Jun 2026 11:00:14 +0200 Subject: [PATCH 20/88] fix(ui): keep actions run title intact when subject contains an issue ref (#38005) --- models/repo/repo.go | 8 +-- modules/markup/html.go | 42 +++++++------- modules/markup/html_internal_test.go | 6 +- modules/templates/util_render.go | 79 +++++++-------------------- modules/templates/util_render_test.go | 8 +-- routers/web/repo/commit.go | 6 +- templates/repo/actions/runs_list.tmpl | 11 ++-- web_src/css/repo/issue-list.css | 14 +++++ 8 files changed, 69 insertions(+), 105 deletions(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 7f8507722f0..e56603fc812 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -23,6 +23,7 @@ import ( "gitea.dev/modules/base" "gitea.dev/modules/git" giturl "gitea.dev/modules/git/url" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/httplib" "gitea.dev/modules/log" "gitea.dev/modules/markup" @@ -641,12 +642,7 @@ func (repo *Repository) CanContentChange() bool { // DescriptionHTML does special handles to description and return HTML string. func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { - desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description) - if err != nil { - log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) - return template.HTML(markup.SanitizeDescription(repo.Description)) - } - return template.HTML(markup.SanitizeDescription(desc)) + return markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), htmlutil.EscapeString(repo.Description)) } // CloneLink represents different types of clone URLs of repository. diff --git a/modules/markup/html.go b/modules/markup/html.go index 4943bdf4a5f..a21c4707113 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -14,8 +14,10 @@ import ( "sync" "gitea.dev/modules/htmlutil" + "gitea.dev/modules/log" "gitea.dev/modules/markup/common" "gitea.dev/modules/translation" + "gitea.dev/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -151,8 +153,7 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e } // PostProcessCommitMessage will use the same logic as PostProcess, but will disable the shortLinkProcessor. -// FIXME: this function and its family have a very strange design: it takes HTML as input and output, processes the "escaped" content. -func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (template.HTML, error) { +func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -166,8 +167,7 @@ func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) (templa emojiProcessor, emojiShortCodeProcessor, } - s, err := postProcessString(ctx, procs, string(content)) - return template.HTML(s), err + return postProcessHTML(ctx, procs, content) } var emojiProcessors = []processor{ @@ -189,7 +189,7 @@ func isBareURLSubject(content string) bool { // PostProcessCommitMessageSubject will use the same logic as PostProcess and // PostProcessCommitMessage, but will disable the shortLinkProcessor and // emailAddressProcessor, and wraps the whole subject in defaultLink. -func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { +func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink string, content template.HTML) template.HTML { procs := []processor{ fullIssuePatternProcessor, comparePatternProcessor, @@ -207,7 +207,7 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st // plain text inside defaultLink. Partial URLs inside larger text still become // their own links (nested anchors aren't legal HTML, so the outer defaultLink // naturally breaks on that span, same as on GitHub). - if !isBareURLSubject(content) { + if !isBareURLSubject(string(content)) { procs = append(procs, linkProcessor) } procs = append(procs, func(ctx *RenderContext, node *html.Node) { @@ -215,27 +215,28 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st node.Type = html.ElementNode node.Data = "a" node.DataAtom = atom.A - node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted"}} + node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted title-full-link"}} node.FirstChild, node.LastChild = ch, ch }) - return postProcessString(ctx, procs, content) + rendered := postProcessHTML(ctx, procs, content) + return htmlutil.HTMLFormat(`%s`, rendered) } // PostProcessIssueTitle to process title on individual issue/pull page -func PostProcessIssueTitle(ctx *RenderContext, title string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessIssueTitle(ctx *RenderContext, titleHTML template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ issueIndexPatternProcessor, commitCrossReferencePatternProcessor, hashCurrentPatternProcessor, emojiShortCodeProcessor, emojiProcessor, - }, title) + }, titleHTML) } // PostProcessDescriptionHTML will use similar logic as PostProcess, but will // use a single special linkProcessor. -func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, []processor{ +func PostProcessDescriptionHTML(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, []processor{ descriptionLinkProcessor, emojiShortCodeProcessor, emojiProcessor, @@ -243,17 +244,18 @@ func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, err } // PostProcessEmoji for when we want to just process emoji and shortcodes -// in various places it isn't already run through the normal markdown processor -func PostProcessEmoji(ctx *RenderContext, content string) (string, error) { - return postProcessString(ctx, emojiProcessors, content) +// in various places it isn't already run through the normal Markdown processor +func PostProcessEmoji(ctx *RenderContext, content template.HTML) template.HTML { + return postProcessHTML(ctx, emojiProcessors, content) } -func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { +func postProcessHTML(ctx *RenderContext, procs []processor, content template.HTML) template.HTML { var buf strings.Builder - if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { - return "", err + if err := postProcess(ctx, procs, strings.NewReader(string(content)), &buf); err != nil { + log.Warn("postProcessHTML err: %v, input: %s", err, util.TruncateRunes(string(content), 200)) + return content } - return buf.String(), nil + return template.HTML(buf.String()) } func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]string, out io.Writer) { diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 186489ae722..6036f368901 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -5,6 +5,7 @@ package markup import ( "fmt" + "html/template" "strconv" "strings" "testing" @@ -260,9 +261,8 @@ func TestRender_PostProcessIssueTitle(t *testing.T) { "repo": "someRepo", "style": IssueNameStyleNumeric, } - actual, err := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") - assert.NoError(t, err) - assert.Equal(t, "#1", actual) + actual := PostProcessIssueTitle(NewTestRenderContext(metas), "#1") + assert.Equal(t, template.HTML("#1"), actual) } func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 3c7f9b0a7a1..81941902aee 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -11,7 +11,6 @@ import ( "net/url" "regexp" "strings" - "unicode" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" @@ -39,60 +38,35 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { return &RenderUtils{ctx: ctx} } -// RenderCommitMessage renders commit message with XSS-safe and special links. +// RenderCommitMessage renders commit message title (only title) func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTML(template.HTMLEscapeString(msg)) - // we can safely assume that it will not return any error, since there shouldn't be any special HTML. - // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. - fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } - msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") - if len(msgLines) == 0 { - return "" - } - return renderCodeBlock(template.HTML(msgLines[0])) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rendered := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) template.HTML { - msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) - lineEnd := strings.IndexByte(msgLine, '\n') - if lineEnd > 0 { - msgLine = msgLine[:lineEnd] - } - msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace) - if len(msgLine) == 0 { - return "" - } - - // we can safely assume that it will not return any error, since there shouldn't be any special HTML. - renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine)) - if err != nil { - log.Error("PostProcessCommitMessageSubject: %v", err) - return "" - } - return renderCodeBlock(template.HTML(renderedMessage)) + msgLine := strings.TrimSpace(msg) + msgLine, _, _ = strings.Cut(msgLine, "\n") + msgLine = strings.TrimSpace(msgLine) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + rendered := markup.PostProcessCommitMessageSubject(rctx, urlDefault, htmlutil.EscapeString(msgLine)) + return renderCodeBlock(rendered) } // RenderCommitBody extracts the body of a commit message without its title. func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { _, body, _ := strings.Cut(strings.TrimSpace(msg), "\n") - body = strings.TrimFunc(body, unicode.IsSpace) + body = strings.TrimSpace(body) if body == "" { return "" } - rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) - htmlContent := template.HTML(template.HTMLEscapeString(body)) - renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) - if err != nil { - log.Error("PostProcessCommitMessage: %v", err) - return "" - } + renderedMessage := markup.PostProcessCommitMessage(rctx, htmlutil.EscapeString(body)) return renderedMessage } @@ -108,25 +82,15 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { // RenderIssueTitle renders issue/pull title with defined post processors func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { // wrap "`…`" in before post-processing so code-span content stays literal, like comment bodies - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), string(htmlWithCode)) - if err != nil { - log.Error("PostProcessIssueTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlWithCode) } // RenderIssueSimpleTitle only renders with emoji and inline code block func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { // see RenderIssueTitle: wrap code spans before processing emoji - htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), string(htmlWithCode)) - if err != nil { - log.Error("RenderIssueSimpleTitle: %v", err) - return "" - } - return template.HTML(renderedText) + htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlWithCode) } func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { @@ -202,12 +166,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { // RenderEmoji renders html text with emoji post processors func (ut *RenderUtils) RenderEmoji(text string) template.HTML { - renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) - if err != nil { - log.Error("RenderEmoji: %v", err) - return "" - } - return template.HTML(renderedText) + return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlutil.EscapeString(text)) } // reactionToEmoji renders emoji for use in reactions diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 50a443c7468..5a28a1feba8 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -131,24 +131,24 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit }) t.Run("RenderCommitMessage", func(t *testing.T) { - expected := `space @mention-user ` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo)) }) t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) { - expected := `space @mention-user` + expected := `space @mention-user` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectURLOnly", func(t *testing.T) { // a bare URL in the subject must not hijack the default link - expected := `https://example.com/file.bin` + expected := `https://example.com/file.bin` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("https://example.com/file.bin", "https://example.com/link", mockRepo)) }) t.Run("RenderCommitMessageLinkSubjectPartialURL", func(t *testing.T) { // a URL embedded in larger subject text still becomes its own link - expected := `see https://example.com/x here` + expected := `see https://example.com/x here` assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo)) }) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 7cb2f82a903..ff3496629fa 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -414,11 +414,7 @@ func Diff(ctx *context.Context) { ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)}) htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, htmlMessage) - if err != nil { - ctx.ServerError("PostProcessCommitMessage", err) - return - } + ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage) } else if !git.IsErrNotExist(err) { log.Error("GetNote: %v", err) } diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index edc167e706b..58dc222cf5b 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -13,13 +13,10 @@
- - {{if $run.Title}} - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $run.Title $run.Link $.Repository}} - {{else}} - {{ctx.Locale.Tr "actions.runs.empty_commit_message"}} - {{end}} - +
+ {{$title := or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}} + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $title $run.Link $.Repository}} +
{{$workflowName := index $.WorkflowNames $run.WorkflowID}} {{if not $.CurWorkflow}}{{if $workflowName}}{{$workflowName}}{{else}}{{$run.WorkflowID}}{{end}} {{end}}#{{$run.Index}}: diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1a635f20199..cecf9920acd 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -92,3 +92,17 @@ margin-right: 8px; text-align: left; } + +/* for "title (#123)": + + title ( + #123 + ) + + * hover on "title": also highlight the right parentheses + * hover on "#123": don't highlight other parts + */ +.title-full-link-hover:not(:has(:not(.title-full-link):hover)):hover > a.title-full-link { + color: var(--color-primary); + text-decoration: underline; +} From e88650cfcf503f45b97d976f76026423ba85135e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 6 Jun 2026 17:24:03 +0800 Subject: [PATCH 21/88] chore: fix various layout problems (#37983) Fix various misaligments, fix space between list item bar items, remove deadcode (milestone dashboard) --- templates/projects/list.tmpl | 22 ++++----- templates/repo/issue/milestones.tmpl | 34 ++++++------- templates/shared/issuelist.tmpl | 18 ++++--- templates/user/dashboard/milestones.tmpl | 43 ++++++---------- tests/e2e/issue-project.test.ts | 2 +- tests/e2e/utils.ts | 2 +- tests/integration/issue_test.go | 2 +- web_src/css/base.css | 5 -- web_src/css/index.css | 1 - web_src/css/modules/svg.css | 2 +- web_src/css/repo/issue-list.css | 38 ++++++++++++--- web_src/css/shared/flex-list.css | 10 ++-- web_src/css/shared/milestone.css | 62 ------------------------ 13 files changed, 93 insertions(+), 148 deletions(-) delete mode 100644 web_src/css/shared/milestone.css diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index 346f4002b40..9fedd05f949 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -41,26 +41,26 @@
-
+
{{/* the milestone-list class is kept because many tests depend on it */}} {{range .Projects}} -
  • -

    +
    + {{svg .IconName 16}} - {{.Title}} -

    -
    -
    -
    + {{.Title}} + +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if and $.CanWriteProjects (not $.Repository.IsArchived)}} -
    +
    {{svg "octicon-pencil" 14}}{{ctx.Locale.Tr "repo.issues.label_edit"}} {{if .IsClosed}} {{svg "octicon-check" 14}}{{ctx.Locale.Tr "repo.projects.open"}} @@ -74,7 +74,7 @@ {{if .Description}}
    {{.RenderedContent}}
    {{end}} -
  • +
    {{else}} {{if and (eq .OpenCount 0) (eq .ClosedCount 0)}}
    diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 90e2a8ae3b9..4af4697b44f 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -15,42 +15,42 @@ {{template "repo/issue/filters" .}} -
    +
    {{range .Milestones}} -
  • -
    -

    +
    +
    + {{svg "octicon-milestone" 16}} {{.Name}} -

    -
    - {{.Completeness}}% - + +
    + {{.Completeness}}% +
    -
    -
    -
    +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if .TotalTrackedTime}} -
    +
    {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Hour}}
    {{end}} {{if .UpdatedUnix}} -
    +
    {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.update_ago" (DateUtils.TimeSince .UpdatedUnix)}}
    {{end}} -
    +
    {{if .IsClosed}} {{$closedDate:= DateUtils.TimeSince .ClosedDateUnix}} {{svg "octicon-clock" 14}} @@ -69,7 +69,7 @@
    {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} -
  • +
    {{end}} {{template "base/paginate" .}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 11c4210b460..838caeb7b28 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -16,7 +16,7 @@
    - {{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} + {{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}} {{if .IsPull}} {{if (index $.CommitStatuses .PullRequest.ID)}} {{/* make the "flex" children align with parent "inline" */}} @@ -37,14 +37,16 @@
    {{end}}
    -
    - +
    + {{if eq $.listType "dashboard"}} {{.Repo.FullName}}#{{.Index}} {{else}} #{{.Index}} {{end}} + +
    {{$timeStr := DateUtils.TimeSince .GetLastEventTimestamp}} {{if .OriginalAuthor}} {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .OriginalAuthor}} @@ -53,6 +55,8 @@ {{else}} {{ctx.Locale.Tr .GetLastEventLabelFake $timeStr .Poster.GetDisplayName}} {{end}} +
    + {{if .IsPull}}
    @@ -99,11 +103,9 @@ {{end}} {{if ne .DeadlineUnix 0}} - - - {{svg "octicon-calendar" 14}} - {{DateUtils.AbsoluteShort .DeadlineUnix}} - + + {{svg "octicon-calendar" 14}} + {{DateUtils.AbsoluteShort .DeadlineUnix}} {{end}} {{if .IsPull}} diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index d5dd64b1a34..62c2df7e8f0 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -71,45 +71,45 @@
    -
    +
    {{range .Milestones}} -
  • -
    -

    +
    +
    + {{.Repo.FullName}} {{svg "octicon-milestone" 16}} {{.Name}} -

    -
    - {{.Completeness}}% - + +
    + {{.Completeness}}% +
    -
    -
    -
    +
    +
    +
    {{svg "octicon-issue-opened" 14}} {{ctx.Locale.PrettyNumber .NumOpenIssues}} {{ctx.Locale.Tr "repo.issues.open_title"}}
    -
    +
    {{svg "octicon-check" 14}} {{ctx.Locale.PrettyNumber .NumClosedIssues}} {{ctx.Locale.Tr "repo.issues.closed_title"}}
    {{if .TotalTrackedTime}} -
    +
    {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Hour}}
    {{end}} {{if .UpdatedUnix}} -
    +
    {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.milestones.update_ago" (DateUtils.TimeSince .UpdatedUnix)}}
    {{end}} -
    +
    {{if .IsClosed}} {{$closedDate:= DateUtils.TimeSince .ClosedDateUnix}} {{svg "octicon-clock" 14}} @@ -127,22 +127,11 @@ {{end}}
    - {{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}} - - {{end}}
    {{if .Content}}
    {{.RenderedContent}}
    {{end}} -
  • +
    {{end}} {{template "base/paginate" .}} diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts index 1595cfadc5a..10466cd822f 100644 --- a/tests/e2e/issue-project.test.ts +++ b/tests/e2e/issue-project.test.ts @@ -372,7 +372,7 @@ test('close project and view in closed projects list', async ({page}) => { await expect(page.locator('.milestone-list')).toContainText(closedProjectTitle); // Close the second project by clicking the close link - const projectCard = page.locator('.milestone-card').filter({hasText: closedProjectTitle}); + const projectCard = page.locator('.milestone-list > .item').filter({hasText: closedProjectTitle}); await projectCard.locator('a.link-action[data-url$="/close"]').click(); // Wait for redirect back to project view page diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 8dcd6aedae2..5b19793f37b 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -159,7 +159,7 @@ export async function createProject( await page.waitForURL(new RegExp(`/${owner}/${repo}/projects$`)); // Extract the project ID from the project link in the list - const projectLink = page.locator('.milestone-list .milestone-card').filter({hasText: title}).locator('a').first(); + const projectLink = page.locator('.milestone-list > .item').filter({hasText: title}).locator('a').first(); const href = await projectLink.getAttribute('href'); const match = /\/projects\/(\d+)/.exec(href || ''); const id = match ? parseInt(match[1]) : 0; diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index aaeba60d8e9..ffb491344b3 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -34,7 +34,7 @@ import ( func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { issueList := htmlDoc.doc.Find("#issue-list") assert.Equal(t, 1, issueList.Length()) - return issueList.Find(".item").Find(".issue-item-title") + return issueList.Find(".item").Find(".list-item-large-title") } func getIssue(t *testing.T, repoID int64, issueSelection *goquery.Selection) *issues_model.Issue { diff --git a/web_src/css/base.css b/web_src/css/base.css index 49ddca57a30..0716ad1913e 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -454,11 +454,6 @@ img.ui.avatar, margin-right: 4px; } -.ui.inline.delete-button { - padding: 8px 15px; - font-weight: var(--font-weight-normal); -} - .ui .migrate { color: var(--color-text-light-2) !important; } diff --git a/web_src/css/index.css b/web_src/css/index.css index 89ad63ba1a8..a56982efcf7 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -37,7 +37,6 @@ @import "./modules/charescape.css"; @import "./shared/flex-list.css"; -@import "./shared/milestone.css"; @import "./shared/settings.css"; @import "./features/dropzone.css"; diff --git a/web_src/css/modules/svg.css b/web_src/css/modules/svg.css index 7adfc77a818..b772f98ac6f 100644 --- a/web_src/css/modules/svg.css +++ b/web_src/css/modules/svg.css @@ -2,7 +2,7 @@ and material icons should have no "fill" set explicitly, otherwise some like ".editorconfig" won't render correctly */ .svg:not(.git-entry-icon) { display: inline-block; - vertical-align: text-top; + vertical-align: middle; /* middle is the best choice for different font sizes from 1px to 36px when no flex */ fill: currentcolor; } diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index cecf9920acd..dfffe201874 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -34,14 +34,6 @@ } } -#issue-list .issue-item-title { - font-size: 16px; - font-weight: var(--font-weight-semibold); - color: var(--color-text); - text-decoration: none; - overflow-wrap: anywhere; -} - #issue-list .branches { display: inline-flex; } @@ -106,3 +98,33 @@ color: var(--color-primary); text-decoration: underline; } + +.list-item-large-title { + font-size: 16px; + font-weight: var(--font-weight-semibold); + color: var(--color-text); + text-decoration: none; + overflow-wrap: anywhere; +} + +.list-item-title-progress { + width: 200px; + height: 16px; +} + +.list-item-secondary-bar { + display: flex; + flex-wrap: wrap; + gap: var(--gap-block); + justify-content: space-between; + color: var(--color-text-light-2); +} + +.list-item-secondary-bar a { + color: var(--color-text-light-2); + text-decoration: none; +} + +.list-item-secondary-bar a:hover { + color: var(--color-text); +} diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index 1acaa362e22..b224fe667c6 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -32,7 +32,7 @@ .items-with-main > .item { display: flex; - gap: 8px; + gap: var(--gap-block); align-items: flex-start; } @@ -44,7 +44,7 @@ .items-with-main > .item .item-main { display: flex; flex-direction: column; - gap: 0.25em; + gap: var(--gap-inline); flex-grow: 1; flex-basis: 60%; /* avoid wrapping the "item-trailing" too aggressively */ min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */ @@ -52,7 +52,7 @@ .items-with-main > .item .item-header { display: flex; - gap: .25rem; + gap: var(--gap-inline); justify-content: space-between; flex-wrap: wrap; } @@ -63,7 +63,7 @@ .items-with-main > .item .item-trailing { display: flex; - gap: 0.5rem; + gap: var(--gap-block); align-items: center; flex-grow: 0; flex-wrap: wrap; @@ -92,7 +92,7 @@ display: flex; align-items: center; flex-wrap: wrap; - gap: .25rem; + gap: var(--gap-inline); color: var(--color-text-light-2); overflow-wrap: anywhere; } diff --git a/web_src/css/shared/milestone.css b/web_src/css/shared/milestone.css deleted file mode 100644 index 47e822f8d3a..00000000000 --- a/web_src/css/shared/milestone.css +++ /dev/null @@ -1,62 +0,0 @@ -.milestone-list { - list-style: none; -} - -.milestone-card { - width: 100%; - padding-top: 10px; - padding-bottom: 10px; -} - -.milestone-card + .milestone-card { - border-top: 1px solid var(--color-secondary); -} - -.milestone-card .render-content { - padding-top: 10px; -} - -.milestone-header progress { - width: 200px; - height: 16px; -} - -.milestone-header { - display: flex; - align-items: center; - margin: 0; - flex-wrap: wrap; - justify-content: space-between; -} - -.milestone-toolbar { - padding-top: 5px; - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: space-between; -} - -.milestone-toolbar .group { - color: var(--color-text-light-2); - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.milestone-toolbar .group > a { - font-size: 15px; - color: var(--color-text-light-2); -} - -.milestone-toolbar .group > a:hover { - color: var(--color-text); -} - -@media (max-width: 767.98px) { - .milestone-card { - display: flex; - flex-direction: column; - gap: 8px; - } -} From 743bbaa9c2a77894302d819a48c33c36cee31946 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 6 Jun 2026 13:06:08 +0200 Subject: [PATCH 22/88] fix: refactor git error handling and make archive streaming handle non-existing commit id (#38007) Co-authored-by: wxiaoguang --- modules/git/gitcmd/error.go | 62 +++++++++++++++--------- modules/git/remote.go | 6 +-- modules/git/repo_commit.go | 6 +-- modules/git/repo_commit_nogogit.go | 14 +++--- modules/git/tree_nogogit.go | 3 +- routers/api/v1/repo/pull.go | 2 +- routers/web/repo/pull.go | 4 +- services/repository/archiver/archiver.go | 6 +-- services/wiki/wiki.go | 2 +- tests/integration/wiki_test.go | 2 +- 10 files changed, 57 insertions(+), 50 deletions(-) diff --git a/modules/git/gitcmd/error.go b/modules/git/gitcmd/error.go index 436f4e18ae4..8d5ec5f5e69 100644 --- a/modules/git/gitcmd/error.go +++ b/modules/git/gitcmd/error.go @@ -9,6 +9,8 @@ import ( "fmt" "os/exec" "strings" + + "gitea.dev/modules/util" ) type RunStdError interface { @@ -41,32 +43,14 @@ func (r *runStdError) Stderr() string { } func ErrorAsStderr(err error) (string, bool) { - var runErr RunStdError - if errors.As(err, &runErr) { + if runErr, ok := errors.AsType[RunStdError](err); ok { return runErr.Stderr(), true } return "", false } -func StderrHasPrefix(err error, prefix string) bool { - stderr, ok := ErrorAsStderr(err) - if !ok { - return false - } - return strings.HasPrefix(stderr, prefix) -} - -func StderrContains(err error, sub string) bool { - stderr, ok := ErrorAsStderr(err) - if !ok { - return false - } - return strings.Contains(stderr, sub) -} - func IsErrorExitCode(err error, code int) bool { - var exitError *exec.ExitError - if errors.As(err, &exitError) { + if exitError, ok := errors.AsType[*exec.ExitError](err); ok { return exitError.ExitCode() == code } return false @@ -85,11 +69,41 @@ func IsErrorCanceledOrKilled(err error) bool { return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) } -func IsStdErrorNotValidObjectName(err error) bool { +type StderrPrefix string + +type StderrSubStr string + +const ( + StderrNotValidObjectName StderrPrefix = "fatal: not a valid object name" + StderrNotTreeObject StderrPrefix = "fatal: not a tree object" + StderrPathSpec StderrPrefix = "fatal: pathspec" + StderrBadRevision StderrPrefix = "fatal: bad revision" + + StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128 + StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2 + + // fatal: ambiguous argument 'origin': unknown revision or path not in the working tree. + StderrUnknownRevisionOrPath StderrSubStr = "unknown revision or path not in the working tree" +) + +func IsStderr[T StderrPrefix | StderrSubStr](err error, check T) bool { stderr, ok := ErrorAsStderr(err) - // Git is lowercasing the "fatal: Not a valid object name" error message - // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com - return ok && strings.Contains(strings.ToLower(stderr), "fatal: not a valid object name") + if !ok { + return false + } + checkLen := len(check) + if len(stderr) < checkLen { + return false + } + switch any(check).(type) { + case StderrPrefix: + // Git is lowercasing the "fatal: Not a valid object name" error message + // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com + return util.AsciiEqualFold(stderr[:checkLen], string(check)) + case StderrSubStr: + return strings.Contains(stderr, string(check)) + } + return false } type pipelineError struct { diff --git a/modules/git/remote.go b/modules/git/remote.go index b4fe6cfd100..e94ca374e4d 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -66,11 +66,7 @@ func (err *ErrInvalidCloneAddr) Unwrap() error { // IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. func IsRemoteNotExistError(err error) bool { - // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 - // Should not add space in the end, sometimes git will add a `:` - prefix1 := "fatal: No such remote" // git < 2.30, exit status 128 - prefix2 := "error: No such remote" // git >= 2.30. exit status 2 - return gitcmd.StderrHasPrefix(err, prefix1) || gitcmd.StderrHasPrefix(err, prefix2) + return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote1) || gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote2) } // ParseRemoteAddr checks if given remote address is valid, diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index b955f2709b3..20827062df3 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -24,9 +24,9 @@ func (repo *Repository) GetTagCommitID(name string) (string, error) { return repo.GetRefCommitID(TagPrefix + name) } -// GetCommit returns commit object of by ID string. -func (repo *Repository) GetCommit(commitID string) (*Commit, error) { - id, err := repo.ConvertToGitID(commitID) +// GetCommit returns a commit object of by the git ref. +func (repo *Repository) GetCommit(ref string) (*Commit, error) { + id, err := repo.ConvertToGitID(ref) if err != nil { return nil, err } diff --git a/modules/git/repo_commit_nogogit.go b/modules/git/repo_commit_nogogit.go index c90650532b8..d8b842beeed 100644 --- a/modules/git/repo_commit_nogogit.go +++ b/modules/git/repo_commit_nogogit.go @@ -109,16 +109,16 @@ func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Co } } -// ConvertToGitID returns a GitHash object from a potential ID string -func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { +// ConvertToGitID returns a git object ID from the git ref, it doesn't guarantee the returned ID really exists +func (repo *Repository) ConvertToGitID(ref string) (ObjectID, error) { objectFormat, err := repo.GetObjectFormat() if err != nil { return nil, err } - if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { - ID, err := NewIDFromString(commitID) + if len(ref) == objectFormat.FullLength() && objectFormat.IsValid(ref) { + id, err := NewIDFromString(ref) if err == nil { - return ID, nil + return id, nil } } @@ -127,10 +127,10 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { return nil, err } defer cancel() - info, err := batch.QueryInfo(commitID) + info, err := batch.QueryInfo(ref) if err != nil { if IsErrNotExist(err) { - return nil, ErrNotExist{commitID, ""} + return nil, ErrNotExist{ref, ""} } return nil, err } diff --git a/modules/git/tree_nogogit.go b/modules/git/tree_nogogit.go index 1d78ecdee24..ebb4c7bb122 100644 --- a/modules/git/tree_nogogit.go +++ b/modules/git/tree_nogogit.go @@ -7,7 +7,6 @@ package git import ( "io" - "strings" "gitea.dev/modules/git/gitcmd" ) @@ -65,7 +64,7 @@ func (t *Tree) ListEntries() (Entries, error) { stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { - if gitcmd.IsStdErrorNotValidObjectName(runErr) || strings.Contains(runErr.Error(), "fatal: not a tree object") { + if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) { return nil, ErrNotExist{ ID: t.ID.String(), } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 123c0c671a9..6f6ba77fae0 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1435,7 +1435,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false) } - if gitcmd.StderrHasPrefix(err, "fatal: bad revision") { + if gitcmd.IsStderr(err, gitcmd.StderrBadRevision) { ctx.APIError(http.StatusNotFound, "invalid base branch or revision") return } else if err != nil { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 6862a3efc0f..eb6d5408a52 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -376,9 +376,7 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Conte pull := prInfo.issue.PullRequest prInfo.CompareInfo, err = git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, baseRef, git.RefName(pull.GetGitHeadRefName()), false, false) if err != nil { - isKnownErrorForBroken := gitcmd.IsStdErrorNotValidObjectName(err) || - // fatal: ambiguous argument 'origin': unknown revision or path not in the working tree. - gitcmd.StderrContains(err, "unknown revision or path not in the working tree") + isKnownErrorForBroken := gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(err, gitcmd.StderrUnknownRevisionOrPath) if !isKnownErrorForBroken { log.Error("GetCompareInfo: %v", err) } diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go index 93e83b51de5..2b781185564 100644 --- a/services/repository/archiver/archiver.go +++ b/services/repository/archiver/archiver.go @@ -56,13 +56,13 @@ func NewRequest(repo *repo_model.Repository, gitRepo *git.Repository, archiveRef } // Get corresponding commit. - commitID, err := gitRepo.ConvertToGitID(archiveRefShortName) + commit, err := gitRepo.GetCommit(archiveRefShortName) if err != nil { return nil, util.NewNotExistErrorf("unrecognized repository reference: %s", archiveRefShortName) } r := &ArchiveRequest{Repo: repo, archiveRefShortName: archiveRefShortName, Type: archiveType, Paths: paths} - r.CommitID = commitID.String() + r.CommitID = commit.ID.String() return r, nil } @@ -330,7 +330,7 @@ func ServeRepoArchive(ctx *gitea_context.Base, archiveReq *ArchiveRequest) error // because errors may happen in git command and such cases aren't in our control. httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{Filename: downloadName}) if err := archiveReq.Stream(ctx, ctx.Resp); err != nil && !ctx.Written() { - if gitcmd.StderrHasPrefix(err, "fatal: pathspec") { + if gitcmd.IsStderr(err, gitcmd.StderrPathSpec) || gitcmd.IsStderr(err, gitcmd.StderrNotTreeObject) { return util.NewInvalidArgumentErrorf("path doesn't exist or is invalid") } return fmt.Errorf("archive repo %s: failed to stream: %w", archiveReq.Repo.FullName(), err) diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 9fcc5750ec9..abe846ae063 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -59,7 +59,7 @@ func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath // Look for both files filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath) if err != nil { - if gitcmd.IsStdErrorNotValidObjectName(err) { + if gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) { return false, gitPath, nil // branch doesn't exist } log.Error("Wiki LsTree failed, err: %v", err) diff --git a/tests/integration/wiki_test.go b/tests/integration/wiki_test.go index 0c1b00e5076..11eef67c685 100644 --- a/tests/integration/wiki_test.go +++ b/tests/integration/wiki_test.go @@ -72,7 +72,7 @@ EOF // reader can't push _, _, runErr = gitcmd.NewCommand("push", "origin", "refs/heads/master").WithDir(dstLocalPath).RunStdString(t.Context()) - assert.True(t, gitcmd.StderrContains(runErr, "remote: Repository not found\n")) + assert.Contains(t, runErr.Stderr(), "remote: Repository not found\n") req := NewRequest(t, "GET", "/user2/repo1/wiki/raw/Home.md") resp := MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "This is the home page!") From 42513398c05ca6bdf71da76cb6f9baaebe8cb924 Mon Sep 17 00:00:00 2001 From: bircni Date: Sat, 6 Jun 2026 17:44:56 +0200 Subject: [PATCH 23/88] fix(lfs): reject unknown SSH LFS sub-verbs to prevent auth bypass (#38008) An authenticated SSH user could pass a malformed sub-verb (e.g. `git-lfs-authenticate badverb`) so getAccessMode falls through to AccessModeNone (0). The permission check in routers/private/serv.go then evaluates `userMode < 0` which is always false, granting a valid LFS JWT for any private repository. The HTTP LFS handler only validates the Op claim on writes, so the token works for downloads. Validate the sub-verb in runServ before calling getAccessMode and fail fast for anything other than upload/download. --- cmd/serv.go | 23 ++++++++++++-------- cmd/serv_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 cmd/serv_test.go diff --git a/cmd/serv.go b/cmd/serv.go index 93009cd32a3..b39076f6a78 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { return nil } -func getAccessMode(verb, lfsVerb string) perm.AccessMode { +// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with +// ok=false for an unrecognised verb. Callers MUST reject the request when ok is +// false: AccessModeNone would otherwise pass the `userMode < mode` permission +// check in routers/private/serv.go and grant access. +func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) { switch verb { case git.CmdVerbUploadPack, git.CmdVerbUploadArchive: - return perm.AccessModeRead + return perm.AccessModeRead, true case git.CmdVerbReceivePack: - return perm.AccessModeWrite + return perm.AccessModeWrite, true case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer: switch lfsVerb { case git.CmdSubVerbLfsUpload: - return perm.AccessModeWrite + return perm.AccessModeWrite, true case git.CmdSubVerbLfsDownload: - return perm.AccessModeRead + return perm.AccessModeRead, true } } - // should be unreachable - setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb) - return perm.AccessModeNone + return perm.AccessModeNone, false } func runServ(ctx context.Context, c *cli.Command) error { @@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error { } } - requestedMode := getAccessMode(verb, lfsVerb) + requestedMode, ok := getAccessMode(verb, lfsVerb) + if !ok { + return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb) + } results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) if extra.HasError() { diff --git a/cmd/serv_test.go b/cmd/serv_test.go new file mode 100644 index 00000000000..3bdcba1df37 --- /dev/null +++ b/cmd/serv_test.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "gitea.dev/models/perm" + "gitea.dev/modules/git" + + "github.com/stretchr/testify/assert" +) + +func TestGetAccessMode(t *testing.T) { + cases := []struct { + verb, lfsVerb string + expected perm.AccessMode + }{ + {git.CmdVerbUploadPack, "", perm.AccessModeRead}, + {git.CmdVerbUploadArchive, "", perm.AccessModeRead}, + {git.CmdVerbReceivePack, "", perm.AccessModeWrite}, + {git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite}, + {git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead}, + {git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite}, + {git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead}, + } + for _, tc := range cases { + t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) { + mode, ok := getAccessMode(tc.verb, tc.lfsVerb) + assert.True(t, ok) + assert.Equal(t, tc.expected, mode) + }) + } +} + +// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports +// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An +// unknown verb has no valid access mode; if it were treated as AccessModeNone (0) +// it would pass the `userMode < mode` permission check in routers/private/serv.go +// and hand out valid LFS JWTs for any private repository. +func TestGetAccessModeUnknownVerb(t *testing.T) { + cases := []struct{ verb, lfsVerb string }{ + {git.CmdVerbLfsAuthenticate, ""}, + {git.CmdVerbLfsAuthenticate, "badverb"}, + {git.CmdVerbLfsTransfer, "badverb"}, + {"git-unknown-verb", ""}, + } + for _, tc := range cases { + t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) { + mode, ok := getAccessMode(tc.verb, tc.lfsVerb) + assert.False(t, ok) + assert.Equal(t, perm.AccessModeNone, mode) + }) + } +} From c43eb7c33a100ffc7b2367adf165f7085e0ccdc5 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 7 Jun 2026 00:07:47 +0200 Subject: [PATCH 24/88] fix(auth): do not auto-reactivate disabled users on OAuth2 callback (#38009) The OAuth2 sign-in callback unconditionally set IsActive=true on the local user row whenever the IdP authenticated them, silently undoing an administrator's "Disable Account" action and granting the user a fresh session in the same response. Treat the local IsActive flag as an authoritative admin override: inactive users get a session and are routed through the existing activate / prohibit-login pages by verifyAuthWithOptions, matching the local-credentials sign-in path. Adds an integration regression test that disables a linked local user and asserts the row stays IsActive=false after a full OIDC callback. --------- Co-authored-by: wxiaoguang --- models/unittest/unit_tests.go | 2 +- routers/web/auth/oauth.go | 16 ++++++- services/auth/source/oauth2/source_sync.go | 4 +- tests/integration/auth_oauth2_test.go | 54 ++++++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 45ea0d175ea..c9895500214 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -47,7 +47,7 @@ func OrderBy(orderBy string) any { } func whereOrderConditions(e db.Engine, conditions []any) db.Engine { - orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic + orderBy := "id" // query must have the "ORDER BY", otherwise the result is not deterministic. FIXME: some tables do not have "id" column for _, condition := range conditions { switch cond := condition.(type) { case *testCond: diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index f7d9c3c34ac..06c1b20d578 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -364,9 +364,21 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m opts := &user_service.UpdateOptions{} - // Reactivate user if they are deactivated + // HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION: see services/auth/source/oauth2/source_sync.go + // Reactivate user only if they were disabled by the OAuth2 auto sync cron (invalid_grant), + // which clears AccessToken/RefreshToken/ExpiresAt on the ExternalLoginUser row + // An admin-disabled user has no such signature, so we leave IsActive alone + // and let verifyAuthWithOptions route them through the prohibit-login / activate page. if !u.IsActive { - opts.IsActive = optional.Some(true) + extLogin, hasExt, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID) + if err != nil { + ctx.ServerError("GetExternalLogin", err) + return + } + isDisabledByAutoSync := hasExt && extLogin.RefreshToken == "" + if isDisabledByAutoSync { + opts.IsActive = optional.Some(true) + } } if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { diff --git a/services/auth/source/oauth2/source_sync.go b/services/auth/source/oauth2/source_sync.go index 8fc58f9ae10..3123e54bbb2 100644 --- a/services/auth/source/oauth2/source_sync.go +++ b/services/auth/source/oauth2/source_sync.go @@ -88,8 +88,8 @@ func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *us } } - // Delete stored tokens, since they are invalid. This - // also provents us from checking this in subsequent runs. + // HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION + // Delete stored tokens, since they are invalid. This also prevents us from checking this in subsequent runs. u.AccessToken = "" u.RefreshToken = "" u.ExpiresAt = time.Time{} diff --git a/tests/integration/auth_oauth2_test.go b/tests/integration/auth_oauth2_test.go index 3bb3d56e7bd..7978c3bd048 100644 --- a/tests/integration/auth_oauth2_test.go +++ b/tests/integration/auth_oauth2_test.go @@ -13,6 +13,7 @@ import ( "time" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" "gitea.dev/modules/json" @@ -23,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "xorm.io/builder" ) // TestMigrateAzureADV2ToOIDC simulates a login source migration from the Azure AD V2 OAuth2 provider to the OpenID Connect provider, @@ -191,6 +193,58 @@ func TestOIDCIgnoresStaleExternalLoginLinks(t *testing.T) { }) } +// TestOAuth2CallbackReactivationGating exercises the gate in handleOAuth2SignIn: +// an inactive user can only be reactivated when who was disabled by auto-sync +func TestOAuth2CallbackReactivationGating(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)() + defer test.MockVariableValue(&setting.OAuth2Client.Username, setting.OAuth2UsernameUserid)() + + srv := newFakeOIDCServer(t, FakeOIDCConfig{Sub: "test-sub", Email: "test@example.com", Name: "Test User"}) + addOAuth2Source(t, "test-oauth-source", oauth2.Source{ + Provider: "openidConnect", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + OpenIDConnectAutoDiscoveryURL: srv.URL + "/.well-known/openid-configuration", + }) + authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "test-oauth-source") + require.NoError(t, err) + + u := &user_model.User{Name: "test-user", Email: "test@example.com"} + require.NoError(t, user_model.CreateUser(t.Context(), u, &user_model.Meta{})) + + extLink := &user_model.ExternalLoginUser{ + UserID: u.ID, + LoginSourceID: authSource.ID, + Provider: authSource.Name, + ExternalID: "test-sub", + } + require.NoError(t, user_model.LinkExternalToUser(t.Context(), u, extLink)) + + prepareUserExternalLink := func(t *testing.T, refreshToken string) { + err := user_model.UpdateUserCols(t.Context(), &user_model.User{ID: u.ID, IsActive: false}, "is_active") + require.NoError(t, err) + _, err = db.GetEngine(t.Context()).Where(builder.Eq{"user_id": u.ID}).Cols("refresh_token"). + Update(&user_model.ExternalLoginUser{RefreshToken: refreshToken}) + require.NoError(t, err) + require.False(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}).IsActive) + } + + t.Run("admin-disabled user is not reactivated", func(t *testing.T) { + prepareUserExternalLink(t, "non-empty-refresh-token") + doOIDCSignIn(t, authSource.Name) + after := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}) + assert.False(t, after.IsActive, "OAuth callback must not re-enable an administrator-disabled account") + }) + + t.Run("auto-sync-disabled user is reactivated", func(t *testing.T) { + prepareUserExternalLink(t, "" /* empty refresh token */) + doOIDCSignIn(t, authSource.Name) + after := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: u.ID}) + assert.True(t, after.IsActive, "OAuth callback must reactivate a sync-disabled account on successful login") + }) +} + // FakeOIDCConfig holds configuration for the fake OIDC server used in tests. type FakeOIDCConfig struct { Sub string From 5fe4f962e87341cb5221f5069366bc1cf85d59ed Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 06:19:39 +0000 Subject: [PATCH 25/88] refactor(api): clarify APIError message usage and fix legacy lint error (#38012) Avoid unclear & fragile "any" tricks, fix various abuses Co-authored-by: wxiaoguang --- modules/markup/markdown/ast.go | 17 +++- .../markup/markdown/transform_blockquote.go | 5 +- modules/markup/markdown/transform_codespan.go | 9 +- routers/api/v1/admin/org.go | 2 +- routers/api/v1/admin/user.go | 46 +++++----- routers/api/v1/api.go | 4 +- routers/api/v1/notify/notifications.go | 4 +- routers/api/v1/notify/repo.go | 2 +- routers/api/v1/notify/threads.go | 4 +- routers/api/v1/notify/user.go | 2 +- routers/api/v1/org/action.go | 22 ++--- routers/api/v1/org/avatar.go | 2 +- routers/api/v1/org/label.go | 4 +- routers/api/v1/org/member.go | 2 +- routers/api/v1/org/org.go | 8 +- routers/api/v1/org/team.go | 4 +- routers/api/v1/packages/package.go | 16 ++-- routers/api/v1/repo/action.go | 44 ++++----- routers/api/v1/repo/avatar.go | 2 +- routers/api/v1/repo/blob.go | 2 +- routers/api/v1/repo/branch.go | 50 +++++------ routers/api/v1/repo/collaborators.go | 12 +-- routers/api/v1/repo/commits.go | 4 +- routers/api/v1/repo/download.go | 6 +- routers/api/v1/repo/file.go | 12 +-- routers/api/v1/repo/fork.go | 4 +- routers/api/v1/repo/issue.go | 20 ++--- routers/api/v1/repo/issue_attachment.go | 10 +-- routers/api/v1/repo/issue_comment.go | 12 +-- .../api/v1/repo/issue_comment_attachment.go | 12 +-- routers/api/v1/repo/issue_label.go | 2 +- routers/api/v1/repo/issue_lock.go | 5 +- routers/api/v1/repo/issue_pin.go | 2 +- routers/api/v1/repo/issue_reaction.go | 12 +-- routers/api/v1/repo/issue_subscription.go | 2 +- routers/api/v1/repo/issue_tracked_time.go | 19 ++-- routers/api/v1/repo/key.go | 2 +- routers/api/v1/repo/label.go | 4 +- routers/api/v1/repo/migrate.go | 10 +-- routers/api/v1/repo/mirror.go | 17 ++-- routers/api/v1/repo/pull.go | 44 ++++----- routers/api/v1/repo/pull_review.go | 28 +++--- routers/api/v1/repo/release.go | 10 +-- routers/api/v1/repo/release_attachment.go | 6 +- routers/api/v1/repo/repo.go | 34 +++---- routers/api/v1/repo/status.go | 2 +- routers/api/v1/repo/tag.go | 18 ++-- routers/api/v1/repo/teams.go | 6 +- routers/api/v1/repo/transfer.go | 22 ++--- routers/api/v1/repo/wiki.go | 10 +-- routers/api/v1/shared/action.go | 7 +- routers/api/v1/shared/block.go | 4 +- routers/api/v1/user/action.go | 22 ++--- routers/api/v1/user/app.go | 7 +- routers/api/v1/user/avatar.go | 2 +- routers/api/v1/user/email.go | 2 +- routers/api/v1/user/follower.go | 2 +- routers/api/v1/user/gpg_key.go | 2 +- routers/api/v1/user/star.go | 2 +- routers/api/v1/user/watch.go | 2 +- routers/api/v1/utils/sort.go | 4 +- services/context/api.go | 89 ++++++++++--------- services/context/package.go | 21 ++--- services/context/user.go | 15 ++-- 64 files changed, 395 insertions(+), 384 deletions(-) diff --git a/modules/markup/markdown/ast.go b/modules/markup/markdown/ast.go index f29f8837349..fae9d06e2a9 100644 --- a/modules/markup/markdown/ast.go +++ b/modules/markup/markdown/ast.go @@ -95,13 +95,13 @@ type Icon struct { // ColorPreview is an inline for a color preview type ColorPreview struct { ast.BaseInline - Color []byte + Color string } // Dump implements Node.Dump. func (n *ColorPreview) Dump(source []byte, level int) { m := map[string]string{} - m["Color"] = string(n.Color) + m["Color"] = n.Color ast.DumpHelper(n, source, level, m, nil) } @@ -114,7 +114,7 @@ func (n *ColorPreview) Kind() ast.NodeKind { } // NewColorPreview returns a new Span node. -func NewColorPreview(color []byte) *ColorPreview { +func NewColorPreview(color string) *ColorPreview { return &ColorPreview{ BaseInline: ast.BaseInline{}, Color: color, @@ -170,3 +170,14 @@ func (n *RawHTML) Kind() ast.NodeKind { func NewRawHTML(rawHTML template.HTML) *RawHTML { return &RawHTML{rawHTML: rawHTML} } + +func childSingleText(node ast.Node, source []byte) (string, bool) { + if node.FirstChild() == nil || node.FirstChild() != node.LastChild() { + return "", false + } + c, ok := node.FirstChild().(*ast.Text) + if !ok { + return "", false + } + return string(c.Segment.Value(source)), true +} diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go index b6cbef9a0a8..2ec7cec9d5e 100644 --- a/modules/markup/markdown/transform_blockquote.go +++ b/modules/markup/markdown/transform_blockquote.go @@ -46,7 +46,10 @@ func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.N if !ok { return "", nil } - val1 := string(node1.Text(reader.Source())) //nolint:staticcheck // Text is deprecated + val1, ok := childSingleText(node1, reader.Source()) + if !ok { + return "", nil + } attentionType := strings.ToLower(val1) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1} diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go index 900c38b129e..df4a56fd9e5 100644 --- a/modules/markup/markdown/transform_codespan.go +++ b/modules/markup/markdown/transform_codespan.go @@ -39,7 +39,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod r.Writer.RawWrite(w, value) } case *ColorPreview: - _ = r.renderInternal.FormatWithSafeAttrs(w, ``, string(v.Color)) + _ = r.renderInternal.FormatWithSafeAttrs(w, ``, v.Color) } } return ast.WalkSkipChildren, nil @@ -68,8 +68,11 @@ func cssColorHandler(value string) bool { } func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) { - colorContent := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated - if cssColorHandler(string(colorContent)) { + colorContent, ok := childSingleText(v, reader.Source()) + if !ok { + return + } + if cssColorHandler(colorContent) { v.AppendChild(v, NewColorPreview(colorContent)) } } diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 6375748086c..4713c7a1c78 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -67,7 +67,7 @@ func CreateOrg(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index d7f10b75b0b..9441fe0c18e 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -40,7 +40,7 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64 source, err := auth.GetSourceByID(ctx, sourceID) if err != nil { if auth.IsErrSourceNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -97,14 +97,12 @@ func CreateUser(ctx *context.APIContext) { if u.LoginType == auth.Plain { if len(form.Password) < setting.MinPasswordLength { - err := errors.New("PasswordIsRequired") - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, "PasswordIsRequired") return } if !password.IsComplexEnough(form.Password) { - err := errors.New("PasswordComplexity") - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, "PasswordComplexity") return } @@ -112,7 +110,7 @@ func CreateUser(ctx *context.APIContext) { if password.IsErrIsPwnedRequest(err) { log.Error(err.Error()) } - ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned")) + ctx.APIError(http.StatusBadRequest, "PasswordPwned") return } } @@ -143,7 +141,7 @@ func CreateUser(ctx *context.APIContext) { user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -204,11 +202,11 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil { switch { case errors.Is(err, password.ErrMinLength): - ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters", setting.MinPasswordLength)) case errors.Is(err, password.ErrComplexity): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } @@ -222,9 +220,9 @@ func EditUser(ctx *context.APIContext) { if !user_model.IsEmailDomainAllowed(*form.Email) { err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email) } - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case user_model.IsErrEmailAlreadyUsed(err): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } @@ -249,7 +247,7 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil { if user_model.IsErrDeleteLastAdminUser(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -289,13 +287,13 @@ func DeleteUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user") return } // admin should not delete themself if ctx.ContextUser.ID == ctx.Doer.ID { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself")) + ctx.APIError(http.StatusUnprocessableEntity, "you cannot delete yourself") return } @@ -304,7 +302,7 @@ func DeleteUser(ctx *context.APIContext) { org_model.IsErrUserHasOrgs(err) || packages_model.IsErrUserOwnPackages(err) || user_model.IsErrDeleteLastAdminUser(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -471,13 +469,11 @@ func SearchUsers(ctx *context.APIContext) { var visible []api.VisibleType visibilityParam := ctx.FormString("visibility") - if len(visibilityParam) > 0 { - if visibility, ok := api.VisibilityModes[visibilityParam]; ok { - visible = []api.VisibleType{visibility} - } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam)) - return - } + if visibility, ok := api.VisibilityModes[visibilityParam]; ok { + visible = []api.VisibleType{visibility} + } else if visibilityParam != "" { + ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility") + return } searchOpts := user_model.SearchUserOptions{ @@ -551,7 +547,7 @@ func RenameUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user") return } @@ -560,7 +556,7 @@ func RenameUser(ctx *context.APIContext) { // Check if username has been changed if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil { if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index eb5f52fd575..4248faea5d4 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -734,14 +734,14 @@ func mustEnableWiki(ctx *context.APIContext) { // FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName())) + ctx.APIError(http.StatusLocked, "repo is archived") return } } func mustEnableEditor(ctx *context.APIContext) { if !ctx.Repo.Repository.CanEnableEditor() { - ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName())) + ctx.APIError(http.StatusLocked, "repo is not allowed to edit") return } } diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go index 92aae46a784..8e032292c2e 100644 --- a/routers/api/v1/notify/notifications.go +++ b/routers/api/v1/notify/notifications.go @@ -28,7 +28,7 @@ func NewAvailable(ctx *context.APIContext) { Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, }) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -38,7 +38,7 @@ func NewAvailable(ctx *context.APIContext) { func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return nil } opts := &activities_model.FindNotificationOptions{ diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index 16996907508..7ba4469ff66 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 2b8c91169f8..c66db13f8b6 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -104,14 +104,14 @@ func getThread(ctx *context.APIContext) *activities_model.Notification { n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } return nil } if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("only user itself and admin are allowed to read/change this thread %d", n.ID)) return nil } return n diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 2e54eb178d4..9894f577c54 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -134,7 +134,7 @@ func ReadNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if !tmpLastRead.IsZero() { diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index d63a134bd55..3f1135dcf3f 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -111,9 +111,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -158,9 +158,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -277,7 +277,7 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -327,9 +327,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -387,13 +387,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -445,7 +445,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -462,7 +462,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go index 38b0655bea9..7e6f8a77c91 100644 --- a/routers/api/v1/org/avatar.go +++ b/routers/api/v1/org/avatar.go @@ -39,7 +39,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 4c3365e6a6d..443003d5de2 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -90,7 +90,7 @@ func CreateLabel(ctx *context.APIContext) { form.Color = strings.Trim(form.Color, " ") color, err := label.NormalizeColor(form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } form.Color = color @@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } l.Color = color diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 957544d1047..11b46a05c1c 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -221,7 +221,7 @@ func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model // allow org owners to change status of members isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID) if err != nil { - ctx.APIError(http.StatusInternalServerError, err) + ctx.APIErrorInternal(err) } else if !isOwner { ctx.APIError(http.StatusForbidden, "Cannot change member visibility") } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 87e847d6421..16d3e230a01 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -256,7 +256,7 @@ func Create(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateOrgOption) if !ctx.Doer.CanCreateOrganization() { - ctx.APIError(http.StatusForbidden, nil) + ctx.APIError(http.StatusForbidden, "not allowed to create org") return } @@ -282,7 +282,7 @@ func Create(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -355,7 +355,7 @@ func Rename(ctx *context.APIContext) { orgUser := ctx.Org.Organization.AsUser() if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil { if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -394,7 +394,7 @@ func Edit(ctx *context.APIContext) { if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index d8acc767c58..4373b90f2fa 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -236,7 +236,7 @@ func CreateTeam(ctx *context.APIContext) { if err := org_service.NewTeam(ctx, team); err != nil { if organization.IsErrTeamAlreadyExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -490,7 +490,7 @@ func AddTeamMember(ctx *context.APIContext) { } if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 6b01023ef91..d06d2a636b8 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -329,7 +329,7 @@ func GetLatestPackageVersion(ctx *context.APIContext) { return } if len(pvs) == 0 { - ctx.APIError(http.StatusNotFound, err) + ctx.APIErrorNotFound() return } @@ -383,7 +383,7 @@ func LinkPackage(ctx *context.APIContext) { pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -393,7 +393,7 @@ func LinkPackage(ctx *context.APIContext) { repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -404,9 +404,9 @@ func LinkPackage(ctx *context.APIContext) { if err != nil { switch { case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -445,7 +445,7 @@ func UnlinkPackage(ctx *context.APIContext) { pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -456,9 +456,9 @@ func UnlinkPackage(ctx *context.APIContext) { if err != nil { switch { case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) default: ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 169ed4b68ec..5fc2e97d7a3 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -141,9 +141,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -195,9 +195,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -243,7 +243,7 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -298,9 +298,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -361,13 +361,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -422,7 +422,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -439,7 +439,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -957,7 +957,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) { workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1005,7 +1005,7 @@ func ActionsDisableWorkflow(ctx *context.APIContext) { err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1062,7 +1062,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { workflowID := ctx.PathParam("workflow_id") opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch) if opt.Ref == "" { - ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter")) + ctx.APIError(http.StatusUnprocessableEntity, "ref is required parameter") return } @@ -1088,11 +1088,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1151,7 +1151,7 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -1490,13 +1490,13 @@ func RerunWorkflowJob(ctx *context.APIContext) { func handleWorkflowRerunError(ctx *context.APIContext, err error) { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } else if errors.Is(err, util.ErrAlreadyExist) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) @@ -1560,7 +1560,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { // Avoid the list all jobs functionality for this api route to be used with a runID == 0. if runID <= 0 { - ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer")) + ctx.APIError(http.StatusBadRequest, "runID must be a positive integer") return } diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go index b3d52e16634..b00394ffcc5 100644 --- a/routers/api/v1/repo/avatar.go +++ b/routers/api/v1/repo/avatar.go @@ -44,7 +44,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index cd731b0d787..79b2dee245c 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -48,7 +48,7 @@ func GetBlob(ctx *context.APIContext) { } if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.JSON(http.StatusOK, blob) } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 3afdf9694d1..3b6575d6763 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -155,9 +155,9 @@ func DeleteBranch(ctx *context.APIContext) { case git.IsErrBranchNotExist(err): ctx.APIErrorNotFound(err) case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch")) + ctx.APIError(http.StatusForbidden, "can not delete default or pull request target branch") case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.APIError(http.StatusForbidden, errors.New("branch protected")) + ctx.APIError(http.StatusForbidden, "branch protected") default: ctx.APIErrorInternal(err) } @@ -448,7 +448,7 @@ func UpdateBranch(ctx *context.APIContext) { case git_model.IsErrBranchNotExist(err): ctx.APIErrorNotFound(err) case errors.Is(err, util.ErrInvalidArgument): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case git.IsErrPushRejected(err): rej := err.(*git.ErrPushRejected) ctx.APIError(http.StatusForbidden, rej.Message) @@ -684,7 +684,7 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -693,7 +693,7 @@ func CreateBranchProtection(ctx *context.APIContext) { forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -702,7 +702,7 @@ func CreateBranchProtection(ctx *context.APIContext) { mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -711,7 +711,7 @@ func CreateBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -722,7 +722,7 @@ func CreateBranchProtection(ctx *context.APIContext) { bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -734,7 +734,7 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -743,7 +743,7 @@ func CreateBranchProtection(ctx *context.APIContext) { forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -752,7 +752,7 @@ func CreateBranchProtection(ctx *context.APIContext) { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -761,7 +761,7 @@ func CreateBranchProtection(ctx *context.APIContext) { approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -771,7 +771,7 @@ func CreateBranchProtection(ctx *context.APIContext) { bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -999,7 +999,7 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1012,7 +1012,7 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1025,7 +1025,7 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1038,7 +1038,7 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1051,7 +1051,7 @@ func EditBranchProtection(ctx *context.APIContext) { bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1067,7 +1067,7 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1080,7 +1080,7 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1093,7 +1093,7 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1106,7 +1106,7 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1119,7 +1119,7 @@ func EditBranchProtection(ctx *context.APIContext) { bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1331,10 +1331,10 @@ func MergeUpstream(ctx *context.APIContext) { mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index b7938cb516d..e254d5e1289 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -107,7 +107,7 @@ func IsCollaborator(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -167,7 +167,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -186,7 +186,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -230,7 +230,7 @@ func DeleteCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -284,7 +284,7 @@ func GetRepoPermissions(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -326,7 +326,7 @@ func GetReviewers(ctx *context.APIContext) { canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0) if !canChooseReviewer { - ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers")) + ctx.APIError(http.StatusForbidden, "doer has no permission to get reviewers") return } diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 620ee037006..d83686083e5 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -217,7 +217,7 @@ func GetAllCommits(ctx *context.APIContext) { // get commit specified by sha baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return } } @@ -383,7 +383,7 @@ func GetCommitPullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.PathParam("sha")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index d9470d143ea..0df406488c3 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -17,9 +17,9 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -28,7 +28,7 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin err = archiver_service.ServeRepoArchive(ctx.Base, aReq) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 5f0659dc818..244d9393ce6 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -409,7 +409,7 @@ func ChangeFiles(ctx *context.APIContext) { for _, file := range apiOpts.Files { contentReader, err := base64Reader(file.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options @@ -483,7 +483,7 @@ func CreateFile(ctx *context.APIContext) { } contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -554,7 +554,7 @@ func UpdateFile(ctx *context.APIContext) { } contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } willCreate := apiOpts.SHA == "" @@ -584,17 +584,17 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) { return } if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) || files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 9d95f538743..8943ad39931 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -165,9 +165,9 @@ func CreateFork(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 5d73cc3b312..a075c68f749 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -187,7 +187,7 @@ func SearchIssues(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -196,7 +196,7 @@ func SearchIssues(ctx *context.APIContext) { repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -384,7 +384,7 @@ func ListIssues(ctx *context.APIContext) { // "$ref": "#/responses/notFound" before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -682,7 +682,7 @@ func CreateIssue(ctx *context.APIContext) { return } if !valid { - ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}.Error()) return } } @@ -693,9 +693,9 @@ func CreateIssue(ctx *context.APIContext) { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -794,7 +794,7 @@ func EditIssue(ctx *context.APIContext) { // handles concurrent requests. // TODO: wrap all mutations in a transaction to fully prevent partial writes. if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { - ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error()) return } @@ -813,7 +813,7 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -869,7 +869,7 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -918,7 +918,7 @@ func EditIssue(ctx *context.APIContext) { if canWrite && form.Projects != nil { if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil { if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index 00fa792771f..123f66399d4 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -194,9 +194,9 @@ func CreateIssueAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -272,7 +272,7 @@ func EditIssueAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -336,7 +336,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) { func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } @@ -361,7 +361,7 @@ func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) { diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 5b2d2084735..02a0f702ce9 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -65,7 +65,7 @@ func ListIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) @@ -169,7 +169,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) @@ -274,7 +274,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -392,14 +392,14 @@ func CreateIssueComment(ctx *context.APIContext) { } if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) + ctx.APIError(http.StatusForbidden, ctx.Locale.TrString("repo.issues.comment_on_locked")) return } comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -595,7 +595,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) comment.Content = form.Body if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index d2650edb634..56d494190e4 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -202,9 +202,9 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -218,7 +218,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -285,7 +285,7 @@ func EditIssueCommentAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -346,7 +346,7 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if err := comment.LoadIssue(ctx); err != nil { @@ -391,7 +391,7 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError(err) + ctx.APIErrorAuto(err) return nil } if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) { diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index ecf42688615..c1bbbe29d00 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -181,7 +181,7 @@ func DeleteIssueLabel(ctx *context.APIContext) { label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrLabelNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_lock.go b/routers/api/v1/repo/issue_lock.go index 283b441fd20..75247593fd3 100644 --- a/routers/api/v1/repo/issue_lock.go +++ b/routers/api/v1/repo/issue_lock.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" issues_model "gitea.dev/models/issues" @@ -63,7 +62,7 @@ func LockIssue(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue")) + ctx.APIError(http.StatusForbidden, "no permission to lock this issue") return } @@ -130,7 +129,7 @@ func UnlockIssue(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue")) + ctx.APIError(http.StatusForbidden, "no permission to unlock this issue") return } diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go index b8cebfbb04e..c71649aeca4 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -46,7 +46,7 @@ func PinIssue(ctx *context.APIContext) { if issues_model.IsErrIssueNotExist(err) { ctx.APIErrorNotFound() } else if issues_model.IsErrIssueMaxPinReached(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index 3662e63c71b..c9fa39e93d2 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -72,7 +72,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, "no permission to get reactions") return } @@ -214,7 +214,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp } if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, "no permission to change reaction") return } @@ -223,7 +223,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), @@ -305,7 +305,7 @@ func GetIssueReactions(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, "no permission to get reactions") return } @@ -429,7 +429,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i } if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, "no permission to change reaction") return } @@ -438,7 +438,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index b2480e88a5a..84af3194df2 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -128,7 +128,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { // only admin and user for itself can change subscription if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) return } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 414b44f5246..1af649bfd79 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" "time" @@ -95,7 +94,7 @@ func ListTrackedTimes(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if err != nil { ctx.APIErrorInternal(err) return @@ -104,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) { } if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -116,7 +115,7 @@ func ListTrackedTimes(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } } @@ -286,7 +285,7 @@ func ResetIssueTime(ctx *context.APIContext) { err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer) if err != nil { if db.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -437,7 +436,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { } if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } @@ -523,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else if err != nil { ctx.APIErrorInternal(err) return @@ -533,7 +532,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } @@ -545,7 +544,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights") return } } @@ -607,7 +606,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 44471a4f73f..b704bcee1d4 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -179,7 +179,7 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) { } else if asymkey_model.IsErrKeyUnableVerify(err) { ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content") } else { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid key content: %v", err)) } } diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index aec82dce757..790a9212bc2 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -153,7 +153,7 @@ func CreateLabel(ctx *context.APIContext) { color, err := label.NormalizeColor(form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } form.Color = color @@ -231,7 +231,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } l.Color = color diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 65ef10777c4..0e3e68d1e8b 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -72,7 +72,7 @@ func Migrate(ctx *context.APIContext) { } if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -110,12 +110,12 @@ func Migrate(ctx *context.APIContext) { gitServiceType := convert.ToGitServiceType(form.Service) if form.Mirror && setting.Mirror.DisableNewPull { - ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors")) + ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new pull mirrors") return } if setting.Repository.DisableMigrations { - ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations")) + ctx.APIError(http.StatusForbidden, "the site administrator has disabled migrations") return } @@ -235,9 +235,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err case db.IsErrNamePatternNotAllowed(err): ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) case git.IsErrInvalidCloneAddr(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case base.IsErrNotSupported(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) default: err = util.SanitizeErrorCredentialURLs(err) if strings.Contains(err.Error(), "Authentication failed") || diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index 53d47ff135e..c76946493ab 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -5,6 +5,7 @@ package repo import ( "errors" + "fmt" "net/http" "strings" "time" @@ -112,7 +113,7 @@ func PushMirrorSync(ctx *context.APIContext) { // Get All push mirrors of a specific repo pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } @@ -175,7 +176,7 @@ func ListPushMirrors(ctx *context.APIContext) { // Get all push mirrors for the specified repository. pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } @@ -239,7 +240,7 @@ func GetPushMirrorByName(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if !exist { - ctx.APIError(http.StatusNotFound, nil) + ctx.APIErrorNotFound() return } @@ -334,7 +335,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return } ctx.Status(http.StatusNoContent) @@ -344,8 +345,12 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro repo := ctx.Repo.Repository interval, err := time.ParseDuration(mirrorOption.Interval) - if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.APIError(http.StatusBadRequest, err) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid interval: %v", err)) + return + } + if interval != 0 && interval < setting.Mirror.MinInterval { + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("interval is shorter than minimum %v", setting.Mirror.MinInterval.String())) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 6f6ba77fae0..54e7b78a1b4 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -126,7 +126,7 @@ func ListPullRequests(ctx *context.APIContext) { poster, err := user_model.GetUserByName(ctx, posterStr) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -448,7 +448,7 @@ func CreatePullRequest(ctx *context.APIContext) { HeadBranch: existingPr.HeadBranch, BaseBranch: existingPr.BaseBranch, } - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -551,7 +551,7 @@ func CreatePullRequest(ctx *context.APIContext) { return } if !valid { - ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error()) return } } @@ -570,11 +570,11 @@ func CreatePullRequest(ctx *context.APIContext) { if err := pull_service.NewPullRequest(ctx, prOpts); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else if errors.Is(err, issues_model.ErrMustCollaborator) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -663,7 +663,7 @@ func EditPullRequest(ctx *context.APIContext) { // handles concurrent requests. // TODO: wrap all mutations in a transaction to fully prevent partial writes. if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion { - ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged) + ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error()) return } @@ -682,7 +682,7 @@ func EditPullRequest(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } @@ -721,7 +721,7 @@ func EditPullRequest(ctx *context.APIContext) { if user_model.IsErrUserNotExist(err) { ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err)) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -788,18 +788,18 @@ func EditPullRequest(ctx *context.APIContext) { return } if !branchExist { - ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base)) + ctx.APIError(http.StatusNotFound, fmt.Sprintf("new base '%s' not exist", form.Base)) return } if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil { if issues_model.IsErrPullRequestAlreadyExists(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } else if issues_model.IsErrIssueIsClosed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } else if pull_service.IsErrPullRequestHasMerged(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -977,11 +977,11 @@ func MergePullRequest(ctx *context.APIContext) { } else if errors.Is(err, pull_service.ErrNotMergeableState) { ctx.APIError(http.StatusMethodNotAllowed, "Please try again later") } else if errors.Is(err, pull_service.ErrNotReadyToMerge) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else if asymkey_service.IsErrWontSign(err) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) { - ctx.APIError(http.StatusMethodNotAllowed, err) + ctx.APIError(http.StatusMethodNotAllowed, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -992,11 +992,11 @@ func MergePullRequest(ctx *context.APIContext) { if manuallyMerged { if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) return } if strings.Contains(err.Error(), "Wrong commit ID") { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -1034,7 +1034,7 @@ func MergePullRequest(ctx *context.APIContext) { scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } ctx.APIErrorInternal(err) @@ -1048,7 +1048,7 @@ func MergePullRequest(ctx *context.APIContext) { if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if pull_service.IsErrMergeConflicts(err) { conflictError := err.(pull_service.ErrMergeConflicts) ctx.JSON(http.StatusConflict, conflictError) @@ -1230,7 +1230,7 @@ func UpdatePullRequest(ctx *context.APIContext) { } if pr.HasMerged { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "pull request is already merged") return } @@ -1240,7 +1240,7 @@ func UpdatePullRequest(ctx *context.APIContext) { } if pr.Issue.IsClosed { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, "pull request is already closed") return } diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 901e6b69c08..54919cef90f 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -458,7 +458,7 @@ func DeletePullReview(ctx *context.APIContext) { return } if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID { - ctx.APIError(http.StatusForbidden, nil) + ctx.APIError(http.StatusForbidden, "no permission to delete comment") return } @@ -575,7 +575,7 @@ func CreatePullReview(ctx *context.APIContext) { review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -641,7 +641,7 @@ func SubmitPullReview(ctx *context.APIContext) { } if review.Type != issues_model.ReviewTypePending { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("only a pending review can be submitted")) + ctx.APIError(http.StatusUnprocessableEntity, "only a pending review can be submitted") return } @@ -653,7 +653,7 @@ func SubmitPullReview(ctx *context.APIContext) { // if review stay pending return if reviewType == issues_model.ReviewTypePending { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("review stay pending")) + ctx.APIError(http.StatusUnprocessableEntity, "review stay pending") return } @@ -667,7 +667,7 @@ func SubmitPullReview(ctx *context.APIContext) { review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -698,7 +698,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateApproved: // can not approve your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("approve your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed") return -1, true } reviewType = issues_model.ReviewTypeApprove @@ -707,7 +707,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateRequestChanges: // can not reject your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("reject your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed") return -1, true } reviewType = issues_model.ReviewTypeReject @@ -717,7 +717,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest needsBody = false // if there is no body we need to ensure that there are comments if !hasBody && !hasComments { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body or a comment", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body or a comment", event)) return -1, true } default: @@ -726,7 +726,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // reject reviews with empty body if a body is required for this call if needsBody && !hasBody { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body", event)) return -1, true } @@ -935,11 +935,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -960,11 +960,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -1102,7 +1102,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors) if err != nil { if pull_service.IsErrDismissRequestOnClosedPR(err) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index ee995f4402b..a4fc03ae321 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -4,8 +4,6 @@ package repo import ( - "errors" - "fmt" "net/http" auth_model "gitea.dev/models/auth" @@ -243,7 +241,7 @@ func CreateRelease(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateReleaseOption) if ctx.Repo.Repository.IsEmpty { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty")) + ctx.APIError(http.StatusUnprocessableEntity, "repo is empty") return } rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) @@ -273,11 +271,11 @@ func CreateRelease(ctx *context.APIContext) { // It doesn't need to be the same as the "release note" if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) } else if release_service.IsErrProtectedTagName(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err)) + ctx.APIError(http.StatusNotFound, "target not found") } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 896c6d24ccb..715552d72c7 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -250,12 +250,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } if errors.Is(err, util.ErrContentTooLarge) { - ctx.APIError(http.StatusRequestEntityTooLarge, err) + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) return } @@ -340,7 +340,7 @@ func EditReleaseAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index e0e40086a56..8adef610001 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -173,7 +173,7 @@ func Search(ctx *context.APIContext) { opts.Collaborate = optional.Some(true) case "": default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode)) + ctx.APIError(http.StatusUnprocessableEntity, "invalid search mode") return } @@ -234,7 +234,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre // If the readme template does not exist, a 400 will be returned. if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("readme template does not exist, available templates: %v", repo_module.Readmes)) return } @@ -258,9 +258,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || label.IsErrTemplateLoad(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else if errors.Is(err, util.ErrPermissionDenied) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -413,7 +413,7 @@ func Generate(ctx *context.APIContext) { ctx.APIError(http.StatusConflict, "The repository with the same name already exists.") } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -652,13 +652,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case db.IsErrNameReserved(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case db.IsErrNamePatternNotAllowed(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("ChangeRepositoryName: %v", err)) } return err } @@ -692,7 +692,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin { err := errors.New("cannot change private repository to public") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -756,12 +756,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { // Check that values are valid if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) { err := errors.New("External tracker URL not valid") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { err := errors.New("External tracker URL format not valid") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -897,7 +897,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { // so unrelated PATCH calls don't reject historical configs. if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil { if err := config.ValidateUpdateSettings(); err != nil { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } } @@ -988,7 +988,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e if opts.Archived != nil { if repo.IsMirror { err := errors.New("repo is a mirror, cannot archive/un-archive") - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } if *opts.Archived { @@ -1042,14 +1042,14 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { interval, err := time.ParseDuration(*opts.MirrorInterval) if err != nil { log.Error("Wrong format for MirrorInternal Sent: %s", err) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } // Ensure the provided duration is not too short if interval != 0 && interval < setting.Mirror.MinInterval { err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } @@ -1119,7 +1119,7 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { // finally update the mirror in the DB if err := repo_model.UpdateMirror(ctx, mirror); err != nil { log.Error("Failed to Set Mirror Interval: %s", err) - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return err } diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 9df2a32e72d..c7d7014e541 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -55,7 +55,7 @@ func NewCommitStatus(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateStatusOption) sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.APIError(http.StatusBadRequest, nil) + ctx.APIError(http.StatusBadRequest, "sha not provided") return } status := &git_model.CommitStatus{ diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 796bdb10883..d43d5ea628b 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -109,13 +109,13 @@ func GetAnnotatedTag(ctx *context.APIContext) { tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) @@ -203,13 +203,13 @@ func CreateTag(ctx *context.APIContext) { commit, err := ctx.Repo.GitRepo.GetCommit(form.Target) if err != nil { - ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err)) + ctx.APIError(http.StatusNotFound, fmt.Sprintf("target not found: %v", err)) return } if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil { if release_service.IsErrTagAlreadyExists(err) { - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) return } if release_service.IsErrProtectedTagName(err) { @@ -278,7 +278,7 @@ func DeleteTag(ctx *context.APIContext) { } if !tag.IsTag { - ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly")) + ctx.APIError(http.StatusConflict, "a tag attached to a release cannot be deleted directly") return } @@ -438,7 +438,7 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -449,7 +449,7 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -546,7 +546,7 @@ func EditTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) @@ -560,7 +560,7 @@ func EditTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) return } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 1913bb6eecd..fc39b1c6166 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -201,13 +201,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { var err error if add { if repoHasTeam { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' is already added to repo", team.Name)) return } err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository) } else { if !repoHasTeam { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' was not added to repo", team.Name)) return } err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID) @@ -224,7 +224,7 @@ func getTeamByParam(ctx *context.APIContext) *organization.Team { team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) return nil } ctx.APIErrorInternal(err) diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index e2c124a86a5..63fc3b0712c 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -87,12 +87,12 @@ func Transfer(ctx *context.APIContext) { for _, tID := range *opts.TeamIDs { team, err := organization.GetTeamByID(ctx, tID) if err != nil { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team %d not found", tID)) return } if team.OrgID != org.ID { - ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("team %d belongs not to org %d", tID, org.ID)) return } @@ -110,13 +110,13 @@ func Transfer(ctx *context.APIContext) { if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { switch { case repo_model.IsErrRepoTransferInProgress(err): - ctx.APIError(http.StatusConflict, err) + ctx.APIError(http.StatusConflict, err.Error()) case repo_model.IsErrRepoAlreadyExist(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case repo_service.IsRepositoryLimitReached(err): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case errors.Is(err, user_model.ErrBlockedUser): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -163,11 +163,11 @@ func AcceptTransfer(ctx *context.APIContext) { if err != nil { switch { case repo_model.IsErrNoPendingTransfer(err): - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) case repo_service.IsRepositoryLimitReached(err): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } @@ -207,9 +207,9 @@ func RejectTransfer(ctx *context.APIContext) { if err != nil { switch { case repo_model.IsErrNoPendingTransfer(err): - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) case errors.Is(err, util.ErrPermissionDenied): - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) default: ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 16471044820..dad0bfbbce0 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -59,7 +59,7 @@ func NewWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) if util.IsEmptyString(form.Title) { - ctx.APIError(http.StatusBadRequest, nil) + ctx.APIError(http.StatusBadRequest, "title is required") return } @@ -71,16 +71,16 @@ func NewWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } form.ContentBase64 = string(content) if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil { if repo_model.IsErrWikiReservedName(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if repo_model.IsErrWikiAlreadyExist(err) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -149,7 +149,7 @@ func EditWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } form.ContentBase64 = string(content) diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go index 3dbe7008bec..5c95fa192c0 100644 --- a/routers/api/v1/shared/action.go +++ b/routers/api/v1/shared/action.go @@ -16,6 +16,7 @@ import ( "gitea.dev/modules/optional" "gitea.dev/modules/setting" api "gitea.dev/modules/structs" + "gitea.dev/modules/util" "gitea.dev/modules/webhook" "gitea.dev/routers/api/v1/utils" "gitea.dev/services/context" @@ -53,7 +54,7 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + ctx.APIError(http.StatusBadRequest, err.Error()) return } opts.Statuses = append(opts.Statuses, values...) @@ -125,7 +126,7 @@ func convertToInternal(s string) ([]actions_model.Status, error) { case "cancelled", "timed_out": return []actions_model.Status{actions_model.StatusCancelled}, nil default: - return nil, fmt.Errorf("invalid status %s", s) + return nil, util.NewInvalidArgumentErrorf("invalid status %s", s) } } @@ -155,7 +156,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { for _, status := range ctx.FormStrings("status") { values, err := convertToInternal(status) if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + ctx.APIError(http.StatusBadRequest, err.Error()) return } opts.Status = append(opts.Status, values...) diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index e8190041aa5..8b2a207ccb2 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -68,7 +68,7 @@ func BlockUser(ctx *context.APIContext, blocker *user_model.User) { if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil { if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -87,7 +87,7 @@ func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil { if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index f5654908f0d..1086b221924 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -53,9 +53,9 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -95,9 +95,9 @@ func DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -148,13 +148,13 @@ func CreateVariable(ctx *context.APIContext) { return } if v != nil && v.ID > 0 { - ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, "variable name already exists") return } if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -201,7 +201,7 @@ func UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -218,7 +218,7 @@ func UpdateVariable(ctx *context.APIContext) { if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -253,9 +253,9 @@ func DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) } else if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } @@ -292,7 +292,7 @@ func GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 729c408a151..a410909e0e5 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -5,7 +5,6 @@ package user import ( - "errors" "fmt" "net/http" "strconv" @@ -112,13 +111,13 @@ func CreateAccessToken(ctx *context.APIContext) { return } if exist { - ctx.APIError(http.StatusBadRequest, errors.New("access token name has been used already")) + ctx.APIError(http.StatusBadRequest, "access token name has been used already") return } scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() if err != nil { - ctx.APIError(http.StatusBadRequest, fmt.Errorf("invalid access token scope provided: %w", err)) + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid access token scope provided: %v", err)) return } if scope == "" { @@ -188,7 +187,7 @@ func DeleteAccessToken(ctx *context.APIContext) { case 1: tokenID = tokens[0].ID default: - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("multiple matches for token name '%s'", token)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("multiple matches for token name '%s'", token)) return } } diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go index d02ca87fb27..428ac0f62b7 100644 --- a/routers/api/v1/user/avatar.go +++ b/routers/api/v1/user/avatar.go @@ -32,7 +32,7 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.APIError(http.StatusBadRequest, err) + ctx.APIError(http.StatusBadRequest, err.Error()) return } diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 71b5a691c15..d3329f7a743 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -122,7 +122,7 @@ func DeleteEmail(ctx *context.APIContext) { if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAddressNotExist(err) { - ctx.APIError(http.StatusNotFound, err) + ctx.APIError(http.StatusNotFound, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 11f659c85a4..47eabb5351d 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -233,7 +233,7 @@ func Follow(ctx *context.APIContext) { if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index bae4d7420f1..562e70b5c0b 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -294,7 +294,7 @@ func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err): ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists") case asymkey_model.IsErrGPGKeyParsing(err): - ctx.APIError(http.StatusUnprocessableEntity, err) + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case asymkey_model.IsErrGPGNoEmailFound(err): ctx.APIError(http.StatusNotFound, "None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: "+token) case asymkey_model.IsErrGPGInvalidTokenSignature(err): diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 7036a4f854b..c78b6872c5a 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -174,7 +174,7 @@ func Star(ctx *context.APIContext) { err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 94d3de84b8a..2d1f55d8709 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -172,7 +172,7 @@ func Watch(ctx *context.APIContext) { err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.APIError(http.StatusForbidden, err) + ctx.APIError(http.StatusForbidden, err.Error()) } else { ctx.APIErrorInternal(err) } diff --git a/routers/api/v1/utils/sort.go b/routers/api/v1/utils/sort.go index b5f819cdb6e..669a09988ad 100644 --- a/routers/api/v1/utils/sort.go +++ b/routers/api/v1/utils/sort.go @@ -26,12 +26,12 @@ func ResolveSortOrder(ctx *context.APIContext, orderByMap map[string]map[string] } orderMap, ok := orderByMap[sortOrder] if !ok { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: %q", sortOrder)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: %q", sortOrder)) return "", false } orderBy, ok := orderMap[sortMode] if !ok { - ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: %q", sortMode)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: %q", sortMode)) return "", false } return orderBy, true diff --git a/services/context/api.go b/services/context/api.go index 05b6490826a..7731b5692fc 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -137,16 +137,34 @@ func (ctx *APIContext) apiErrorInternal(skip int, err error) { }) } -// APIError responds with an error message to client with given obj as the message. -// If status is 500, also it prints error to log. -func (ctx *APIContext) APIError(status int, obj any) { +// APIErrorNotFound handles 404s for APIContext +// String will replace message, errors will be added to a slice +func (ctx *APIContext) APIErrorNotFound(objs ...any) { var message string - if err, ok := obj.(error); ok { - message = err.Error() - } else { - message = fmt.Sprintf("%s", obj) - } + var errs []string + for _, obj := range objs { + // Ignore nil + if obj == nil { + continue + } + if err, ok := obj.(error); ok { + errs = append(errs, err.Error()) + } else { + message = obj.(string) + } + } + ctx.JSON(http.StatusNotFound, map[string]any{ + "message": util.IfZero(message, "not found"), // do not use locale in API + "url": setting.API.SwaggerURL, + "errors": errs, + }) +} + +// APIError responds with an error message to client. +// If status is 500, also it prints error to log. +func (ctx *APIContext) APIError(status int, msg string) { + message := msg if status == http.StatusInternalServerError { log.ErrorWithSkip(1, "APIError: %s", message) @@ -161,6 +179,26 @@ func (ctx *APIContext) APIError(status int, obj any) { }) } +// APIErrorAuto use error check function to determine the response code +func (ctx *APIContext) APIErrorAuto(err error) { + switch { + case errors.Is(err, util.ErrInvalidArgument): + ctx.APIError(http.StatusBadRequest, err.Error()) + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err.Error()) + case errors.Is(err, util.ErrNotExist): + ctx.APIError(http.StatusNotFound, err.Error()) + case errors.Is(err, util.ErrAlreadyExist): + ctx.APIError(http.StatusConflict, err.Error()) + case errors.Is(err, util.ErrContentTooLarge): + ctx.APIError(http.StatusRequestEntityTooLarge, err.Error()) + case errors.Is(err, util.ErrUnprocessableContent): + ctx.APIError(http.StatusUnprocessableEntity, err.Error()) + default: + ctx.apiErrorInternal(1, err) + } +} + type apiContextKeyType struct{} var apiContextKey = apiContextKeyType{} @@ -248,30 +286,6 @@ func APIContexter() func(http.Handler) http.Handler { } } -// APIErrorNotFound handles 404s for APIContext -// String will replace message, errors will be added to a slice -func (ctx *APIContext) APIErrorNotFound(objs ...any) { - var message string - var errs []string - for _, obj := range objs { - // Ignore nil - if obj == nil { - continue - } - - if err, ok := obj.(error); ok { - errs = append(errs, err.Error()) - } else { - message = obj.(string) - } - } - ctx.JSON(http.StatusNotFound, map[string]any{ - "message": util.IfZero(message, "not found"), // do not use locale in API - "url": setting.API.SwaggerURL, - "errors": errs, - }) -} - // ReferencesGitRepo injects the GitRepo into the Context // you can optional skip the IsEmpty check func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) { @@ -329,17 +343,6 @@ func RepoRefForAPI(next http.Handler) http.Handler { }) } -// NotFoundOrServerError use error check function to determine if the error -// is about not found. It responds with 404 status code for not found error, -// or error context description for logging purpose of 500 server error. -func (ctx *APIContext) NotFoundOrServerError(err error) { - if errors.Is(err, util.ErrNotExist) { - ctx.JSON(http.StatusNotFound, nil) - return - } - ctx.APIErrorInternal(err) -} - // IsUserSiteAdmin returns true if current user is a site admin func (ctx *APIContext) IsUserSiteAdmin() bool { return ctx.IsSigned && ctx.Doer.IsAdmin diff --git a/services/context/package.go b/services/context/package.go index 4918e124913..7c1e55c8a34 100644 --- a/services/context/package.go +++ b/services/context/package.go @@ -34,11 +34,8 @@ type packageAssignmentCtx struct { // PackageAssignment returns a middleware to handle Context.Package assignment func PackageAssignment() func(ctx *Context) { return func(ctx *Context) { - errorFn := func(status int, obj any) { - err, ok := obj.(error) - if !ok { - err = fmt.Errorf("%s", obj) - } + errorFn := func(status int, msg string) { + err := fmt.Errorf("%s", msg) if status == http.StatusNotFound { ctx.NotFound(err) } else { @@ -58,11 +55,11 @@ func PackageAssignmentAPI() func(ctx *APIContext) { } } -func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package { +func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, string)) *Package { pkgOwner := ctx.ContextUser accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer) if err != nil { - errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("determineAccessMode: %v", err)) return nil } @@ -81,25 +78,25 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) { - errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + errCb(http.StatusNotFound, fmt.Sprintf("GetVersionByNameAndVersion: %v", err)) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetVersionByNameAndVersion: %v", err)) } return pkg } pkg.Descriptor, err = packages_model.GetPackageDescriptor(ctx, pv) if err != nil { - errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageDescriptor: %v", err)) return pkg } } else { p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) { - errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err)) + errCb(http.StatusNotFound, fmt.Sprintf("GetPackageByName: %v", err)) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetPackageByName: %v", err)) } return pkg } diff --git a/services/context/user.go b/services/context/user.go index ad0876ebf00..d335b9738a5 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -14,11 +14,8 @@ import ( // UserAssignmentWeb returns a middleware to handle context-user assignment for web routes func UserAssignmentWeb() func(ctx *Context) { return func(ctx *Context) { - errorFn := func(status int, obj any) { - err, ok := obj.(error) - if !ok { - err = fmt.Errorf("%s", obj) - } + errorFn := func(status int, msg string) { + err := fmt.Errorf("%s", msg) if status == http.StatusNotFound { ctx.NotFound(err) } else { @@ -37,7 +34,7 @@ func UserAssignmentAPI() func(ctx *APIContext) { } } -func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (contextUser *user_model.User) { +func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string)) (contextUser *user_model.User) { username := ctx.PathParam("username") if doer != nil && strings.EqualFold(doer.LowerName, username) { @@ -50,12 +47,12 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil { RedirectToUser(ctx, doer, username, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - errCb(http.StatusNotFound, err) + errCb(http.StatusNotFound, err.Error()) } else { - errCb(http.StatusInternalServerError, fmt.Errorf("LookupUserRedirect: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("LookupUserRedirect: %v", err)) } } else { - errCb(http.StatusInternalServerError, fmt.Errorf("GetUserByName: %w", err)) + errCb(http.StatusInternalServerError, fmt.Sprintf("GetUserByName: %v", err)) } } } From 9bbea90bfe1aedec1433b28a06649b33d48e88a7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 7 Jun 2026 18:28:17 +0800 Subject: [PATCH 26/88] fix: pgsql lint (#38022) --- models/db/driver_postgresschema.go | 65 +++++++++--------------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/models/db/driver_postgresschema.go b/models/db/driver_postgresschema.go index ad2b5abc04a..616712fb2a0 100644 --- a/models/db/driver_postgresschema.go +++ b/models/db/driver_postgresschema.go @@ -4,8 +4,10 @@ package db import ( + "context" "database/sql" "database/sql/driver" + "errors" "sync" "gitea.dev/modules/setting" @@ -14,61 +16,34 @@ import ( "xorm.io/xorm/dialects" ) -var registerOnce sync.Once +type postgresSchemaDriver struct{} -func registerPostgresSchemaDriver() { - registerOnce.Do(func() { - sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) - dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) - }) -} +var registerPostgresSchemaDriver = sync.OnceFunc(func() { + sql.Register(sqlDriverPostgresSchema, &postgresSchemaDriver{}) + dialects.RegisterDriver(sqlDriverPostgresSchema, dialects.QueryDriver("postgres")) +}) -type postgresSchemaDriver struct { - pq.Driver -} - -// Open opens a new connection to the database. name is a connection string. -// This function opens the postgres connection in the default manner but immediately -// runs set_config to set the search_path appropriately -func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { - conn, err := d.Driver.Open(name) +// Open opens the postgres connection in the default manner with default schema support. +// It immediately runs "set_config" to set the search_path appropriately. +func (*postgresSchemaDriver) Open(connStr string) (driver.Conn, error) { + conn, err := pq.Driver{}.Open(connStr) if err != nil { - return conn, err + return nil, err } - schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema) - // golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here - // and in any case pq does not implement it - if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above - _, err := execer.Exec(`SELECT set_config( + connExec, ok := conn.(driver.ExecerContext) + if !ok { + return nil, errors.New("postgres driver does not implement ExecerContext interface") + } + _, err = connExec.ExecContext(context.Background(), `SELECT set_config( 'search_path', $1 || ',' || current_setting('search_path'), - false)`, []driver.Value{schemaValue}) - if err != nil { - _ = conn.Close() - return nil, err - } - return conn, nil - } - - stmt, err := conn.Prepare(`SELECT set_config( - 'search_path', - $1 || ',' || current_setting('search_path'), - false)`) + false)`, + []driver.NamedValue{{Ordinal: 1, Value: setting.Database.Schema}}, + ) if err != nil { _ = conn.Close() return nil, err } - defer stmt.Close() - - // driver.String.ConvertValue will never return err for string - - // golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here - _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above - if err != nil { - _ = conn.Close() - return nil, err - } - return conn, nil } From e2fbfc8730f589235932d8d694db5d6e07ae091b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 7 Jun 2026 18:33:16 +0800 Subject: [PATCH 27/88] fix: various dropdown problems (#38020) 1. remove legacy onResponseKeepSelectedItem, refactor the code to dropdown.js 2. make dropdown correctly handle "single selection + remote query + filter" * fix #38018 3. fix incorrect "transition" class usage for the dropdown dividers --- web_src/css/modules/dropdown.css | 4 ++++ web_src/fomantic/build/components/dropdown.js | 20 ++++++++++-------- web_src/js/features/repo-new.ts | 1 - web_src/js/modules/fomantic/dropdown.test.ts | 16 +++++++------- web_src/js/modules/fomantic/dropdown.ts | 21 ++----------------- 5 files changed, 25 insertions(+), 37 deletions(-) diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css index 9d3dc48adae..02957fe31d9 100644 --- a/web_src/css/modules/dropdown.css +++ b/web_src/css/modules/dropdown.css @@ -80,6 +80,10 @@ border-top-width: 0; } +.ui.dropdown .menu .hidden { /* for hidden items and dividers */ + display: none; +} + .ui.dropdown .menu > .header { margin: 1rem 0 0.75rem; padding: 0 1.14285714rem; diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index b8f066db748..0faed1858c4 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -308,12 +308,12 @@ $.fn.dropdown = function(parameters) { firstUnfiltered: function() { module.verbose('Selecting first non-filtered element'); module.remove.selectedItem(); - $item + const $selectable = $item .not(selector.unselectable) - .not(selector.addition + selector.hidden) - .eq(0) - .addClass(className.selected) - ; + .not(selector.addition + selector.hidden); + let $selectedItem = $selectable.filter(`[data-value="${CSS.escape($input.val())}"]`); // GITEA-PATCH: try to re-select the last selected item for single selection + if (!$selectedItem.length) $selectedItem = $item.eq(0); + $selectedItem.addClass(className.selected); }, nextAvailable: function($selected) { $selected = $selected.eq(0); @@ -772,11 +772,13 @@ $.fn.dropdown = function(parameters) { if(!Array.isArray(preSelected)) { preSelected = preSelected && preSelected!=="" ? preSelected.split(settings.delimiter) : []; } - $.each(preSelected,function(index,value){ - $item.filter('[data-value="'+CSS.escape(value)+'"]') // GITEA-PATCH: use "CSS.escape" for query selector + if (module.is.multiple()) { // GITEA-PATCH: only hide selected items when the dropdown is "multiple selection" + $.each(preSelected, function (index, value) { + $item.filter('[data-value="' + CSS.escape(value) + '"]') // GITEA-PATCH: use "CSS.escape" for query selector .addClass(className.filtered) - ; - }); + ; + }); + } afterFiltered(); }); } diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index b690ace0f7f..8b1188f19f1 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -47,7 +47,6 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { value: String(tmplRepo.repository.id), }); } - $repoTemplateDropdown.fomanticExt.onResponseKeepSelectedItem($repoTemplateDropdown, inputRepoTemplate.value); return {results}; }, cache: false, diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index dd3497c8fce..542cb854b84 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -13,13 +13,13 @@ test('hideScopedEmptyDividers-simple', () => {
    `); hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(` - +
    a
    - - + +
    b
    - + `); }); @@ -35,7 +35,7 @@ test('hideScopedEmptyDividers-items-all-filtered', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    - +
    a
    b
    @@ -52,7 +52,7 @@ test('hideScopedEmptyDividers-hide-last', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    a
    - +
    b
    `); }); @@ -68,9 +68,9 @@ test('hideScopedEmptyDividers-scoped-items', () => { hideScopedEmptyDividers(container); expect(container.innerHTML).toEqual(`
    a
    - +
    b
    - +
    c
    `); }); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index e49c410682f..a12b56d23d9 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -8,7 +8,6 @@ const fomanticDropdownFn = $.fn.dropdown; export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); $.fn.dropdown = ariaDropdownFn; - $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } @@ -296,8 +295,8 @@ export function hideScopedEmptyDividers(container: Element) { let curScope: string = '', lastVisibleScope: string = ''; const isDivider = (item: Element) => item.classList.contains('divider'); const isScopedDivider = (item: Element) => isDivider(item) && item.hasAttribute('data-scope'); - const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items - const showDivider = (item: Element) => item.classList.remove('hidden', 'transition'); + const hideDivider = (item: Element) => item.classList.add('hidden'); // dropdown has its own classes to hide items + const showDivider = (item: Element) => item.classList.remove('hidden'); const isHidden = (item: Element) => item.classList.contains('hidden') || item.classList.contains('filtered') || item.classList.contains('tw-hidden'); const handleScopeSwitch = (itemScope: string) => { if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { @@ -347,19 +346,3 @@ export function hideScopedEmptyDividers(container: Element) { if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]); } } - -function onResponseKeepSelectedItem(dropdown: typeof $ | HTMLElement, selectedValue: string) { - // There is a bug in fomantic dropdown when using "apiSettings" to fetch data - // * when there is a selected item, the dropdown insists on hiding the selected one from the list: - // * in the "filter" function: ('[data-value="'+value+'"]').addClass(className.filtered) - // - // When user selects one item, and click the dropdown again, - // then the dropdown only shows other items and will select another (wrong) one. - // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)` - // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)` - const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : (dropdown as any)[0]; - setTimeout(() => { - queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); - $(elDropdown).dropdown('set selected', selectedValue ?? ''); - }, 10); -} From ea35af1b68d57522c7686618bd61d3216d91589f Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 7 Jun 2026 17:30:18 +0200 Subject: [PATCH 28/88] fix: bound CODEOWNERS regex match time (#38011) User-supplied CODEOWNERS patterns were compiled without a match timeout, so a crafted pattern (e.g. (a+)+) against a crafted file path could backtrack for tens of seconds inside the PR creation transaction and exhaust the database connection pool. Set MatchTimeout on each compiled rule; the caller already treats match errors as non-matches. --------- Signed-off-by: wxiaoguang Co-authored-by: wxiaoguang --- models/issues/pull.go | 8 ++++++++ models/issues/pull_test.go | 19 +++++++++++++++++++ services/issue/pull.go | 14 ++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/models/issues/pull.go b/models/issues/pull.go index 7dbcef0d3fe..2b93b926b01 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "strings" + "time" "gitea.dev/models/db" git_model "gitea.dev/models/git" @@ -860,6 +861,11 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul return rules, warnings } +// codeOwnerMatchTimeout bounds a single pattern match so a crafted pattern +// cannot stall via catastrophic backtracking. See also the aggregate budget +// enforced by the caller across the whole rules×files match loop. +const codeOwnerMatchTimeout = 150 * time.Millisecond + type CodeOwnerRule struct { Rule *regexp2.Regexp // it supports negative lookahead, does better for end users Negative bool @@ -888,6 +894,8 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) return nil, warnings } + // Bound matching time so user-supplied patterns cannot stall PR creation via catastrophic backtracking. + rule.Rule.MatchTimeout = codeOwnerMatchTimeout for _, user := range tokens[1:] { user = strings.TrimPrefix(user, "@") diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 166fdd8482b..bd7499fa80b 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -4,7 +4,9 @@ package issues_test import ( + "strings" "testing" + "time" "gitea.dev/models/db" issues_model "gitea.dev/models/issues" @@ -39,6 +41,7 @@ func TestPullRequest(t *testing.T) { t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects) t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine) t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns) + t.Run("CodeOwnerPatternMatchTimeout", testCodeOwnerPatternMatchTimeout) t.Run("GetApprovers", testGetApprovers) t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit) t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests) @@ -376,6 +379,22 @@ func testCodeOwnerAbsolutePathPatterns(t *testing.T) { } } +// testCodeOwnerPatternMatchTimeout ensures user-supplied CODEOWNERS patterns +// cannot stall pull request processing through catastrophic regex backtracking: +// each compiled rule must enforce a bounded match time. +func testCodeOwnerPatternMatchTimeout(t *testing.T) { + rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), "(a+)+ @user5\n") + require.Len(t, rules, 1) + + maliciousInput := strings.Repeat("a", 30) + "X" + start := time.Now() + _, err := rules[0].Rule.MatchString(maliciousInput) + elapsed := time.Since(start) + + require.Error(t, err, "expected MatchTimeout error on pathological input") + assert.Less(t, elapsed, time.Second, "match timeout did not bound regex evaluation; took %s", elapsed) +} + func testGetApprovers(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5}) // Official reviews are already deduplicated. Allow unofficial reviews diff --git a/services/issue/pull.go b/services/issue/pull.go index 055c1023b4b..260ac5ceaec 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "slices" + "time" issues_model "gitea.dev/models/issues" org_model "gitea.dev/models/organization" @@ -26,6 +27,10 @@ type ReviewRequestNotifier struct { var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} +// codeOwnerMatchBudget caps the total wall-clock time spent evaluating all +// CODEOWNERS rules against all changed files for a single PR. +const codeOwnerMatchBudget = 2 * time.Second + func IsCodeOwnerFile(f string) bool { return slices.Contains(codeOwnerFiles, f) } @@ -93,8 +98,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque uniqUsers := make(map[int64]*user_model.User) uniqTeams := make(map[string]*org_model.Team) + // Bound the total time spent matching rules×files. The per-rule MatchTimeout + // only caps a single match; without an aggregate budget a crafted CODEOWNERS + // plus a PR touching many files could still exhaust CPU inside this loop. + matchDeadline := time.Now().Add(codeOwnerMatchBudget) +ruleLoop: for _, rule := range rules { for _, f := range changedFiles { + if time.Now().After(matchDeadline) { + log.Warn("CODEOWNERS matching for PR %s#%d exceeded its time budget; some rules were not evaluated", pr.BaseRepo.FullName(), pr.ID) + break ruleLoop + } shouldMatch := !rule.Negative matched, _ := rule.Rule.MatchString(f) // err only happens when timeouts, any error can be considered as not matched if matched == shouldMatch { From 1c289df6eb13a5acbb7345c0e61fbf7b8e749ba3 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 7 Jun 2026 18:45:20 +0200 Subject: [PATCH 29/88] enhance: Adjust Workflow Graph styling (#37497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix workflow dependency graph overflow by making the graph container scrollable (no more clipped DAGs; addresses #37493). - Improve Actions job list readability by keeping durations fixed-width/right-aligned so long times don’t squeeze job names. - Make workflow graph layout more intuitive by vertically centering shorter columns to reduce misleading “looks like it depends on” alignments (addresses #37395). ### Screenshot Fixes #37493 Fixes #37395 --------- Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.7) Co-authored-by: wxiaoguang --- routers/web/devtest/mock_actions.go | 71 +- templates/devtest/repo-action-view.tmpl | 10 +- web_src/js/components/ActionRunJobView.vue | 36 +- web_src/js/components/ActionRunView.ts | 6 - web_src/js/components/RepoActionView.vue | 48 +- .../js/components/WorkflowGraph.utils.test.ts | 197 ++++ web_src/js/components/WorkflowGraph.utils.ts | 559 +++++++++++ web_src/js/components/WorkflowGraph.vue | 916 ++++++++---------- 8 files changed, 1246 insertions(+), 597 deletions(-) create mode 100644 web_src/js/components/WorkflowGraph.utils.test.ts create mode 100644 web_src/js/components/WorkflowGraph.utils.ts diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 69062ff6e78..61f20a3ef48 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -214,6 +214,75 @@ func MockActionsRunsJobs(ctx *context.Context) { return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID) } + // Keep devtest mock runs minimal: use run 10 as a "complex graph" repro. + // This combines long durations, parallel roots, and a multi-dependency downstream job + // to validate the workflow graph rendering. + if runID == 10 { + resp.State.Run.WorkflowID = "workflow-devtest-complex" + resp.State.Run.Duration = "7h 12m 34s" + + type mj struct { + jobID string + name string + status actions_model.Status + duration string + needs []string + } + mockJobs := []mj{ + {jobID: "job-100", name: "job-100", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + {jobID: "job-101", name: "job-101", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"job-100"}}, + {jobID: "job-102", name: "job-102", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"job-100", "job-101"}}, + {jobID: "job-103", name: "job-103", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"job-100"}}, + + {jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + {jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil}, + + // Matrix expansion (the " (...)" suffix is the heuristic the frontend uses to group rows) + {jobID: "matrix-e2e-1-chromium", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-1-firefox", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-2-chromium", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-3-chromium", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-3-firefox", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + {jobID: "matrix-e2e-99-webkit", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}}, + + {jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}}, + {jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}}, + {jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}}, + + {jobID: "build-image", name: "build-image", status: actions_model.StatusSuccess, duration: "3s", needs: []string{ + "unit-test", + "arch-test", + "integration-test", + "code-analysis", + "matrix-e2e-1-chromium", + "matrix-e2e-1-firefox", + "matrix-e2e-2-chromium", + "matrix-e2e-3-chromium", + "matrix-e2e-3-firefox", + "matrix-e2e-99-webkit", + }}, + } + + resp.State.Run.Jobs = nil + for i, j := range mockJobs { + id := runID*1000 + int64(i) + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ + ID: id, + Link: jobLink(id), + JobID: j.jobID, + Name: j.name, + Status: j.status.String(), + CanRerun: j.jobID == "job-100", + Duration: j.duration, + Needs: j.needs, + }) + } + + fillViewRunResponseCurrentJob(ctx, resp) + ctx.JSON(http.StatusOK, resp) + return + } + resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ ID: runID * 10, Link: jobLink(runID * 10), @@ -240,7 +309,7 @@ func MockActionsRunsJobs(ctx *context.Context) { Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit", Status: actions_model.StatusFailure.String(), CanRerun: false, - Duration: "3h", + Duration: "3h35m10s", Needs: []string{"job-100", "job-101"}, }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ diff --git a/templates/devtest/repo-action-view.tmpl b/templates/devtest/repo-action-view.tmpl index 4e06f72cdd9..12da63a7e9e 100644 --- a/templates/devtest/repo-action-view.tmpl +++ b/templates/devtest/repo-action-view.tmpl @@ -1,11 +1,11 @@ {{template "base/head" .}}
    {{template "repo/actions/view_component" (dict "JobID" (or .JobID 0) diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 835518ecb8e..d150e5899b6 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -2,7 +2,6 @@ import {computed, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch} from 'vue'; import {SvgIcon} from '../svg.ts'; import ActionStatusIcon from './ActionStatusIcon.vue'; -import WorkflowGraph from './WorkflowGraph.vue'; import {addDelegatedEventListener, createElementFromAttrs, toggleElem} from '../utils/dom.ts'; import {formatDatetime, formatDatetimeISO} from '../utils/time.ts'; import {POST} from '../modules/fetch.ts'; @@ -13,7 +12,6 @@ import {localUserSettings} from '../modules/user-settings.ts'; import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../modules/gitea-actions.ts'; import { type ActionRunViewStore, - collectCallerChildJobs, createLogLineMessage, type LogLine, type LogLineCommand, @@ -118,14 +116,11 @@ const currentJob = ref({ const stepsContainer = ref(null); const jobStepLogs = ref>([]); -// Reusable workflow caller view: when the selected job is a caller node, the right pane -// shows the children list rather than step logs (callers don't run on a runner). +// Reusable workflow caller view: the right pane shows just the header (name + uses path + +// status). Callers don't run on a runner, and the dependency graph for their children lives +// in the run summary's WorkflowGraph, not here — matching GitHub Actions. const selectedJob = computed(() => (run.value.jobs || []).find((it) => it.id === props.jobId)); const isCallerJob = computed(() => Boolean(selectedJob.value?.isReusableCaller)); -const callerChildJobs = computed(() => { - if (!isCallerJob.value) return []; - return collectCallerChildJobs(run.value.jobs || [], props.jobId); -}); watch(optionAlwaysAutoScroll, () => { saveLocaleStorageOptions(); @@ -477,20 +472,6 @@ async function hashChangeListener() {
    - -
    - -
    -
    @@ -578,8 +559,7 @@ async function hashChangeListener() { border-radius: 3px; } -.job-info-header:has(+ .job-step-container), -.job-info-header:has(+ .caller-children-container) { +.job-info-header:has(+ .job-step-container) { border-radius: var(--border-radius) var(--border-radius) 0 0; } @@ -613,14 +593,6 @@ async function hashChangeListener() { min-width: 0; } -.caller-children-container { - flex: 1; - display: flex; - flex-direction: column; - border-top: 1px solid var(--color-console-border); - color: var(--color-console-fg); -} - .job-step-container { max-height: 100%; border-radius: 0 0 var(--border-radius) var(--border-radius); diff --git a/web_src/js/components/ActionRunView.ts b/web_src/js/components/ActionRunView.ts index e9b929444fd..0d6ae9a61bd 100644 --- a/web_src/js/components/ActionRunView.ts +++ b/web_src/js/components/ActionRunView.ts @@ -104,12 +104,6 @@ export function buildJobsByParentJobID(jobs: ActionsJob[]): Map(() => { while (stack.length > 0) { const {job, depth} = stack.pop()!; const children = childrenByParent.get(job.id) || []; - const hasChildren = children.length > 0; - result.push({job, depth, hasChildren}); - if (hasChildren && isJobCollapsed(job.id)) continue; + result.push({job, depth}); + if (children.length > 0 && isJobCollapsed(job.id)) continue; for (let i = children.length - 1; i >= 0; i--) stack.push({job: children[i], depth: depth + 1}); } return result; @@ -216,24 +214,28 @@ async function deleteArtifact(name: string) { v-for="item in visibleJobListItems" :key="item.job.id" > - - - {{ item.job.name }} - - {{ item.job.duration }} - + + + + {{ item.job.name }} + + {{ item.job.duration }} +
    @@ -258,7 +260,7 @@ async function deleteArtifact(name: string) { - + {{ artifact.name }} {{ locale.artifactExpired }} @@ -406,23 +408,23 @@ async function deleteArtifact(name: string) { background-color: var(--color-active); } -.job-brief-toggle { +.caller-row-toggle { border: none; padding: 0; background: transparent; - cursor: pointer; color: inherit; - display: inline-flex; - align-items: center; - justify-content: center; + cursor: pointer; + text-align: inherit; +} + +.job-brief-toggle-icon { flex-shrink: 0; - /* the icon is always chevron-down; flip to chevron-up when expanded */ transition: transform 0.15s ease; - /* sit right after the job name; rerun/duration float to the right via auto-margin */ + /* sit between name and duration; duration uses order:2 with margin-left:auto to float right */ order: 1; } -.job-brief-toggle:not(.collapsed) { +.job-brief-toggle-icon:not(.collapsed) { transform: rotate(180deg); } diff --git a/web_src/js/components/WorkflowGraph.utils.test.ts b/web_src/js/components/WorkflowGraph.utils.test.ts new file mode 100644 index 00000000000..9d79766d83c --- /dev/null +++ b/web_src/js/components/WorkflowGraph.utils.test.ts @@ -0,0 +1,197 @@ +import {computeGraphHighlightState, computeJobLevels, createWorkflowGraphModel, matrixKeyFromJobName} from './WorkflowGraph.utils.ts'; +import type {ActionsJob} from '../modules/gitea-actions.ts'; + +const mockJobs: ActionsJob[] = [ + {id: 1, link: '', jobId: 'job-100', name: 'job-100', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s'}, + {id: 2, link: '', jobId: 'job-101', name: 'job-101', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['job-100']}, + {id: 3, link: '', jobId: 'job-102', name: 'job-102', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['job-101']}, + {id: 4, link: '', jobId: 'job-103', name: 'job-103', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100']}, + {id: 5, link: '', jobId: 'prep-jdk', name: 'prep-jdk', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s'}, + {id: 6, link: '', jobId: 'code-analysis', name: 'code-analysis', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s'}, + {id: 7, link: '', jobId: 'matrix-e2e-1-chromium', name: 'matrix-e2e (1, chromium)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 8, link: '', jobId: 'matrix-e2e-1-firefox', name: 'matrix-e2e (1, firefox)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 9, link: '', jobId: 'matrix-e2e-2-chromium', name: 'matrix-e2e (2, chromium)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 10, link: '', jobId: 'matrix-e2e-3-chromium', name: 'matrix-e2e (3, chromium)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 11, link: '', jobId: 'matrix-e2e-3-firefox', name: 'matrix-e2e (3, firefox)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 12, link: '', jobId: 'matrix-e2e-99-webkit', name: 'matrix-e2e (99, webkit)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['job-100', 'prep-jdk', 'code-analysis']}, + {id: 13, link: '', jobId: 'unit-test', name: 'unit-test', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['prep-jdk', 'code-analysis']}, + {id: 14, link: '', jobId: 'arch-test', name: 'arch-test', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['prep-jdk', 'code-analysis']}, + {id: 15, link: '', jobId: 'integration-test', name: 'integration-test', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['prep-jdk', 'code-analysis']}, + {id: 16, link: '', jobId: 'build-image', name: 'build-image', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: [ + 'unit-test', + 'arch-test', + 'integration-test', + 'matrix-e2e-1-chromium', + 'matrix-e2e-1-firefox', + 'matrix-e2e-2-chromium', + 'matrix-e2e-3-chromium', + 'matrix-e2e-3-firefox', + 'matrix-e2e-99-webkit', + ]}, +]; + +const verifyDeployJobs: ActionsJob[] = [ + {id: 101, link: '', jobId: 'seed-dev', name: 'seed-dev', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s'}, + {id: 102, link: '', jobId: 'seed-qa', name: 'seed-qa', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s'}, + {id: 103, link: '', jobId: 'verify-dev', name: 'Verify Dev', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['seed-dev']}, + {id: 104, link: '', jobId: 'verify-qa', name: 'Verify QA', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['seed-qa']}, + {id: 105, link: '', jobId: 'deploy', name: 'Deploy', status: 'blocked', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '', needs: ['verify-dev', 'verify-qa']}, +]; + +// Multi-level pipeline with two matrices and a leaf with two parents. +const wfTest1Jobs: ActionsJob[] = [ + {id: 1, link: '', jobId: 'init', name: 'Initialize Pipeline', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '1s'}, + {id: 2, link: '', jobId: 'lint-frontend', name: 'Lint Frontend', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['init']}, + {id: 3, link: '', jobId: 'lint-backend', name: 'Lint Backend', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['init']}, + {id: 4, link: '', jobId: 'build-frontend', name: 'Build Frontend', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['lint-frontend']}, + {id: 5, link: '', jobId: 'build-backend', name: 'Build Backend', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '5s', needs: ['lint-backend']}, + {id: 6, link: '', jobId: 'tu-api-t', name: 'Unit Tests (api, true)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['build-frontend', 'build-backend']}, + {id: 7, link: '', jobId: 'tu-api-f', name: 'Unit Tests (api, false)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['build-frontend', 'build-backend']}, + {id: 8, link: '', jobId: 'tu-svc-t', name: 'Unit Tests (service, true)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['build-frontend', 'build-backend']}, + {id: 9, link: '', jobId: 'test-integration', name: 'Integration Tests', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '6s', needs: ['build-backend']}, + {id: 10, link: '', jobId: 'te-c-d', name: 'E2E Tests (chrome, desktop)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['build-frontend', 'tu-api-t', 'tu-api-f', 'tu-svc-t']}, + {id: 11, link: '', jobId: 'te-c-m', name: 'E2E Tests (chrome, mobile)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['build-frontend', 'tu-api-t', 'tu-api-f', 'tu-svc-t']}, + {id: 12, link: '', jobId: 'te-f-d', name: 'E2E Tests (firefox, desktop)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '4s', needs: ['build-frontend', 'tu-api-t', 'tu-api-f', 'tu-svc-t']}, + {id: 13, link: '', jobId: 'bundle-app', name: 'Bundle Application', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['tu-api-t', 'tu-api-f', 'tu-svc-t', 'test-integration', 'te-c-d', 'te-c-m', 'te-f-d']}, + {id: 14, link: '', jobId: 'deploy-dev', name: 'Deploy to Dev', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['bundle-app']}, + {id: 15, link: '', jobId: 'deploy-qa', name: 'Deploy to QA', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '3s', needs: ['bundle-app']}, + {id: 16, link: '', jobId: 'verify-dev', name: 'Verify Dev', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['deploy-dev']}, + {id: 17, link: '', jobId: 'verify-qa', name: 'Verify QA', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['deploy-qa']}, + {id: 18, link: '', jobId: 'deploy-prod', name: 'Deploy to Production', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '5s', needs: ['verify-dev', 'verify-qa']}, + {id: 19, link: '', jobId: 'post-deploy-checks', name: 'Post-Deploy Checks', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '2s', needs: ['deploy-prod']}, +]; + +test('matrix key heuristic strips trailing parameter list', () => { + expect(matrixKeyFromJobName('matrix-e2e (1, chromium)')).toBe('matrix-e2e'); + expect(matrixKeyFromJobName('plain-job')).toBeNull(); +}); + +test('computeJobLevels keeps stable topological levels', () => { + const levels = computeJobLevels(mockJobs); + expect(levels.get('job-100')).toBe(0); + expect(levels.get('job-101')).toBe(1); + expect(levels.get('job-102')).toBe(2); + expect(levels.get('build-image')).toBe(2); +}); + +test('graph model collapses matrix and groups jobs that share parents and children', () => { + const graph = createWorkflowGraphModel(mockJobs); + + expect(graph.nodes.find((n) => n.type === 'matrix')?.jobs).toHaveLength(6); + const groupJobIds = graph.nodes.filter((n) => n.type === 'group').map((g) => g.jobs.map((j) => j.jobId)); + expect(groupJobIds).toEqual(expect.arrayContaining([ + ['prep-jdk', 'code-analysis'], + ['unit-test', 'arch-test', 'integration-test'], + ])); +}); + +test('expanded matrix height includes summary and toggle rows', () => { + const collapsed = createWorkflowGraphModel(mockJobs); + const expanded = createWorkflowGraphModel(mockJobs, new Set(['matrix-e2e'])); + const collapsedMatrix = collapsed.nodes.find((n) => n.id === 'matrix:matrix-e2e'); + const expandedMatrix = expanded.nodes.find((n) => n.id === 'matrix:matrix-e2e'); + + expect(collapsedMatrix?.displayHeight).toBeLessThan(expandedMatrix?.displayHeight ?? 0); + // 6 jobs * 26 row height + 24 header + 6 pad * 2 = 192 + expect(expandedMatrix?.displayHeight).toBe(192); +}); + +test('every dependency is rendered as one routed edge', () => { + const graph = createWorkflowGraphModel(mockJobs); + const rootGroup = graph.nodes.find((n) => n.type === 'group' && n.jobs.some((j) => j.jobId === 'prep-jdk'))!; + const testGroup = graph.nodes.find((n) => n.type === 'group' && n.jobs.some((j) => j.jobId === 'unit-test'))!; + const expectedKeys = [ + `${rootGroup.id}->matrix:matrix-e2e`, + `${rootGroup.id}->${testGroup.id}`, + ]; + const keys = new Set(graph.routedEdges.map((e) => e.key)); + for (const k of expectedKeys) expect(keys.has(k)).toBe(true); +}); + +test('same-row edge collapses to a single horizontal line', () => { + const graph = createWorkflowGraphModel(verifyDeployJobs); + const verifyDevEdge = graph.routedEdges.find((e) => e.fromId === 'job:101' && e.toId === 'job:103'); + const verifyQaEdge = graph.routedEdges.find((e) => e.fromId === 'job:102' && e.toId === 'job:104'); + expect(verifyDevEdge?.path).toMatch(/^M [\d.]+ [\d.]+ H [\d.]+$/); + expect(verifyQaEdge?.path).toMatch(/^M [\d.]+ [\d.]+ H [\d.]+$/); +}); + +test('different-row edge uses cubic bezier curve', () => { + const graph = createWorkflowGraphModel(verifyDeployJobs); + const deployLowerEdge = graph.routedEdges.find((e) => e.fromId === 'job:104' && e.toId === 'job:105'); + expect(deployLowerEdge?.path).toContain(' C '); +}); + +test('multi-level pipeline with two matrices and a converging leaf renders without errors', () => { + const graph = createWorkflowGraphModel(wfTest1Jobs); + const matrices = graph.nodes.filter((n) => n.type === 'matrix'); + expect(matrices.map((n) => n.matrixKey).sort()).toEqual(['E2E Tests', 'Unit Tests']); + + const deployProd = graph.nodes.find((n) => n.id === 'job:18'); + const verifyDev = graph.nodes.find((n) => n.id === 'job:16'); + const verifyQa = graph.nodes.find((n) => n.id === 'job:17'); + expect(verifyDev?.level).toBe(verifyQa?.level); + expect(deployProd?.level).toBe((verifyDev?.level ?? 0) + 1); + + for (const node of graph.nodes) { + expect(Number.isFinite(node.x)).toBe(true); + expect(Number.isFinite(node.y)).toBe(true); + expect(node.x).toBeGreaterThanOrEqual(0); + expect(node.y).toBeGreaterThanOrEqual(0); + } + for (const edge of graph.routedEdges) { + expect(edge.path).not.toMatch(/NaN|undefined|Infinity/); + } +}); + +test('reusable callers with identical dependency signature are kept as separate nodes', () => { + const jobs: ActionsJob[] = [ + {id: 1, link: '', jobId: 'prepare', name: 'prepare', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '30s'}, + {id: 2, link: '', jobId: 'local_caller', name: 'local caller', status: 'running', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '5m', needs: ['prepare'], callUses: './.gitea/workflows/lib.yml'}, + {id: 3, link: '', jobId: 'cross_caller', name: 'cross-repo caller', status: 'waiting', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '0s', needs: ['prepare'], callUses: 'user2/lib/.gitea/workflows/ext.yml@main'}, + {id: 4, link: '', jobId: 'final', name: 'final', status: 'blocked', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '0s', needs: ['local_caller', 'cross_caller']}, + ]; + const graph = createWorkflowGraphModel(jobs); + expect(graph.nodes.find((n) => n.type === 'group')).toBeUndefined(); + expect(graph.nodes.find((n) => n.id === 'job:2')?.name).toBe('local caller'); + expect(graph.nodes.find((n) => n.id === 'job:3')?.name).toBe('cross-repo caller'); +}); + +test('reusable caller with matrix-pattern name does not get absorbed into a sibling matrix node', () => { + const jobs: ActionsJob[] = [ + {id: 1, link: '', jobId: 'deploy_dev', name: 'deploy (dev)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '1s'}, + {id: 2, link: '', jobId: 'deploy_qa', name: 'deploy (qa)', status: 'success', canRerun: false, isReusableCaller: false, parentJobID: 0, duration: '1s'}, + {id: 3, link: '', jobId: 'deploy_staging', name: 'deploy (staging)', status: 'running', canRerun: false, isReusableCaller: true, parentJobID: 0, duration: '2s', callUses: './.gitea/workflows/deploy.yml'}, + ]; + const graph = createWorkflowGraphModel(jobs); + expect(graph.nodes.find((n) => n.id === 'job:3')?.name).toBe('deploy (staging)'); + const matrixNode = graph.nodes.find((n) => n.type === 'matrix'); + expect(matrixNode?.jobs.map((j) => j.id).sort()).toEqual([1, 2]); +}); + +test('directed highlight state covers ancestors and descendants of the hovered node', () => { + const graph = createWorkflowGraphModel(mockJobs); + const rootGroup = graph.nodes.find((n) => n.type === 'group' && n.jobs.some((j) => j.jobId === 'prep-jdk'))!; + + const highlight = computeGraphHighlightState(rootGroup.id, graph.adjacency); + expect(highlight.nodeIds.has('matrix:matrix-e2e')).toBe(true); + expect(highlight.nodeIds.has('job:16')).toBe(true); + expect(highlight.edgeKeys.has(`${rootGroup.id}->matrix:matrix-e2e`)).toBe(true); +}); + +test('directed highlight state for converging graph excludes sibling branch when hovering parent', () => { + const graph = createWorkflowGraphModel(verifyDeployJobs); + + const parentHighlight = computeGraphHighlightState('job:103', graph.adjacency); + expect(parentHighlight.nodeIds.has('job:101')).toBe(true); + expect(parentHighlight.nodeIds.has('job:105')).toBe(true); + expect(parentHighlight.nodeIds.has('job:104')).toBe(false); + expect(parentHighlight.edgeKeys.has('job:103->job:105')).toBe(true); + expect(parentHighlight.edgeKeys.has('job:104->job:105')).toBe(false); + + const sinkHighlight = computeGraphHighlightState('job:105', graph.adjacency); + expect(sinkHighlight.nodeIds.has('job:103')).toBe(true); + expect(sinkHighlight.nodeIds.has('job:104')).toBe(true); + expect(sinkHighlight.edgeKeys.has('job:103->job:105')).toBe(true); + expect(sinkHighlight.edgeKeys.has('job:104->job:105')).toBe(true); +}); diff --git a/web_src/js/components/WorkflowGraph.utils.ts b/web_src/js/components/WorkflowGraph.utils.ts new file mode 100644 index 00000000000..71ef7e6fc9e --- /dev/null +++ b/web_src/js/components/WorkflowGraph.utils.ts @@ -0,0 +1,559 @@ +import type {ActionsJob, ActionsStatus} from '../modules/gitea-actions.ts'; + +export type GraphNodeType = 'job' | 'matrix' | 'group'; + +export type GraphNode = { + id: string; + type: GraphNodeType; + name: string; + status: ActionsStatus; + duration: string; + x: number; + y: number; + level: number; + displayHeight: number; + jobs: ActionsJob[]; + matrixKey?: string; +}; + +export type Edge = { + fromId: string; + toId: string; + key: string; +}; + +export type RoutedEdge = Edge & { + path: string; + fromNode: GraphNode; + toNode: GraphNode; +}; + +export type SharedSegment = { + key: string; + edgeKeys: string[]; + path: string; +}; + +export type GraphHighlightState = { + nodeIds: Set; + edgeKeys: Set; +}; + +export type WorkflowGraphLayoutOptions = { + margin: number; + nodeWidth: number; + nodeHeight: number; + columnGap: number; + laneGap: number; + groupRowHeight: number; + groupPadY: number; + matrixCollapsedHeight: number; + matrixHeaderHeight: number; + matrixRowHeight: number; + matrixPadY: number; +}; + +export type WorkflowGraphModel = { + nodes: GraphNode[]; + edges: Edge[]; + routedEdges: RoutedEdge[]; + sharedSegments: SharedSegment[]; + adjacency: NodeAdjacency; +}; + +export type NodeAdjacency = { + incomingByNodeId: Map; + outgoingByNodeId: Map; +}; + +const defaultLayoutOptions: WorkflowGraphLayoutOptions = { + margin: 24, + nodeWidth: 220, + nodeHeight: 40, + columnGap: 96, + laneGap: 32, + groupRowHeight: 28, + groupPadY: 8, + matrixCollapsedHeight: 78, + matrixHeaderHeight: 24, + matrixRowHeight: 26, + matrixPadY: 6, +}; + +function canonicalKey(ids: Iterable): string { + return Array.from(ids).sort().join(''); +} + +function graphIdForJob(job: ActionsJob): string { + return `job:${job.id}`; +} + +export function matrixKeyFromJobName(name: string): string | null { + const idx = name.indexOf(' ('); + if (idx === -1) return null; + return name.slice(0, idx).trim() || null; +} + +export function boxBottom(node: GraphNode): number { + return node.y + node.displayHeight; +} + +export function boxCenterY(node: GraphNode): number { + return node.y + node.displayHeight / 2; +} + +function matrixPanelHeight(rowCount: number, expanded: boolean, options: WorkflowGraphLayoutOptions): number { + if (rowCount <= 0) return options.nodeHeight; + if (!expanded) return options.matrixCollapsedHeight; + return options.matrixHeaderHeight + rowCount * options.matrixRowHeight + options.matrixPadY * 2; +} + +function groupPanelHeight(rowCount: number, options: WorkflowGraphLayoutOptions): number { + return rowCount * options.groupRowHeight + options.groupPadY * 2; +} + +function compareStatusWorstFirst(a: ActionsStatus, b: ActionsStatus): number { + const rank = (s: ActionsStatus) => { + if (s === 'failure') return 0; + if (s === 'cancelled') return 1; + if (s === 'running') return 2; + if (s === 'waiting') return 3; + if (s === 'blocked') return 4; + if (s === 'success') return 5; + if (s === 'skipped') return 6; + return 7; + }; + return rank(a) - rank(b); +} + +function aggregateStatus(children: ActionsJob[]): ActionsStatus { + return children.map((c) => c.status).slice().sort(compareStatusWorstFirst)[0] ?? 'unknown'; +} + +function buildDirectNeedsMap(jobs: ActionsJob[]): Map { + const directNeedsByJobId = new Map(); + const dependentsByJobId = new Map>(); + + for (const job of jobs) { + const needs = job.needs || []; + directNeedsByJobId.set(job.jobId, needs); + for (const need of needs) { + if (!dependentsByJobId.has(need)) dependentsByJobId.set(need, new Set()); + dependentsByJobId.get(need)!.add(job.jobId); + } + } + + const reachabilityCache = new Map(); + function canReach(fromJobId: string, toJobId: string): boolean { + const cacheKey = `${fromJobId}->${toJobId}`; + if (reachabilityCache.has(cacheKey)) return reachabilityCache.get(cacheKey)!; + const visited = new Set(); + const stack = Array.from(dependentsByJobId.get(fromJobId) || []); + while (stack.length > 0) { + const current = stack.pop()!; + if (current === toJobId) { + reachabilityCache.set(cacheKey, true); + return true; + } + if (visited.has(current)) continue; + visited.add(current); + stack.push(...(dependentsByJobId.get(current) || [])); + } + reachabilityCache.set(cacheKey, false); + return false; + } + + const reducedNeedsByJobId = new Map(); + for (const [jobId, needs] of directNeedsByJobId) { + reducedNeedsByJobId.set(jobId, needs.filter((need) => { + return !needs.some((other) => other !== need && canReach(need, other)); + })); + } + return reducedNeedsByJobId; +} + +export function computeJobLevels(jobs: ActionsJob[]): Map { + const jobMap = new Map(); + for (const job of jobs) { + jobMap.set(job.name, job); + if (job.jobId) jobMap.set(job.jobId, job); + } + + const levels = new Map(); + const visited = new Set(); + const recursionStack = new Set(); + + function dfs(jobNameOrId: string): number { + if (recursionStack.has(jobNameOrId)) return 0; + if (visited.has(jobNameOrId)) return levels.get(jobNameOrId) ?? 0; + recursionStack.add(jobNameOrId); + visited.add(jobNameOrId); + + const job = jobMap.get(jobNameOrId); + if (!job) { + recursionStack.delete(jobNameOrId); + return 0; + } + if (!job.needs?.length) { + levels.set(job.jobId, 0); + if (job.jobId !== job.name) levels.set(job.name, 0); + recursionStack.delete(jobNameOrId); + return 0; + } + + let maxLevel = -1; + for (const need of job.needs) { + if (!jobMap.has(need)) continue; + maxLevel = Math.max(maxLevel, dfs(need)); + } + const level = maxLevel + 1; + levels.set(job.name, level); + levels.set(job.jobId, level); + recursionStack.delete(jobNameOrId); + return level; + } + + for (const job of jobs) { + if (!visited.has(job.jobId)) dfs(job.jobId); + } + return levels; +} + +export function computeGraphHighlightState(hoveredId: string | null, adjacency: NodeAdjacency): GraphHighlightState { + if (!hoveredId) return {nodeIds: new Set(), edgeKeys: new Set()}; + const {incomingByNodeId, outgoingByNodeId} = adjacency; + + const edgeKeys = new Set(); + const collect = (startId: string, adj: Map, edgeKeyForward: boolean): Set => { + const seen = new Set(); + const queue = [startId]; + while (queue.length > 0) { + const current = queue.shift()!; + if (seen.has(current)) continue; + seen.add(current); + for (const next of adj.get(current) || []) { + edgeKeys.add(edgeKeyForward ? `${current}->${next}` : `${next}->${current}`); + if (!seen.has(next)) queue.push(next); + } + } + return seen; + }; + + const ancestors = collect(hoveredId, incomingByNodeId, false); + const descendants = collect(hoveredId, outgoingByNodeId, true); + return {nodeIds: new Set([...ancestors, ...descendants]), edgeKeys}; +} + +type VisualGraphBuild = { + nodes: GraphNode[]; + edges: Edge[]; +}; + +function buildVisualGraph( + jobs: ActionsJob[], + expandedMatrixKeys: ReadonlySet, + options: WorkflowGraphLayoutOptions, +): VisualGraphBuild { + const jobsByJobId = new Map(); + const jobIndexById = new Map(); + for (const [index, job] of jobs.entries()) { + jobIndexById.set(job.id, index); + if (!jobsByJobId.has(job.jobId)) jobsByJobId.set(job.jobId, []); + jobsByJobId.get(job.jobId)!.push(job); + } + + const matrixJobsByKey = new Map(); + for (const job of jobs) { + // Reusable callers are distinct workflow files — never fold them into a matrix bucket + // even if their display name happens to look like "name (variant)". + if (job.isReusableCaller) continue; + const matrixKey = matrixKeyFromJobName(job.name); + if (!matrixKey) continue; + if (!matrixJobsByKey.has(matrixKey)) matrixJobsByKey.set(matrixKey, []); + matrixJobsByKey.get(matrixKey)!.push(job); + } + for (const list of matrixJobsByKey.values()) { + list.sort((a, b) => (jobIndexById.get(a.id) ?? 0) - (jobIndexById.get(b.id) ?? 0)); + } + + const directNeedsByJobId = buildDirectNeedsMap(jobs); + const rawLevels = computeJobLevels(jobs); + const dependentsByJobId = new Map(); + const rawEdges: Array<{from: ActionsJob; to: ActionsJob}> = []; + + for (const job of jobs) { + for (const need of directNeedsByJobId.get(job.jobId) || []) { + for (const upstream of jobsByJobId.get(need) || []) { + rawEdges.push({from: upstream, to: job}); + if (!dependentsByJobId.has(upstream.jobId)) dependentsByJobId.set(upstream.jobId, []); + dependentsByJobId.get(upstream.jobId)!.push(job.jobId); + } + } + } + for (const list of dependentsByJobId.values()) list.sort(); + + // Group sibling jobs that share an identical (parents, children) signature into a single + // collapsed "group" node. This is a visual aggregation only - the underlying jobs are + // preserved on the node so the panel can list them. + const groupedJobIds = new Map(); + const groupsById = new Map(); + const groupCandidateBuckets = new Map(); + for (const job of jobs) { + if (matrixKeyFromJobName(job.name)) continue; + // Reusable callers represent distinct workflow files — keep each as its own node so the + // graph mirrors GitHub Actions, where every caller shows up as its own box even when + // siblings share an identical (parents, children) dependency signature. + if (job.isReusableCaller) continue; + const needsKey = canonicalKey(directNeedsByJobId.get(job.jobId) || []); + const childrenKey = (dependentsByJobId.get(job.jobId) || []).join(''); + if (!needsKey && !childrenKey) continue; + const level = rawLevels.get(job.jobId) ?? 0; + const key = `group:${level}:${needsKey}:${childrenKey}`; + if (!groupCandidateBuckets.has(key)) groupCandidateBuckets.set(key, []); + groupCandidateBuckets.get(key)!.push(job); + } + for (const [groupId, groupJobs] of groupCandidateBuckets) { + if (groupJobs.length < 2) continue; + groupJobs.sort((a, b) => (jobIndexById.get(a.id) ?? 0) - (jobIndexById.get(b.id) ?? 0)); + groupsById.set(groupId, groupJobs); + for (const job of groupJobs) groupedJobIds.set(job.id, groupId); + } + + const visualIdByJobId = new Map(); + for (const job of jobs) { + const matrixKey = matrixKeyFromJobName(job.name); + // Symmetric with the matrix-bucket loop above: a reusable caller whose display name + // happens to look like "name (variant)" must never be folded into the matrix node, or it + // would silently vanish (its visualId would point at a matrix node it isn't part of). + if (matrixKey && !job.isReusableCaller && (matrixJobsByKey.get(matrixKey)?.length ?? 0) > 1) { + visualIdByJobId.set(job.id, `matrix:${matrixKey}`); + continue; + } + visualIdByJobId.set(job.id, groupedJobIds.get(job.id) || graphIdForJob(job)); + } + + const emittedNodeIds = new Set(); + const nodes: GraphNode[] = []; + for (const job of jobs) { + const visualId = visualIdByJobId.get(job.id); + if (!visualId || emittedNodeIds.has(visualId)) continue; + emittedNodeIds.add(visualId); + + const matrixKey = matrixKeyFromJobName(job.name); + if (matrixKey && visualId.startsWith('matrix:')) { + const matrixJobs = matrixJobsByKey.get(matrixKey) || []; + nodes.push({ + id: visualId, + type: 'matrix', + name: matrixKey, + status: aggregateStatus(matrixJobs), + duration: '', + x: 0, y: 0, level: 0, + displayHeight: matrixPanelHeight(matrixJobs.length, expandedMatrixKeys.has(matrixKey), options), + jobs: matrixJobs, + matrixKey, + }); + continue; + } + + const groupJobs = groupsById.get(visualId); + if (groupJobs) { + nodes.push({ + id: visualId, + type: 'group', + name: groupJobs.map((g) => g.name).join(', '), + status: aggregateStatus(groupJobs), + duration: '', + x: 0, y: 0, level: 0, + displayHeight: groupPanelHeight(groupJobs.length, options), + jobs: groupJobs, + }); + continue; + } + + nodes.push({ + id: visualId, + type: 'job', + name: job.name, + status: job.status, + duration: job.duration, + x: 0, y: 0, level: 0, + displayHeight: options.nodeHeight, + jobs: [job], + }); + } + + const seenEdges = new Set(); + const edges: Edge[] = []; + for (const {from, to} of rawEdges) { + const fromId = visualIdByJobId.get(from.id); + const toId = visualIdByJobId.get(to.id); + if (!fromId || !toId || fromId === toId) continue; + const key = `${fromId}->${toId}`; + if (seenEdges.has(key)) continue; + seenEdges.add(key); + edges.push({fromId, toId, key}); + } + + return {nodes, edges}; +} + +function buildNodeAdjacency(edges: Edge[]): NodeAdjacency { + const incomingByNodeId = new Map(); + const outgoingByNodeId = new Map(); + for (const edge of edges) { + if (!incomingByNodeId.has(edge.toId)) incomingByNodeId.set(edge.toId, []); + incomingByNodeId.get(edge.toId)!.push(edge.fromId); + if (!outgoingByNodeId.has(edge.fromId)) outgoingByNodeId.set(edge.fromId, []); + outgoingByNodeId.get(edge.fromId)!.push(edge.toId); + } + return {incomingByNodeId, outgoingByNodeId}; +} + +function assignNodeLevels(nodes: GraphNode[], {incomingByNodeId}: NodeAdjacency): void { + const cache = new Map(); + function levelFor(id: string, visiting = new Set()): number { + if (cache.has(id)) return cache.get(id)!; + if (visiting.has(id)) return 0; + visiting.add(id); + const incoming = incomingByNodeId.get(id) || []; + const level = incoming.length > 0 ? + Math.max(...incoming.map((fromId) => levelFor(fromId, visiting))) + 1 : + 0; + visiting.delete(id); + cache.set(id, level); + return level; + } + for (const node of nodes) node.level = levelFor(node.id); +} + +// Roots stay in input order; later levels are sorted by the mean parent Y so that simple +// chains stay on a straight horizontal line. +function assignNodeCoordinates(nodesById: Map, nodes: GraphNode[], adjacency: NodeAdjacency, options: WorkflowGraphLayoutOptions): void { + const {incomingByNodeId} = adjacency; + const inputRank = (node: GraphNode): number => Math.min(...node.jobs.map((j) => j.id)); + + const nodesByLevel = new Map(); + for (const node of nodes) { + if (!nodesByLevel.has(node.level)) nodesByLevel.set(node.level, []); + nodesByLevel.get(node.level)!.push(node); + } + const orderedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b); + + // Initial X assignment and a default Y so barycenters can use a finite value. + for (const level of orderedLevels) { + const list = nodesByLevel.get(level)!; + list.sort((a, b) => inputRank(a) - inputRank(b)); + let yCursor = options.margin; + for (const node of list) { + node.x = options.margin + level * (options.nodeWidth + options.columnGap); + node.y = yCursor; + yCursor += node.displayHeight + options.laneGap; + } + } + + function packLevel(level: number, anchorOf: (n: GraphNode) => number): void { + const list = nodesByLevel.get(level)!; + const sorted = Array.from(list).sort((a, b) => anchorOf(a) - anchorOf(b) || inputRank(a) - inputRank(b)); + // Pack tight to top after sorting. Using barycenter only for order (not Y) keeps terminal + // nodes like build-image close to the top of their column instead of being pulled down to + // the mean Y of their parents — matching GitHub Actions' compact layout. + let prevBottom = options.margin - options.laneGap; + for (const node of sorted) { + node.y = prevBottom + options.laneGap; + prevBottom = boxBottom(node); + } + nodesByLevel.set(level, sorted); + } + + function meanCenterOf(ids: string[]): number | null { + if (ids.length === 0) return null; + let sum = 0; + for (const id of ids) sum += boxCenterY(nodesById.get(id)!); + return sum / ids.length; + } + + // Down-only barycenter pass: each child is anchored to the mean Y of its parents. Roots + // keep their initial yaml-declaration order (via inputRank), matching how GitHub Actions + // arranges root jobs. This produces a "main chain on top" layout where job-100 → job-101 → + // job-102 stays on a straight horizontal line. + for (const level of orderedLevels) { + if (level === 0) continue; + packLevel(level, (node) => meanCenterOf(incomingByNodeId.get(node.id) || []) ?? boxCenterY(node)); + } +} + +// Per-edge connector: source stub → cubic-bezier corner down/up to column midpoint → +// vertical run → cubic-bezier corner back to horizontal → target stub. The corner radius is +// fixed (not clamped to the row delta) so any two edges sharing the same source produce the +// same source-side path and overlap into a single visual line until they diverge at the V. +const cornerRadius = 12; + +function connectorPath(sx: number, sy: number, ex: number, ey: number, options: WorkflowGraphLayoutOptions): string { + if (Math.abs(sy - ey) < 0.5) return `M ${sx} ${sy} H ${ex}`; + // Anchor the V segment in the column gap immediately before the target instead of the + // horizontal midpoint. The long H stays at the source's Y, matching GitHub Actions' style + // — a multi-column edge runs along the source row across intermediate columns, then turns + // up/down only when it reaches the target column. + const midX = Math.max(ex - options.columnGap / 2, (sx + ex) / 2); + const dy = ey > sy ? 1 : -1; + // Keep the same H prefix to `midX - cornerRadius` for every edge so that edges sharing a + // source overlap visually until they fork. When there isn't 2*cornerRadius of vertical + // room for the V segment, emit a single S-curve between (midX - r, sy) and (midX + r, ey) + // instead of a backward V kink. + if (Math.abs(ey - sy) < cornerRadius * 2) { + return [ + `M ${sx} ${sy}`, + `H ${midX - cornerRadius}`, + `C ${midX} ${sy} ${midX} ${ey} ${midX + cornerRadius} ${ey}`, + `H ${ex}`, + ].join(' '); + } + const half = cornerRadius / 2; + return [ + `M ${sx} ${sy}`, + `H ${midX - cornerRadius}`, + `C ${midX - half} ${sy} ${midX} ${sy + half * dy} ${midX} ${sy + cornerRadius * dy}`, + `V ${ey - cornerRadius * dy}`, + `C ${midX} ${ey - half * dy} ${midX + half} ${ey} ${midX + cornerRadius} ${ey}`, + `H ${ex}`, + ].join(' '); +} + +function buildRoutedEdges( + nodesById: Map, + edges: Edge[], + options: WorkflowGraphLayoutOptions, +): Pick { + const routedEdges: RoutedEdge[] = []; + for (const edge of edges) { + const fromNode = nodesById.get(edge.fromId); + const toNode = nodesById.get(edge.toId); + if (!fromNode || !toNode) continue; + const startX = fromNode.x + options.nodeWidth; + const endX = toNode.x; + const startY = boxCenterY(fromNode); + const endY = boxCenterY(toNode); + routedEdges.push({...edge, fromNode, toNode, path: connectorPath(startX, startY, endX, endY, options)}); + } + + return {routedEdges, sharedSegments: []}; +} + +export function createWorkflowGraphModel( + jobs: ActionsJob[], + expandedMatrixKeys: ReadonlySet = new Set(), + partialOptions: Partial = {}, +): WorkflowGraphModel { + const options = {...defaultLayoutOptions, ...partialOptions}; + const {nodes, edges} = buildVisualGraph(jobs, expandedMatrixKeys, options); + const nodesById = new Map(nodes.map((n) => [n.id, n])); + const adjacency = buildNodeAdjacency(edges); + assignNodeLevels(nodes, adjacency); + assignNodeCoordinates(nodesById, nodes, adjacency, options); + return {nodes, edges, ...buildRoutedEdges(nodesById, edges, options), adjacency}; +} + +export function getWorkflowGraphLayoutOptions(partialOptions: Partial = {}): WorkflowGraphLayoutOptions { + return {...defaultLayoutOptions, ...partialOptions}; +} diff --git a/web_src/js/components/WorkflowGraph.vue b/web_src/js/components/WorkflowGraph.vue index 27ba960feb6..4c8762dc392 100644 --- a/web_src/js/components/WorkflowGraph.vue +++ b/web_src/js/components/WorkflowGraph.vue @@ -6,31 +6,17 @@ import {localUserSettings} from '../modules/user-settings.ts'; import {isPlainClick} from '../utils/dom.ts'; import {trN} from '../modules/i18n.ts'; import {debounce} from 'throttle-debounce'; -import type {ActionsJob, ActionsStatus} from '../modules/gitea-actions.ts'; +import type {ActionsJob} from '../modules/gitea-actions.ts'; import type {ActionRunViewStore} from './ActionRunView.ts'; - -interface JobNode { - id: number; - name: string; - status: ActionsStatus; - duration: string; - - x: number; - y: number; - level: number; -} - -interface Edge { - fromId: number; - toId: number; - key: string; -} - -interface RoutedEdge extends Edge { - path: string; - fromNode: JobNode; - toNode: JobNode; -} +import { + boxBottom, + boxCenterY, + computeGraphHighlightState, + createWorkflowGraphModel, + getWorkflowGraphLayoutOptions, + type GraphNode, + type RoutedEdge, +} from './WorkflowGraph.utils.ts'; interface StoredState { scale: number; @@ -45,10 +31,11 @@ const props = defineProps<{ runLink: string; workflowId: string; locale: Record; -}>() +}>(); const settingKeyStates = 'actions-graph-states'; const maxStoredStates = 10; +const layout = getWorkflowGraphLayoutOptions(); const scale = ref(1); const translateX = ref(0); @@ -56,9 +43,21 @@ const translateY = ref(0); const isDragging = ref(false); const lastMousePos = ref({x: 0, y: 0}); const graphContainer = ref(null); -const hoveredJobId = ref(null); +const hoveredGraphId = ref(null); const stateKey = () => `${props.store.viewData.currentRun.repoId}-${props.workflowId}`; +const expandedMatrixKeys = ref>(new Set()); + +function isMatrixExpanded(key: string): boolean { + return expandedMatrixKeys.value.has(key); +} + +function toggleMatrixExpanded(key: string) { + const next = new Set(expandedMatrixKeys.value); + if (next.has(key)) next.delete(key); + else next.add(key); + expandedMatrixKeys.value = next; +} const loadSavedState = () => { const allStates = localUserSettings.getJsonObject>(settingKeyStates, {}); @@ -85,289 +84,35 @@ const saveState = () => { localUserSettings.setJsonObject(settingKeyStates, Object.fromEntries(sortedStates)); }; -const minNodeWidth = 168; -const maxNodeWidth = 232; -const nodeWidth = computed(() => { - const maxNameLength = Math.max(...props.jobs.map(j => j.name.length), 0); - return Math.min(Math.max(minNodeWidth, maxNameLength * 8), maxNodeWidth); -}); +const graphModel = computed(() => createWorkflowGraphModel(props.jobs, expandedMatrixKeys.value)); +const jobsWithLayout = computed(() => graphModel.value.nodes); +const edges = computed(() => graphModel.value.edges); +const routedEdges = computed(() => graphModel.value.routedEdges); -const horizontalSpacing = computed(() => nodeWidth.value + 84); +const nodeWidth = layout.nodeWidth; const graphWidth = computed(() => { if (jobsWithLayout.value.length === 0) return 800; - const maxX = Math.max(...jobsWithLayout.value.map(j => j.x + nodeWidth.value)); - return maxX + margin * 2; + const maxX = Math.max(...jobsWithLayout.value.map((job) => job.x + nodeWidth)); + return maxX + layout.margin * 2; }); const graphHeight = computed(() => { if (jobsWithLayout.value.length === 0) return 400; - const maxY = Math.max(...jobsWithLayout.value.map(j => j.y + nodeHeight)); - return maxY + margin * 2; + const maxY = Math.max(...jobsWithLayout.value.map((job) => boxBottom(job))); + return maxY + layout.margin * 2; }); - -const jobsWithLayout = computed(() => { - try { - const levels = computeJobLevels(props.jobs); - const currentHorizontalSpacing = horizontalSpacing.value; - - const jobsByLevel: ActionsJob[][] = []; - let maxJobsPerLevel = 0; - - props.jobs.forEach(job => { - // `?? 0`, not `|| 0`: a root job's level is 0, which `||` would wrongly discard. - const level = levels.get(scopedKey(job)) ?? 0; - - if (!jobsByLevel[level]) { - jobsByLevel[level] = []; - } - jobsByLevel[level].push(job); - - if (jobsByLevel[level].length > maxJobsPerLevel) { - maxJobsPerLevel = jobsByLevel[level].length; - } - }); - - const result: JobNode[] = []; - jobsByLevel.forEach((levelJobs, levelIndex) => { - if (!levelJobs || levelJobs.length === 0) { - return; - } - - const startY = margin; - - levelJobs.forEach((job, jobIndex) => { - result.push({ - id: job.id, - name: job.name, - status: job.status, - duration: job.duration, - - x: margin + levelIndex * currentHorizontalSpacing, - y: startY + jobIndex * verticalSpacing, - level: levelIndex, - }); - }); - }); - - return result; - } catch (error) { - return props.jobs.map((job, index) => ({ - id: job.id, - name: job.name, - status: job.status, - duration: job.duration, - - x: margin + index * horizontalSpacing.value, - y: margin, - level: 0, - })); - } +const successRateLabel = computed(() => { + if (props.jobs.length === 0) return '0%'; + const successCount = props.jobs.filter((job) => job.status === 'success').length; + return `${((successCount / props.jobs.length) * 100).toFixed(0)}%`; }); -// scopedKey identifies a job within its reusable-workflow call scope so that the same -// JobID in different reusable calls does not collide. -function scopedKey(job: {parentJobID: number; jobId: string}): string { - return `${job.parentJobID || 0}:${job.jobId}`; -} - -function buildDirectNeedsMap(jobs: ActionsJob[]): Map { - // The map keys/values are scoped keys, not bare jobIds, so we keep edge construction - // accurate when reusable workflows reuse common job names like "build" / "test". - const directNeedsByScopedKey = new Map(); - const dependentsByScopedKey = new Map>(); - - for (const job of jobs) { - const fromKey = scopedKey(job); - const needKeys = (job.needs || []).map((n) => `${job.parentJobID || 0}:${n}`); - directNeedsByScopedKey.set(fromKey, needKeys); - - for (const needKey of needKeys) { - if (!dependentsByScopedKey.has(needKey)) { - dependentsByScopedKey.set(needKey, new Set()); - } - dependentsByScopedKey.get(needKey)!.add(fromKey); - } - } - - const reachabilityCache = new Map(); - - function canReach(fromKey: string, toKey: string): boolean { - const cacheKey = `${fromKey}->${toKey}`; - if (reachabilityCache.has(cacheKey)) { - return reachabilityCache.get(cacheKey)!; - } - - const visited = new Set(); - const stack = [...(dependentsByScopedKey.get(fromKey) || [])]; - - while (stack.length > 0) { - const current = stack.pop()!; - if (current === toKey) { - reachabilityCache.set(cacheKey, true); - return true; - } - if (visited.has(current)) continue; - visited.add(current); - stack.push(...(dependentsByScopedKey.get(current) || [])); - } - - reachabilityCache.set(cacheKey, false); - return false; - } - - const reducedNeedsByScopedKey = new Map(); - for (const [fromKey, needs] of directNeedsByScopedKey.entries()) { - reducedNeedsByScopedKey.set(fromKey, needs.filter((need) => { - return !needs.some((otherNeed) => otherNeed !== need && canReach(need, otherNeed)); - })); - } - - return reducedNeedsByScopedKey; -} - -const directNeedsByScopedKey = computed(() => buildDirectNeedsMap(props.jobs)); - -const edges = computed(() => { - const edgesList: Edge[] = []; - // Store every job per scoped key, not just one: matrix-expanded jobs share same jobId - const jobsByScopedKey = new Map(); - - for (const job of props.jobs) { - const key = scopedKey(job); - const existing = jobsByScopedKey.get(key); - if (existing) { - existing.push(job); - } else { - jobsByScopedKey.set(key, [job]); - } - } - - for (const job of props.jobs) { - for (const needKey of directNeedsByScopedKey.value.get(scopedKey(job)) || []) { - for (const upstreamJob of jobsByScopedKey.get(needKey) || []) { - edgesList.push({ - fromId: upstreamJob.id, - toId: job.id, - key: `${upstreamJob.id}-${job.id}`, - }); - } - } - } - - return edgesList; -}); - -function buildRoundedConnectorPath(startX: number, startY: number, endX: number, endY: number, turnX: number): string { - const deltaY = endY - startY; - if (Math.abs(deltaY) < 1) { - return `M ${startX} ${startY} H ${endX}`; - } - - const direction = deltaY > 0 ? 1 : -1; - const elbowSize = Math.max(8, Math.min(24, Math.abs(deltaY) / 2, Math.abs(endX - startX) / 2)); - const controlOffset = elbowSize / 2; - const clampedTurnX = Math.min(Math.max(turnX, startX + elbowSize), endX - elbowSize); - - return [ - `M ${startX} ${startY}`, - `H ${clampedTurnX - elbowSize}`, - `C ${clampedTurnX - controlOffset} ${startY} ${clampedTurnX} ${startY + direction * controlOffset} ${clampedTurnX} ${startY + direction * elbowSize}`, - `V ${endY - direction * elbowSize}`, - `C ${clampedTurnX} ${endY - direction * controlOffset} ${clampedTurnX + controlOffset} ${endY} ${clampedTurnX + elbowSize} ${endY}`, - `H ${endX}`, - ].join(' '); -} - -const routedEdges = computed(() => { - const nodesById = new Map(jobsWithLayout.value.map((job) => [job.id, job])); - const outgoingEdges = new Map(); - const incomingEdges = new Map(); - - for (const edge of edges.value) { - if (!outgoingEdges.has(edge.fromId)) { - outgoingEdges.set(edge.fromId, []); - } - outgoingEdges.get(edge.fromId)!.push(edge); - - if (!incomingEdges.has(edge.toId)) { - incomingEdges.set(edge.toId, []); - } - incomingEdges.get(edge.toId)!.push(edge); - } - - for (const sourceEdges of outgoingEdges.values()) { - sourceEdges.sort((a, b) => { - const targetA = nodesById.get(a.toId); - const targetB = nodesById.get(b.toId); - if (!targetA || !targetB) return 0; - return targetA.y - targetB.y || a.toId - b.toId; - }); - } - - const edgePaths: RoutedEdge[] = []; - - for (const edge of edges.value) { - const fromNode = nodesById.get(edge.fromId); - const toNode = nodesById.get(edge.toId); - if (!fromNode || !toNode) continue; - - const startX = fromNode.x + nodeWidth.value; - const startY = fromNode.y + nodeHeight / 2; - const endX = toNode.x; - const endY = toNode.y + nodeHeight / 2; - const sourceEdges = outgoingEdges.get(edge.fromId) || []; - const targetEdges = incomingEdges.get(edge.toId) || []; - const horizontalGap = endX - startX; - const turnOffset = Math.min(28, Math.max(16, horizontalGap * 0.14)); - const sourceTurnX = startX + turnOffset; - const targetTurnX = endX - turnOffset; - - let turnX = startX + horizontalGap / 2; - if (sourceEdges.length > 1) { - turnX = sourceTurnX; - } else if (targetEdges.length > 1) { - turnX = targetTurnX; - } - - const path = buildRoundedConnectorPath(startX, startY, endX, endY, turnX); - - edgePaths.push({ - ...edge, - path, - fromNode, - toNode, - }); - } - - return edgePaths; -}); - -const graphMetrics = computed(() => { - const successCount = jobsWithLayout.value.filter(job => job.status === 'success').length; - - const levels = new Map(); - jobsWithLayout.value.forEach(job => { - const count = levels.get(job.level) || 0; - levels.set(job.level, count + 1); - }) - const parallelism = Math.max(...Array.from(levels.values()), 0); - - return { - successRate: `${((successCount / jobsWithLayout.value.length) * 100).toFixed(0)}%`, - parallelism, - }; -}) - const graphStats = computed(() => [ trN(props.jobs.length, props.locale.graphJobsCount1, props.locale.graphJobsCountN), trN(edges.value.length, props.locale.graphDependenciesCount1, props.locale.graphDependenciesCountN), - props.locale.graphSuccessRate.replace('%s', graphMetrics.value.successRate), -].join(' • ')) - -const nodeHeight = 52; -const verticalSpacing = 90; -const margin = 40; + props.locale.graphSuccessRate.replace('%s', successRateLabel.value), +].join(' • ')); const minScale = 0.3; const maxScale = 1; @@ -398,42 +143,32 @@ function resetView() { function handleMouseDown(e: MouseEvent) { if (!isPlainClick(e)) return; - - // don't start drag on interactive/text elements inside the SVG const target = e.target as Element; const interactive = target.closest('div, p, a, span, button, input, text, .job-node-group'); if (interactive?.closest('svg')) return; e.preventDefault(); - isDragging.value = true; lastMousePos.value = {x: e.clientX, y: e.clientY}; - graphContainer.value!.style.cursor = 'grabbing'; + if (graphContainer.value) graphContainer.value.style.cursor = 'grabbing'; } function handleMouseMoveOnDocument(event: MouseEvent) { if (!isDragging.value) return; - const dx = event.clientX - lastMousePos.value.x; - const dy = event.clientY - lastMousePos.value.y; - - translateX.value += dx; - translateY.value += dy; - + translateX.value += event.clientX - lastMousePos.value.x; + translateY.value += event.clientY - lastMousePos.value.y; lastMousePos.value = {x: event.clientX, y: event.clientY}; } function handleMouseUpOnDocument() { if (!isDragging.value) return; isDragging.value = false; - graphContainer.value!.style.cursor = 'grab'; + if (graphContainer.value) graphContainer.value.style.cursor = 'grab'; } function handleWheel(event: WheelEvent) { - // Without a modifier, let the wheel scroll the page - if (!event.ctrlKey && !event.metaKey) { - return; - } + if (!event.ctrlKey && !event.metaKey) return; event.preventDefault(); const zoomFactor = Math.exp(-event.deltaY * 0.0015); zoomTo(scale.value * zoomFactor); @@ -442,8 +177,6 @@ function handleWheel(event: WheelEvent) { onMounted(() => { loadSavedState(); watch([translateX, translateY, scale], debounce(500, saveState)); - watch([scale], debounce(100, saveState)); - document.addEventListener('mousemove', handleMouseMoveOnDocument); document.addEventListener('mouseup', handleMouseUpOnDocument); }); @@ -453,106 +186,40 @@ onUnmounted(() => { document.removeEventListener('mouseup', handleMouseUpOnDocument); }); -function handleNodeMouseEnter(job: JobNode) { - hoveredJobId.value = job.id; +function handleNodeMouseEnter(id: string) { + hoveredGraphId.value = id; } function handleNodeMouseLeave() { - hoveredJobId.value = null; + hoveredGraphId.value = null; +} + +const highlightState = computed(() => computeGraphHighlightState(hoveredGraphId.value, graphModel.value.adjacency)); + +function isNodeHighlighted(nodeId: string): boolean { + return highlightState.value.nodeIds.has(nodeId); } function isEdgeHighlighted(edge: RoutedEdge): boolean { - if (!hoveredJobId.value) { - return false; - } - return edge.fromId === hoveredJobId.value || edge.toId === hoveredJobId.value; + return highlightState.value.edgeKeys.has(edge.key); } -const nodesWithIncomingEdge = computed(() => { - const set = new Set(); - for (const edge of routedEdges.value) set.add(edge.toId); - return set; +const splitRoutedEdges = computed(() => { + const highlighted: RoutedEdge[] = []; + const dimmed: RoutedEdge[] = []; + for (const edge of routedEdges.value) (isEdgeHighlighted(edge) ? highlighted : dimmed).push(edge); + return {highlighted, dimmed}; }); -const nodesWithOutgoingEdge = computed(() => { - const set = new Set(); - for (const edge of routedEdges.value) set.add(edge.fromId); - return set; -}); +const nodesWithIncomingEdge = computed(() => new Set(graphModel.value.adjacency.incomingByNodeId.keys())); +const nodesWithOutgoingEdge = computed(() => new Set(graphModel.value.adjacency.outgoingByNodeId.keys())); - -function computeJobLevels(jobs: ActionsJob[]): Map { - // Scope-aware: each job is keyed by `${parentJobID}:${jobId}` so the same JobID - // in different reusable workflow calls does not cross-link in the level graph. - const jobMap = new Map(); - jobs.forEach(job => { - jobMap.set(scopedKey(job), job); - }); - - const levels = new Map(); - const visited = new Set(); - const recursionStack = new Set(); - const MAX_DEPTH = 100; - - function dfs(scoped: string, depth: number = 0): number { - if (depth > MAX_DEPTH) { - console.error(`Max recursion depth (${MAX_DEPTH}) reached for: ${scoped}`); - return 0; - } - - if (recursionStack.has(scoped)) { - console.error(`Cycle detected involving: ${scoped}`); - return 0; - } - - if (visited.has(scoped)) { - return levels.get(scoped) || 0; - } - - recursionStack.add(scoped); - visited.add(scoped); - - const job = jobMap.get(scoped); - if (!job) { - recursionStack.delete(scoped); - return 0; - } - - if (!job.needs?.length) { - levels.set(scoped, 0); - recursionStack.delete(scoped); - return 0; - } - - let maxLevel = -1; - for (const need of job.needs) { - const needScoped = `${job.parentJobID || 0}:${need}`; - const needJob = jobMap.get(needScoped); - if (!needJob) continue; - - const needLevel = dfs(needScoped, depth + 1); - maxLevel = Math.max(maxLevel, needLevel); - } - - const level = maxLevel + 1; - levels.set(scoped, level); - - recursionStack.delete(scoped); - return level; - } - - jobs.forEach(job => { - const sk = scopedKey(job); - if (!visited.has(sk)) { - dfs(sk); - } - }); - - return levels; -} - -function onNodeClick(job: JobNode, event: MouseEvent) { - const link = `${props.runLink}/jobs/${job.id}`; +function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) { + const target = 'jobs' in job ? job.jobs[0]! : job; + // Reusable callers have no per-job detail page; clicking them is a no-op so the graph + // doesn't lead users to a dead destination. + if (target.isReusableCaller) return; + const link = `${props.runLink}/jobs/${target.id}`; if (event.ctrlKey || event.metaKey) { window.open(link, '_blank'); return; @@ -562,7 +229,7 @@ function onNodeClick(job: JobNode, event: MouseEvent) { + + + + +
    @@ -688,6 +419,7 @@ function onNodeClick(job: JobNode, event: MouseEvent) { display: flex; flex-direction: column; } + .graph-header { display: flex; justify-content: space-between; @@ -719,7 +451,7 @@ function onNodeClick(job: JobNode, event: MouseEvent) { .graph-container { flex: 1; - overflow: hidden; + overflow: auto; padding: 10px 14px 18px; border-radius: 0 0 var(--border-radius) var(--border-radius); cursor: grab; @@ -737,86 +469,210 @@ function onNodeClick(job: JobNode, event: MouseEvent) { } .graph-svg path { - transition: all 0.2s ease; + transition: stroke-width 0.2s ease, opacity 0.2s ease; stroke-linecap: round; stroke-linejoin: round; } +.node-edge { + stroke: var(--color-secondary-dark-2); + stroke-width: 1.5; + opacity: 0.9; +} + .highlighted-edge { - stroke-width: 2 !important; - stroke: var(--color-workflow-edge-hover) !important; + stroke: var(--color-primary); + stroke-width: 2; } .job-node-group { cursor: pointer; - transition: all 0.2s ease; + transition: opacity 0.15s ease; } -.job-node-group:hover .job-rect { - /* due to SVG rendering limitation, only one of fill and drop-shadow can work */ - fill: var(--color-hover); - /* filter: drop-shadow(0 1px 3px var(--color-shadow-opaque)); */ +.job-node-group.caller-node { + cursor: default; } -.job-text-wrap { +.job-node-group:hover .job-rect, +.job-node-group.related-node .job-rect { + stroke: var(--color-primary); + stroke-width: 1.5; + fill: var(--color-primary-alpha-10); +} + +.graph-svg.has-hover .job-node-group:not(.related-node) { + opacity: 0.2; +} + +.graph-svg.has-hover .node-edge:not(.highlighted-edge) { + opacity: 0.15; +} + +.highlighted-edge-layer { + pointer-events: none; +} + +.highlighted-port { + fill: var(--color-primary); + stroke: var(--color-primary); +} + +.job-rect { + fill: var(--color-box-body); + stroke: var(--color-secondary); + stroke-width: 1; +} + +.matrix-foreign-object { + pointer-events: auto; + overflow: visible; +} + +.matrix-panel, +.grouped-panel { + width: 100%; + height: 100%; + box-sizing: border-box; + border-radius: 6px; + background: transparent; + pointer-events: auto; + user-select: none; +} + +.matrix-panel { + display: flex; + flex-direction: column; + padding: 6px 10px 8px; +} + +.matrix-panel-label { + font-size: 10px; + font-weight: var(--font-weight-medium); + color: var(--color-text-light-2); + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; +} + +.matrix-panel-collapsed { + display: flex; + flex-direction: column; + gap: 2px; + padding: 2px 0 0 2px; + cursor: pointer; +} + +.matrix-panel-summary-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.matrix-panel-summary { + font-size: 12px; + font-weight: var(--font-weight-semibold); + line-height: 1.3; + color: var(--color-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.matrix-panel-toggle { + font-size: 11px; + color: var(--color-text-light-2); + padding-left: 24px; + cursor: pointer; +} + +.matrix-panel-toggle:hover { + color: var(--color-primary); + text-decoration: underline; +} + +.matrix-panel-jobs { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0 0 2px; + overflow-y: auto; +} + +.grouped-panel { + display: flex; + flex-direction: column; + justify-content: center; + padding: 6px; + gap: 2px; +} + +.graph-list-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 24px; + padding: 1px 6px; + border-radius: 5px; +} + +.graph-list-row:hover { + background: var(--color-hover); +} + +.graph-list-row-main, +.job-row-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.graph-list-row-name, +.job-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} + +.graph-list-row-duration, +.job-duration { + flex: 0 0 auto; + font-size: 10px; + color: var(--color-text-light-2); + white-space: nowrap; +} + +.job-row { width: 100%; height: 100%; display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - gap: 1px; - padding: 4px 8px 4px 0; - overflow: hidden; -} - -.job-name { - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: var(--font-weight-semibold); - color: var(--color-text); - user-select: none; - pointer-events: none; -} - -.job-duration { - font-size: 10px; - line-height: 1.2; - color: var(--color-text-light-2); - white-space: nowrap; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - user-select: none; - pointer-events: none; -} - -.job-status-fg-obj, -.job-status-icon-wrap { - pointer-events: none; -} - -.job-status-icon-wrap { - width: 20px; - height: 20px; - display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + gap: 8px; +} + +.job-card { + border-radius: 6px; + padding: 0 2px; } .node-port { - fill: var(--color-box-body); - stroke: var(--color-light-border); + fill: var(--color-secondary-dark-2); + stroke: var(--color-box-body); stroke-width: 1.25; - opacity: 0.85; + opacity: 0.9; pointer-events: none; } -.node-edge { - transition: stroke-width 0.2s ease, opacity 0.2s ease; - opacity: 0.75; +.job-node-group.related-node .node-port { + fill: var(--color-primary); } From 6dcae57b54c4e8197185c4675a09cfc8acf58bac Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 7 Jun 2026 22:40:35 -0700 Subject: [PATCH 30/88] chore(deps): update action dependencies (#38027) --- .github/workflows/cache-seeder.yml | 4 ++-- .github/workflows/cron-licenses.yml | 2 +- .github/workflows/cron-renovate.yml | 2 +- .github/workflows/cron-translations.yml | 2 +- .github/workflows/files-changed.yml | 2 +- .github/workflows/pull-compliance.yml | 10 +++++----- .github/workflows/pull-db-tests.yml | 12 ++++++------ .github/workflows/pull-docker-dryrun.yml | 6 +++--- .github/workflows/pull-e2e-tests.yml | 2 +- .github/workflows/pull-labeler.yml | 2 +- .github/workflows/release-nightly-snapcraft.yml | 2 +- .github/workflows/release-nightly.yml | 6 +++--- .github/workflows/release-tag-rc.yml | 6 +++--- .github/workflows/release-tag-version.yml | 6 +++--- 14 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml index 8ec7adee07d..4e2988adb4e 100644 --- a/.github/workflows/cache-seeder.yml +++ b/.github/workflows/cache-seeder.yml @@ -29,7 +29,7 @@ jobs: gobuild: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: TAGS="bindata" make backend @@ -59,7 +59,7 @@ jobs: include: - { tags: "bindata", target: "lint-backend" } steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup with: lint-cache: "true" diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index edb6f2e1576..2d4e9262883 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version-file: go.mod diff --git a/.github/workflows/cron-renovate.yml b/.github/workflows/cron-renovate.yml index a50af530f20..4db83a336dd 100644 --- a/.github/workflows/cron-renovate.yml +++ b/.github/workflows/cron-renovate.yml @@ -20,7 +20,7 @@ jobs: if: github.repository == 'go-gitea/gitea' # prevent running on forks timeout-minutes: 30 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: renovatebot/github-action@693b9ef15eec82123529a37c782242f091365961 # v46.1.14 with: renovate-version: ${{ env.RENOVATE_VERSION }} diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 17f29d4e0c5..7c215b2c176 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -12,7 +12,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 with: upload_sources: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index c17afbca97f..3c0603974e4 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -49,7 +49,7 @@ jobs: e2e: ${{ steps.changes.outputs.e2e }} shell: ${{ steps.changes.outputs.shell }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 id: changes with: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 801966e1444..6c41b6b4c11 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -19,7 +19,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup with: lint-cache: "true" @@ -31,7 +31,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup with: cache: "false" @@ -62,7 +62,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - run: make deps-backend deps-tools - run: make --always-make checks-backend # ensure the "go-licenses" make target runs @@ -72,7 +72,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/node-setup - run: make deps-frontend - run: make lint-frontend @@ -85,7 +85,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - run: make deps-backend generate-go # no frontend build here as backend should be able to build, even without any frontend files diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index cbf86247ce1..4cc8d25bbb9 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -42,7 +42,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - uses: ./.github/actions/pgsql-shard with: @@ -78,7 +78,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - uses: ./.github/actions/pgsql-shard with: @@ -90,7 +90,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - run: make deps-backend - run: make backend @@ -151,7 +151,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts' @@ -208,7 +208,7 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts' @@ -241,7 +241,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - name: Add hosts to /etc/hosts run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts' diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 43a4f48669d..f7483132b5b 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -21,7 +21,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/docker-dryrun with: platform: linux/amd64 @@ -31,7 +31,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/docker-dryrun with: platform: linux/arm64 @@ -41,7 +41,7 @@ jobs: needs: [files-changed] runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/docker-dryrun with: platform: linux/riscv64 diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index f0283f40227..bcd5eba381e 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -19,7 +19,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./.github/actions/go-setup - uses: ./.github/actions/node-setup - run: make deps-frontend diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml index 34395c8d9e3..dd190551624 100644 --- a/.github/workflows/pull-labeler.yml +++ b/.github/workflows/pull-labeler.yml @@ -30,7 +30,7 @@ jobs: pull-requests: write steps: # Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here. - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.base.sha }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/release-nightly-snapcraft.yml b/.github/workflows/release-nightly-snapcraft.yml index 0f9ac1d423b..46ea663f838 100644 --- a/.github/workflows/release-nightly-snapcraft.yml +++ b/.github/workflows/release-nightly-snapcraft.yml @@ -17,7 +17,7 @@ jobs: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Install snapcraft run: sudo snap install snapcraft --classic diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index d1561329431..70251bb0910 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -14,7 +14,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -57,7 +57,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -75,7 +75,7 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 3e7655027c4..34ed45b281f 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -15,7 +15,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -58,7 +58,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -86,7 +86,7 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 66a2984def3..394c524b755 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -18,7 +18,7 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -61,7 +61,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2 + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -89,7 +89,7 @@ jobs: contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force From 1e9ea9c8f54c25bcaf34f8a14a0e00fac5c99a2d Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 7 Jun 2026 23:03:55 -0700 Subject: [PATCH 31/88] fix(deps): update npm dependencies (#38029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@primer/octicons](https://primer.style/octicons) ([source](https://redirect.github.com/primer/octicons)) | [`19.27.0` → `19.28.0`](https://renovatebot.com/diffs/npm/@primer%2focticons/19.27.0/19.28.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@primer%2focticons/19.28.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@primer%2focticons/19.27.0/19.28.0?slim=true) | | [@typescript-eslint/parser](https://typescript-eslint.io/packages/parser) ([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser)) | [`8.60.0` → `8.60.1`](https://renovatebot.com/diffs/npm/@typescript-eslint%2fparser/8.60.0/8.60.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2fparser/8.60.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2fparser/8.60.0/8.60.1?slim=true) | | [@vitest/eslint-plugin](https://redirect.github.com/vitest-dev/eslint-plugin-vitest) | [`1.6.18` → `1.6.19`](https://renovatebot.com/diffs/npm/@vitest%2feslint-plugin/1.6.18/1.6.19) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2feslint-plugin/1.6.19?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2feslint-plugin/1.6.18/1.6.19?slim=true) | | [eslint](https://eslint.org) ([source](https://redirect.github.com/eslint/eslint)) | [`10.4.0` → `10.4.1`](https://renovatebot.com/diffs/npm/eslint/10.4.0/10.4.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/10.4.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/10.4.0/10.4.1?slim=true) | | [eslint-import-resolver-typescript](https://redirect.github.com/import-js/eslint-import-resolver-typescript) | [`4.4.4` → `4.4.5`](https://renovatebot.com/diffs/npm/eslint-import-resolver-typescript/4.4.4/4.4.5) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-import-resolver-typescript/4.4.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-import-resolver-typescript/4.4.4/4.4.5?slim=true) | | [eslint-plugin-vue-scoped-css](https://future-architect.github.io/eslint-plugin-vue-scoped-css/) ([source](https://redirect.github.com/future-architect/eslint-plugin-vue-scoped-css)) | [`3.1.0` → `3.1.1`](https://renovatebot.com/diffs/npm/eslint-plugin-vue-scoped-css/3.1.0/3.1.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-vue-scoped-css/3.1.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-vue-scoped-css/3.1.0/3.1.1?slim=true) | | [js-yaml](https://redirect.github.com/nodeca/js-yaml) | [`4.1.1` → `4.2.0`](https://renovatebot.com/diffs/npm/js-yaml/4.1.1/4.2.0) | ![age](https://developer.mend.io/api/mc/badges/age/npm/js-yaml/4.2.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/js-yaml/4.1.1/4.2.0?slim=true) | | [pnpm](https://pnpm.io) ([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) | [`11.4.0` → `11.5.1`](https://renovatebot.com/diffs/npm/pnpm/11.4.0/11.5.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/11.5.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/11.4.0/11.5.1?slim=true) | | [rolldown-license-plugin](https://redirect.github.com/silverwind/rolldown-license-plugin) | [`3.0.8` → `3.0.9`](https://renovatebot.com/diffs/npm/rolldown-license-plugin/3.0.8/3.0.9) | ![age](https://developer.mend.io/api/mc/badges/age/npm/rolldown-license-plugin/3.0.9?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/rolldown-license-plugin/3.0.8/3.0.9?slim=true) | | [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) ([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint)) | [`8.60.0` → `8.60.1`](https://renovatebot.com/diffs/npm/typescript-eslint/8.60.0/8.60.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.60.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.60.0/8.60.1?slim=true) | | [updates](https://redirect.github.com/silverwind/updates) | [`17.17.2` → `17.17.3`](https://renovatebot.com/diffs/npm/updates/17.17.2/17.17.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/updates/17.17.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/updates/17.17.2/17.17.3?slim=true) | | [vite](https://vite.dev) ([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite)) | [`8.0.14` → `8.0.16`](https://renovatebot.com/diffs/npm/vite/8.0.14/8.0.16) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vite/8.0.16?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/8.0.14/8.0.16?slim=true) | | [vitest](https://vitest.dev) ([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`4.1.7` → `4.1.8`](https://renovatebot.com/diffs/npm/vitest/4.1.7/4.1.8) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/4.1.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/4.1.7/4.1.8?slim=true) | | [vue-tsc](https://redirect.github.com/vuejs/language-tools) ([source](https://redirect.github.com/vuejs/language-tools/tree/HEAD/packages/tsc)) | [`3.3.2` → `3.3.3`](https://renovatebot.com/diffs/npm/vue-tsc/3.3.2/3.3.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/vue-tsc/3.3.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-tsc/3.3.2/3.3.3?slim=true) | --- ### Release Notes
    primer/octicons (@​primer/octicons) ### [`v19.28.0`](https://redirect.github.com/primer/octicons/blob/HEAD/CHANGELOG.md#19280) [Compare Source](https://redirect.github.com/primer/octicons/compare/v19.27.0...v19.28.0) ##### Minor Changes - [#​1208](https://redirect.github.com/primer/octicons/pull/1208) [`eddab3ff`](https://redirect.github.com/primer/octicons/commit/eddab3ff19f1450eb1d60c78b1d20c2c4bc3fd15) Thanks [@​dylanatsmith](https://redirect.github.com/dylanatsmith)! - Fix vscode icon: update 16px, add 24px, remove 32px and 48px
    typescript-eslint/typescript-eslint (@​typescript-eslint/parser) ### [`v8.60.1`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/parser/CHANGELOG.md#8601-2026-06-01) [Compare Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.60.0...v8.60.1) This was a version bump only for parser to align it with other projects, there were no code changes. See [GitHub Releases](https://redirect.github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.1) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
    vitest-dev/eslint-plugin-vitest (@​vitest/eslint-plugin) ### [`v1.6.19`](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/releases/tag/v1.6.19) [Compare Source](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.18...v1.6.19) *No significant changes* #####     [View changes on GitHub](https://redirect.github.com/vitest-dev/eslint-plugin-vitest/compare/v1.6.18...v1.6.19)
    eslint/eslint (eslint) ### [`v10.4.1`](https://redirect.github.com/eslint/eslint/releases/tag/v10.4.1) [Compare Source](https://redirect.github.com/eslint/eslint/compare/v10.4.0...v10.4.1) #### Bug Fixes - [`e557467`](https://redirect.github.com/eslint/eslint/commit/e557467db7496220eebcbe2ac5ea6d38c12bb1ec) fix: update `@eslint/plugin-kit` version to 0.7.2 ([#​20930](https://redirect.github.com/eslint/eslint/issues/20930)) (Francesco Trotta) - [`d4ce898`](https://redirect.github.com/eslint/eslint/commit/d4ce898796ca22c3b96aa70d3014cb85f4bac1cd) fix: propagate failures from delegated commands ([#​20917](https://redirect.github.com/eslint/eslint/issues/20917)) (Minh Vu) - [`f4f3507`](https://redirect.github.com/eslint/eslint/commit/f4f3507460bc016b5be979c05d2969793f570cbf) fix: prefer-arrow-callback invalid autofix with newline after `async` ([#​20916](https://redirect.github.com/eslint/eslint/issues/20916)) (kuldeep kumar) - [`c5bc78b`](https://redirect.github.com/eslint/eslint/commit/c5bc78b37e08b9054a11f0cc2d81808bb24acb85) fix: false positive for reference in `finally` block ([#​20655](https://redirect.github.com/eslint/eslint/issues/20655)) (Tanuj Kanti) - [`27538c0`](https://redirect.github.com/eslint/eslint/commit/27538c01f5df4e9306f6f4ba867b2dd6307fae59) fix: add missing CodePath and CodePathSegment types ([#​20853](https://redirect.github.com/eslint/eslint/issues/20853)) (Pixel998) #### Documentation - [`61b0add`](https://redirect.github.com/eslint/eslint/commit/61b0add61ffc52665562be7bb96f526690a78b30) docs: remove deprecated rule from related rules of `max-params` ([#​20921](https://redirect.github.com/eslint/eslint/issues/20921)) (Tanuj Kanti) - [`305d5b9`](https://redirect.github.com/eslint/eslint/commit/305d5b91aeac24d36fde42f75625a8f183d4ce43) docs: remove deprecated rules from related rules section ([#​20911](https://redirect.github.com/eslint/eslint/issues/20911)) (Tanuj Kanti) - [`49b0202`](https://redirect.github.com/eslint/eslint/commit/49b0202d01918b8061720d586dffd7c68047090c) docs: fix `display: none` of ad ([#​20901](https://redirect.github.com/eslint/eslint/issues/20901)) (Tanuj Kanti) - [`9067f94`](https://redirect.github.com/eslint/eslint/commit/9067f9492ec998afc5b4f057a477ecf6ebd45e44) docs: switch build to Node.js 24 ([#​20893](https://redirect.github.com/eslint/eslint/issues/20893)) (Milos Djermanovic) - [`c91b041`](https://redirect.github.com/eslint/eslint/commit/c91b0417e3420c76807ce1fa2aea76e2de87ab86) docs: Update README (GitHub Actions Bot) - [`e349265`](https://redirect.github.com/eslint/eslint/commit/e349265cb37f3ebc837e178e48a725bb782bd870) docs: clarify semver strings in rule deprecation objects ([#​20885](https://redirect.github.com/eslint/eslint/issues/20885)) (Milos Djermanovic) #### Chores - [`b0e466b`](https://redirect.github.com/eslint/eslint/commit/b0e466b6ab47bfc7de43d8de0c315d8ee83aa584) test: add `data` property to invalid tests cases for rules ([#​20924](https://redirect.github.com/eslint/eslint/issues/20924)) (Tanuj Kanti) - [`f78838b`](https://redirect.github.com/eslint/eslint/commit/f78838bc4c86d487e1bcc7cede260c4467721c46) test: add CodePath type coverage ([#​20904](https://redirect.github.com/eslint/eslint/issues/20904)) (Pixel998) - [`1daa4bd`](https://redirect.github.com/eslint/eslint/commit/1daa4bd734b79a62e317d0394394a6b38cff49f9) chore: update `eslint-plugin-eslint-comments` test data to latest commit ([#​20922](https://redirect.github.com/eslint/eslint/issues/20922)) (Francesco Trotta) - [`002942c`](https://redirect.github.com/eslint/eslint/commit/002942ce988ea28b78e0a2f3b074081e638b552c) ci: declare contents:read on update-readme workflow ([#​20919](https://redirect.github.com/eslint/eslint/issues/20919)) (Arpit Jain) - [`64bca24`](https://redirect.github.com/eslint/eslint/commit/64bca24e7bed35bc3c864fc625cb2d89eca87d5b) chore: update ecosystem plugins ([#​20912](https://redirect.github.com/eslint/eslint/issues/20912)) (ESLint Bot) - [`6d7c832`](https://redirect.github.com/eslint/eslint/commit/6d7c832950d5e92499d88e504080661f888f8f56) chore: ignore fflate updates in renovate ([#​20908](https://redirect.github.com/eslint/eslint/issues/20908)) (Pixel998) - [`b2c8638`](https://redirect.github.com/eslint/eslint/commit/b2c86382164d87c6203b78d52068cd6a2a6ffe30) ci: bump pnpm/action-setup from 6.0.7 to 6.0.8 ([#​20889](https://redirect.github.com/eslint/eslint/issues/20889)) (dependabot\[bot]) - [`a9b8d7f`](https://redirect.github.com/eslint/eslint/commit/a9b8d7f74c50211701cfc49710fa541fd91b2aa5) chore: increase maxBuffer for ecosystem tests ([#​20881](https://redirect.github.com/eslint/eslint/issues/20881)) (sethamus) - [`b702ead`](https://redirect.github.com/eslint/eslint/commit/b702ead5e1ed7cb9f28238a454797662efb37396) chore: update ecosystem update PR settings ([#​20884](https://redirect.github.com/eslint/eslint/issues/20884)) (Pixel998) - [`507f60e`](https://redirect.github.com/eslint/eslint/commit/507f60e9a78c9a902bc8759f066ae17a1ea6cd81) chore: update ecosystem plugins ([#​20882](https://redirect.github.com/eslint/eslint/issues/20882)) (ESLint Bot) - [`92f5c5b`](https://redirect.github.com/eslint/eslint/commit/92f5c5bb6bf3a5d167c8ee53a430833410295c6d) test: add unit test for message-count ([#​20878](https://redirect.github.com/eslint/eslint/issues/20878)) (kuldeep kumar) - [`df32108`](https://redirect.github.com/eslint/eslint/commit/df321080af5758b1fa25e4b9a40e26135642dd6e) chore: add [@​eslint/markdown](https://redirect.github.com/eslint/markdown) and typescript-eslint ecosystem tests ([#​20837](https://redirect.github.com/eslint/eslint/issues/20837)) (sethamus) - [`327f91d`](https://redirect.github.com/eslint/eslint/commit/327f91d36aa49f2a50ded931d841a16374fd875f) chore: use includeIgnoreFile internally ([#​20876](https://redirect.github.com/eslint/eslint/issues/20876)) (Kirk Waiblinger) - [`f0dc4bd`](https://redirect.github.com/eslint/eslint/commit/f0dc4bd893fb3a9f44e4ddc3ad7063ffb0beacd3) chore: pin fflate\@​0.8.2 ([#​20877](https://redirect.github.com/eslint/eslint/issues/20877)) (Milos Djermanovic) - [`0f4bd25`](https://redirect.github.com/eslint/eslint/commit/0f4bd257a67a082b756de746d9e0c4842ab764ca) ci: run Discord alert for ecosystem test failures ([#​20873](https://redirect.github.com/eslint/eslint/issues/20873)) (Copilot)
    import-js/eslint-import-resolver-typescript (eslint-import-resolver-typescript) ### [`v4.4.5`](https://redirect.github.com/import-js/eslint-import-resolver-typescript/blob/HEAD/CHANGELOG.md#445) [Compare Source](https://redirect.github.com/import-js/eslint-import-resolver-typescript/compare/v4.4.4...v4.4.5) ##### Patch Changes - [#​473](https://redirect.github.com/import-js/eslint-import-resolver-typescript/pull/473) [`32c61ab`](https://redirect.github.com/import-js/eslint-import-resolver-typescript/commit/32c61abccf26bd2a2267f2e0e67d82e6f88d149a) Thanks [@​leey0818](https://redirect.github.com/leey0818)! - fix: check tsconfig matching before using resolver
    future-architect/eslint-plugin-vue-scoped-css (eslint-plugin-vue-scoped-css) ### [`v3.1.1`](https://redirect.github.com/future-architect/eslint-plugin-vue-scoped-css/blob/HEAD/CHANGELOG.md#311) [Compare Source](https://redirect.github.com/future-architect/eslint-plugin-vue-scoped-css/compare/v3.1.0...v3.1.1) ##### Patch Changes - Fix false positives in `vue-scoped-css/require-selector-used-inside` for selectors that start with ignored pseudo-classes such as `:has(...)`. ([#​496](https://redirect.github.com/future-architect/eslint-plugin-vue-scoped-css/pull/496))
    nodeca/js-yaml (js-yaml) ### [`v4.2.0`](https://redirect.github.com/nodeca/js-yaml/blob/HEAD/CHANGELOG.md#420---2026-06-01) [Compare Source](https://redirect.github.com/nodeca/js-yaml/compare/4.1.1...590dbabadd172b099c07654fab2eabec8c7a07b9) ##### Added - Added `docs/safety.md` with notes about processing untrusted YAML. - Added `maxDepth` (100) loader option. Not a problem, but gives a better exception instead of RangeError on stack overflow. - Added `maxMergeSeqLength` (20) loader option. Not a problem after `merge` fix, but an additional restriction for safety. - Added sourcemaps to `dist/` builds. ##### Changed - Stop resolving numbers with underscores as numeric scalars, [#​627](https://redirect.github.com/nodeca/js-yaml/issues/627). - Switched dev toolchains to Vite / neostandard. - Updated demo. - Reorganized tests. - `dist/` files are no longer kept in the repository. ##### Fixed - Fix parsing of properties on the first implicit block mapping key, [#​62](https://redirect.github.com/nodeca/js-yaml/issues/62). - Fix trailing whitespace handling when folding flow scalar lines, [#​307](https://redirect.github.com/nodeca/js-yaml/issues/307). - Reject top-level block scalars without content indentation, [#​280](https://redirect.github.com/nodeca/js-yaml/issues/280). - Ensure numbers survive round-trip, [#​737](https://redirect.github.com/nodeca/js-yaml/issues/737). - Fix test coverage for issue [#​221](https://redirect.github.com/nodeca/js-yaml/issues/221). - Fix flow scalar trailing whitespace folding, [#​307](https://redirect.github.com/nodeca/js-yaml/issues/307). - Fix digits in YAML named tag handles. ##### Security - Fix potential DoS via quadratic complexity in merge - deduplicate repeated elements (makes sense for malformed files > 10K).
    pnpm/pnpm (pnpm) ### [`v11.5.1`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1151) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.5.0...v11.5.1) ##### Patch Changes - Improve `pnpm audit` performance by pruning non-vulnerable lockfile subtrees and stopping path enumeration once vulnerable findings reach the path cap. - Avoid crashing when the workspace state cache is partially written or malformed. - Set `npm_config_user_agent` for root lifecycle scripts during headless installs. - Preserve the `integrity` field of a remote (non-registry) tarball dependency when its lockfile entry is rebuilt. Re-resolving such a dependency without re-fetching it (for example via `pnpm update`, or when another dependency changes) produced a resolution with no integrity — URL/tarball resolvers only learn the integrity after the tarball is downloaded — so the previously recorded integrity was dropped, making later installs fail with `ERR_PNPM_MISSING_TARBALL_INTEGRITY` [#​12067](https://redirect.github.com/pnpm/pnpm/issues/12067). - Normalize a string `repository` field into the `{ type, url }` object form when creating the publish manifest, matching npm's behavior. Some registries (e.g. Gitea/Codeberg) reject a string `repository` with a 500 Internal Server Error during `pnpm publish` [#​12099](https://redirect.github.com/pnpm/pnpm/issues/12099). - Preserve compatible optional peer versions already present in the lockfile when resolving dependencies. - Fixed inconsistent resolution of a peer dependency that is shared through a diamond. When a package peer-depends on both another package and one of that package's own peer dependencies (for example `@typescript-eslint/eslint-plugin` peer-depends on both `@typescript-eslint/parser` and `typescript`, and `@typescript-eslint/parser` peer-depends on `typescript`), pnpm no longer reuses a hoisted instance of the shared peer that was resolved against a different version [#​12079](https://redirect.github.com/pnpm/pnpm/issues/12079). ### [`v11.5.0`](https://redirect.github.com/pnpm/pnpm/blob/HEAD/pnpm/CHANGELOG.md#1150) [Compare Source](https://redirect.github.com/pnpm/pnpm/compare/v11.4.0...v11.5.0) ##### Minor Changes - Added a new `hoistingLimits` setting for `nodeLinker: hoisted` installs, mirroring yarn's `nmHoistingLimits`. It accepts `none` (the default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), or `dependencies` (hoist only up to each workspace package's direct dependencies). Originally proposed in [#​6468](https://redirect.github.com/pnpm/pnpm/pull/6468), closing [#​6457](https://redirect.github.com/pnpm/pnpm/issues/6457). - Replaced `enquirer` with `@inquirer/prompts` for all interactive prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal [#​6643](https://redirect.github.com/pnpm/pnpm/issues/6643). **User-facing changes:** - `pnpm update -i` / `pnpm update -i --latest`: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination` - `pnpm audit --fix -i`: Same scrolling fix for vulnerability selection - `pnpm approve-builds`: Interactive build approval prompts updated - `pnpm patch`: Version selection and "apply to all" prompts updated - `pnpm patch-remove`: Patch removal selection updated - `pnpm publish`: Branch confirmation prompt updated - `pnpm login`: Credential prompts updated - `pnpm run` / `pnpm exec` (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated Vim-style `j`/`k` keys still work for up/down navigation in all interactive prompts. **Internal:** The `OtpEnquirer` and `LoginEnquirer` DI interfaces changed from `{ prompt }` to `{ input }` / `{ input, password }` respectively. Plugins or custom builds that inject their own enquirer mock will need to update. - Staged publishes are now recognized in the trust scale. When a package version's registry metadata carries an `approver` field, it is treated as the strongest trust evidence (ranked above trusted publishers and provenance attestations), since staged publishes require 2FA publish approvals. This prevents false-positive trust downgrade errors when moving from a staged publish to a lower trust level [#​11887](https://redirect.github.com/pnpm/pnpm/issues/11887). ##### Patch Changes - Fix pnpm hanging during peer resolution when an aliased install pulls in transitive packages with mutual peer cycles at different depths in the dependency tree (for example, `pnpm i nuxt@npm:nuxt-nightly@5x`). Cycles whose members hit the `findHit` cache instead of running their own `calculateDepPath` are now short-circuited by sibling resolutions at the level where the cycle is detected, so the cached path promises no longer deadlock. [#​11999](https://redirect.github.com/pnpm/pnpm/issues/11999). - Fix `pnpm dist-tag add` and `pnpm dist-tag rm` against npmjs.org failing without `--otp` with `[ERR_PNPM_UNAUTHORIZED] You must be logged in to set dist-tag … "You must provide a one-time pass. Upgrade your client to npm@latest in order to use 2FA."`. pnpm now sends `npm-auth-type: web` on dist-tag writes and surfaces the resulting OTP challenge through the existing browser-based 2FA flow (the same `withOtpHandling` helper used by `pnpm publish`), so the browser opens, the user authenticates, and the dist-tag is set on retry. `--otp=` continues to work via the classic flow. - Fix `minimumReleaseAgeExclude` handling in npm resolution fast paths so excluded packages do not get pinned to stale versions. Excludes are honored consistently during `publishedBy` metadata selection and cache-mtime shortcuts. - Fix the `integrity` field being dropped from the lockfile entry of a remote (non-registry) https-tarball dependency when an unrelated package is installed afterwards. URL/tarball resolvers do not return an integrity (it is only known after the tarball is downloaded), so when such a dependency was reused from the lockfile without being re-fetched, its integrity was lost. It is now carried over from the existing resolution. With pnpm's lockfile-integrity hardening, the missing integrity made subsequent `--frozen-lockfile` installs fail with `ERR_PNPM_MISSING_TARBALL_INTEGRITY`. [#​12001](https://redirect.github.com/pnpm/pnpm/issues/12001). - Skip dependency re-resolution when `pnpm-lock.yaml` is missing but `node_modules/.pnpm/lock.yaml` exists and still satisfies the manifest. `pnpm install` now reuses the materialized snapshot to regenerate `pnpm-lock.yaml` instead of walking the registry to rebuild it from scratch, turning the cache+node\_modules variation into a near-no-op for users who deleted the lockfile but kept the install [#​11993](https://redirect.github.com/pnpm/pnpm/issues/11993). `--frozen-lockfile` still refuses to proceed when `pnpm-lock.yaml` is absent — the regenerated lockfile must be committed, so failing loudly is the correct behavior for CI.
    silverwind/rolldown-license-plugin (rolldown-license-plugin) ### [`v3.0.9`](https://redirect.github.com/silverwind/rolldown-license-plugin/releases/tag/3.0.9) [Compare Source](https://redirect.github.com/silverwind/rolldown-license-plugin/compare/3.0.8...3.0.9) - update deps (silverwind) - make: collapse patch/minor/major into one rule (silverwind) - simplify generateBundle: pair dir+raw, rename shadow, inline single-use const (silverwind) - make update a combination target, split out update-js (silverwind) - add update-actions make target (silverwind) - remove authorship attribution rule from AGENTS.md (silverwind) - docs: use defineConfig in README usage example (silverwind)
    typescript-eslint/typescript-eslint (typescript-eslint) ### [`v8.60.1`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/typescript-eslint/CHANGELOG.md#8601-2026-06-01) [Compare Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.60.0...v8.60.1) This was a version bump only for typescript-eslint to align it with other projects, there were no code changes. See [GitHub Releases](https://redirect.github.com/typescript-eslint/typescript-eslint/releases/tag/v8.60.1) for more information. You can read about our [versioning strategy](https://typescript-eslint.io/users/versioning) and [releases](https://typescript-eslint.io/users/releases) on our website.
    silverwind/updates (updates) ### [`v17.17.3`](https://redirect.github.com/silverwind/updates/releases/tag/17.17.3) [Compare Source](https://redirect.github.com/silverwind/updates/compare/17.17.2...17.17.3) - fix prerelease drop in updateVersionRange and scope regex (silverwind) - fix 1.2.x ranges, docker tag corruption, and per-file cooldown (silverwind) - fix go +incompatible, cargo inline-table, and prerelease selection (silverwind) - fix --pin range parsing, url tag deps, and -s flag docs (silverwind) - make update a combination target, split out update-js (silverwind) - add update-actions make target (silverwind) - remove authorship attribution rule from AGENTS.md (silverwind)
    vitejs/vite (vite) ### [`v8.0.16`](https://redirect.github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-8016-2026-06-01-small) [Compare Source](https://redirect.github.com/vitejs/vite/compare/v8.0.15...v8.0.16) ##### Bug Fixes - **deps:** reject UNC paths for launch-editor-middleware ([#​22571](https://redirect.github.com/vitejs/vite/issues/22571)) ([50b9512](https://redirect.github.com/vitejs/vite/commit/50b951225bbf6151eb84a3ad5a454908ab4a76c9)) - reject windows alternate paths ([#​22572](https://redirect.github.com/vitejs/vite/issues/22572)) ([dc245c7](https://redirect.github.com/vitejs/vite/commit/dc245c71e5007ea4d891a025e2d69ac96c736546)) ### [`v8.0.15`](https://redirect.github.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small-8015-2026-06-01-small) [Compare Source](https://redirect.github.com/vitejs/vite/compare/v8.0.14...v8.0.15) ##### Features - send 408 on request timeout ([#​22476](https://redirect.github.com/vitejs/vite/issues/22476)) ([c85c9ee](https://redirect.github.com/vitejs/vite/commit/c85c9eeb9aaf41f477b48b057146887bd5620797)) - update rolldown to 1.0.3 ([#​22538](https://redirect.github.com/vitejs/vite/issues/22538)) ([646dbed](https://redirect.github.com/vitejs/vite/commit/646dbedd2870f8ec48df0321177d8aa64bbd1575)) ##### Bug Fixes - capitalize error messages and remove spurious space in parse error ([#​22488](https://redirect.github.com/vitejs/vite/issues/22488)) ([85a0eff](https://redirect.github.com/vitejs/vite/commit/85a0eff1c82bbb7c99a0fe8e63704316578a40d3)) - **deps:** update all non-major dependencies ([#​22511](https://redirect.github.com/vitejs/vite/issues/22511)) ([2686d7d](https://redirect.github.com/vitejs/vite/commit/2686d7d0b722402204d3bcc687a87adea1bcf9fa)) - **dev:** fix html-proxy cache key mismatch for /@​fs/ HTML paths ([#​21762](https://redirect.github.com/vitejs/vite/issues/21762)) ([47c4213](https://redirect.github.com/vitejs/vite/commit/47c4213f134f562c41ed7c031e4788510cf7e31e)) - **glob:** error on relative glob in virtual module when no files match ([#​22497](https://redirect.github.com/vitejs/vite/issues/22497)) ([5c8e98f](https://redirect.github.com/vitejs/vite/commit/5c8e98f8b584ac5d42f0f9b8580c49792213b13c)) - **optimizer:** close the rolldown bundle when write() rejects ([#​22528](https://redirect.github.com/vitejs/vite/issues/22528)) ([e3cfb9d](https://redirect.github.com/vitejs/vite/commit/e3cfb9deecff563550fa1b8abd27656b8b292815)) - **resolve:** provide onWarn for viteResolvePlugin in JS plugin containers ([#​22509](https://redirect.github.com/vitejs/vite/issues/22509)) ([40985f1](https://redirect.github.com/vitejs/vite/commit/40985f1c09b7696e594e6c5695fbc315d2da2c83)) ##### Miscellaneous Chores - **deps:** update rolldown-related dependencies ([#​22566](https://redirect.github.com/vitejs/vite/issues/22566)) ([3052a67](https://redirect.github.com/vitejs/vite/commit/3052a67d9350f4c5076ab1c222c4a21a589cbcdd)) ##### Code Refactoring - correct logic in `collectAllModules` function ([#​22562](https://redirect.github.com/vitejs/vite/issues/22562)) ([6978a9c](https://redirect.github.com/vitejs/vite/commit/6978a9ceb942c4f5e211d52b8a1e569f8a65c80c))
    vitest-dev/vitest (vitest) ### [`v4.1.8`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v4.1.8) [Compare Source](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.7...v4.1.8) #####    🐞 Bug Fixes - **browser**: - Disable client `cdp` API when `allowWrite/allowExec: false` \[backport to v4]  -  by [@​hi-ogawa](https://redirect.github.com/hi-ogawa) and **Codex** in [#​10450](https://redirect.github.com/vitest-dev/vitest/issues/10450) [(e4067)](https://redirect.github.com/vitest-dev/vitest/commit/e4067b3b1) - Remove orphaned Playwright route when same module is mocked via multiple ids \[backport to v4]  -  by [@​toxik](https://redirect.github.com/toxik) and [@​Zelys-DFKH](https://redirect.github.com/Zelys-DFKH) in [#​10474](https://redirect.github.com/vitest-dev/vitest/issues/10474) [(675b4)](https://redirect.github.com/vitest-dev/vitest/commit/675b4343f) #####     [View changes on GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v4.1.7...v4.1.8)
    vuejs/language-tools (vue-tsc) ### [`v3.3.3`](https://redirect.github.com/vuejs/language-tools/blob/HEAD/CHANGELOG.md#333-2026-05-30) [Compare Source](https://redirect.github.com/vuejs/language-tools/compare/v3.3.2...v3.3.3) ##### vscode - **fix:** prevent grammar scopes leakage in capitalized tags ([#​6073](https://redirect.github.com/vuejs/language-tools/issues/6073)) - Thanks to [@​KazariEX](https://redirect.github.com/KazariEX)! - **fix:** preserve TS auto imports behavior in Vue files ([#​6072](https://redirect.github.com/vuejs/language-tools/issues/6072)) - Thanks to [@​KazariEX](https://redirect.github.com/KazariEX)! ##### workspace - **fix:** read PR title from env in `auto-version` workflow to prevent injection ([#​6074](https://redirect.github.com/vuejs/language-tools/issues/6074)) - Thanks to [@​arpitjain099](https://redirect.github.com/arpitjain099)!
    --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: bircni --- package.json | 28 +- pnpm-lock.yaml | 906 ++++++++++------------- public/assets/img/svg/octicon-vscode.svg | 2 +- 3 files changed, 393 insertions(+), 543 deletions(-) diff --git a/package.json b/package.json index 8a915ea795c..cc6c270a842 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "packageManager": "pnpm@11.4.0", + "packageManager": "pnpm@11.5.1", "engines": { "node": ">= 22.18.0", "pnpm": ">= 11.0.0" @@ -28,7 +28,7 @@ "@lezer/highlight": "1.2.3", "@mcaptcha/vanilla-glue": "0.1.0-rc2", "@mermaid-js/layout-elk": "0.2.1", - "@primer/octicons": "19.27.0", + "@primer/octicons": "19.28.0", "@replit/codemirror-indentation-markers": "6.5.3", "@replit/codemirror-lang-nix": "6.0.1", "@replit/codemirror-lang-svelte": "6.0.0", @@ -50,14 +50,14 @@ "esbuild": "0.28.0", "idiomorph": "0.7.4", "jquery": "4.0.0", - "js-yaml": "4.1.1", + "js-yaml": "4.2.0", "katex": "0.17.0", "mermaid": "11.15.0", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", "postcss": "8.5.15", - "rolldown-license-plugin": "3.0.8", + "rolldown-license-plugin": "3.0.9", "sortablejs": "1.15.7", "swagger-ui-dist": "5.32.6", "tailwindcss": "3.4.19", @@ -67,7 +67,7 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.14", + "vite": "8.0.16", "vite-string-plugin": "2.0.4", "vue": "3.5.35", "vue-bar-graph": "2.2.0", @@ -89,11 +89,11 @@ "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", - "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/parser": "8.60.1", "@vitejs/plugin-vue": "6.0.7", - "@vitest/eslint-plugin": "1.6.18", - "eslint": "10.4.0", - "eslint-import-resolver-typescript": "4.4.4", + "@vitest/eslint-plugin": "1.6.19", + "eslint": "10.4.1", + "eslint-import-resolver-typescript": "4.4.5", "eslint-plugin-array-func": "5.1.1", "eslint-plugin-de-morgan": "2.1.2", "eslint-plugin-github": "6.0.0", @@ -103,7 +103,7 @@ "eslint-plugin-sonarjs": "4.0.3", "eslint-plugin-unicorn": "64.0.0", "eslint-plugin-vue": "10.9.1", - "eslint-plugin-vue-scoped-css": "3.1.0", + "eslint-plugin-vue-scoped-css": "3.1.1", "eslint-plugin-wc": "3.1.0", "globals": "17.6.0", "happy-dom": "20.9.0", @@ -119,9 +119,9 @@ "stylelint-value-no-unknown-custom-properties": "6.1.1", "svgo": "4.0.1", "typescript": "6.0.3", - "typescript-eslint": "8.60.0", - "updates": "17.17.2", - "vitest": "4.1.7", - "vue-tsc": "3.3.2" + "typescript-eslint": "8.60.1", + "updates": "17.17.3", + "vitest": "4.1.8", + "vue-tsc": "3.3.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11c6ebf5add..ce5d84c9823 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: 0.2.1 version: 0.2.1(mermaid@11.15.0) '@primer/octicons': - specifier: 19.27.0 - version: 19.27.0 + specifier: 19.28.0 + version: 19.28.0 '@replit/codemirror-indentation-markers': specifier: 6.5.3 version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) @@ -94,7 +94,7 @@ importers: version: 2.6.2 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -141,8 +141,8 @@ importers: specifier: 4.0.0 version: 4.0.0 js-yaml: - specifier: 4.1.1 - version: 4.1.1 + specifier: 4.2.0 + version: 4.2.0 katex: specifier: 0.17.0 version: 0.17.0 @@ -162,8 +162,8 @@ importers: specifier: 8.5.15 version: 8.5.15 rolldown-license-plugin: - specifier: 3.0.8 - version: 3.0.8(rolldown@1.0.2) + specifier: 3.0.9 + version: 3.0.9(rolldown@1.0.3) sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -192,11 +192,11 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.14 - version: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + specifier: 8.0.16 + version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) vite-string-plugin: specifier: 2.0.4 - version: 2.0.4(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue: specifier: 3.5.35 version: 3.5.35(typescript@6.0.3) @@ -209,7 +209,7 @@ importers: devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.2 - version: 4.7.2(eslint@10.4.0(jiti@2.7.0)) + version: 4.7.2(eslint@10.4.1(jiti@2.7.0)) '@eslint/json': specifier: 1.2.0 version: 1.2.0 @@ -218,7 +218,7 @@ importers: version: 1.60.0 '@stylistic/eslint-plugin': specifier: 5.10.0 - version: 5.10.0(eslint@10.4.0(jiti@2.7.0)) + version: 5.10.0(eslint@10.4.1(jiti@2.7.0)) '@stylistic/stylelint-plugin': specifier: 5.2.0 version: 5.2.0(stylelint@17.12.0(typescript@6.0.3)) @@ -253,50 +253,50 @@ importers: specifier: 1.12.4 version: 1.12.4 '@typescript-eslint/parser': - specifier: 8.60.0 - version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': - specifier: 1.6.18 - version: 1.6.18(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) + specifier: 1.6.19 + version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) eslint: - specifier: 10.4.0 - version: 10.4.0(jiti@2.7.0) + specifier: 10.4.1 + version: 10.4.1(jiti@2.7.0) eslint-import-resolver-typescript: - specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + specifier: 4.4.5 + version: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-array-func: specifier: 5.1.1 - version: 5.1.1(eslint@10.4.0(jiti@2.7.0)) + version: 5.1.1(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-de-morgan: specifier: 2.1.2 - version: 2.1.2(eslint@10.4.0(jiti@2.7.0)) + version: 2.1.2(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-github: specifier: 6.0.0 - version: 6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) + version: 6.0.0(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-import-x: specifier: 4.16.2 - version: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + version: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-playwright: specifier: 2.10.4 - version: 2.10.4(eslint@10.4.0(jiti@2.7.0)) + version: 2.10.4(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-regexp: specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0)) + version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-sonarjs: specifier: 4.0.3 - version: 4.0.3(eslint@10.4.0(jiti@2.7.0)) + version: 4.0.3(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-unicorn: specifier: 64.0.0 - version: 64.0.0(eslint@10.4.0(jiti@2.7.0)) + version: 64.0.0(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-vue: specifier: 10.9.1 - version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) + version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) eslint-plugin-vue-scoped-css: - specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))) + specifier: 3.1.1 + version: 3.1.1(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) eslint-plugin-wc: specifier: 3.1.0 - version: 3.1.0(eslint@10.4.0(jiti@2.7.0)) + version: 3.1.0(eslint@10.4.1(jiti@2.7.0)) globals: specifier: 17.6.0 version: 17.6.0 @@ -340,17 +340,17 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.60.0 - version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + specifier: 8.60.1 + version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) updates: - specifier: 17.17.2 - version: 17.17.2 + specifier: 17.17.3 + version: 17.17.3 vitest: - specifier: 4.1.7 - version: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + specifier: 4.1.8 + version: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) vue-tsc: - specifier: 3.3.2 - version: 3.3.2(typescript@6.0.3) + specifier: 3.3.3 + version: 3.3.3(typescript@6.0.3) packages: @@ -985,8 +985,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -1003,8 +1003,8 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@primer/octicons@19.27.0': - resolution: {integrity: sha512-7xC6D89f9IcoDezeKTGETbgRAoXJnbZlakavqYzD4Wo+uTC6212k0fTE/dLV8WCDOwfp//WyONftdaFRdI1VdQ==} + '@primer/octicons@19.28.0': + resolution: {integrity: sha512-FCpW9ZXI9U9h7wjYSXFQK4Zyp1Roc/kF8nymak4bYccWaWoUixbnIr4u8UYiRoPRSglm+23TZEyUZHrgNql9Jw==} '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} @@ -1054,97 +1054,97 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1408,14 +1408,6 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.60.0': - resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.60.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/eslint-plugin@8.60.1': resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1424,52 +1416,29 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.60.0': - resolution: {integrity: sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==} + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.0': - resolution: {integrity: sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.60.1': resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.60.0': - resolution: {integrity: sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.60.1': resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.60.0': - resolution: {integrity: sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/tsconfig-utils@8.60.1': resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.60.0': - resolution: {integrity: sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.60.1': resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1477,33 +1446,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.60.0': - resolution: {integrity: sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.60.1': resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.60.0': - resolution: {integrity: sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.60.1': resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.0': - resolution: {integrity: sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.60.1': resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1511,10 +1463,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.60.0': - resolution: {integrity: sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.60.1': resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1649,8 +1597,8 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 - '@vitest/eslint-plugin@1.6.18': - resolution: {integrity: sha512-J6U4X0jH3NwTuYouvrJn6I8ypTOU+GhKEjyVwpoPnDuc23usa/xi/R0caWLBbNp3xLy3/rL1YkuJuneTMVV4Mg==} + '@vitest/eslint-plugin@1.6.19': + resolution: {integrity: sha512-zodmXRsVKFsuHxHJILuTFaaKsrsxm0YsiOX65clk+LpCW9JrVXaf6ERXr0caDs+NEk0S62Jyk0K7XYQ7gWXheA==} engines: {node: '>=18'} peerDependencies: '@typescript-eslint/eslint-plugin': '*' @@ -1665,11 +1613,11 @@ packages: vitest: optional: true - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1679,20 +1627,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1715,8 +1663,8 @@ packages: '@vue/compiler-ssr@3.5.35': resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} - '@vue/language-core@3.3.2': - resolution: {integrity: sha512-CLwjSfHlPLhjd2qhuS3tTFtnOIWHXAM5u4X1DxmzlQ8j5bmOYlKCsSusOP7jCRJnlVg0mCTQtHU3vwFvopZGoQ==} + '@vue/language-core@3.3.3': + resolution: {integrity: sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==} '@vue/reactivity@3.5.35': resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} @@ -2594,8 +2542,8 @@ packages: eslint-import-resolver-node@0.3.10: resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + eslint-import-resolver-typescript@4.4.5: + resolution: {integrity: sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==} engines: {node: ^16.17.0 || >=18.6.0} peerDependencies: eslint: '*' @@ -2737,8 +2685,8 @@ packages: peerDependencies: eslint: '>=9.38.0' - eslint-plugin-vue-scoped-css@3.1.0: - resolution: {integrity: sha512-R9XLrIZaP6QGz9b4kO2K4+lP4NcO2TKcw71zBtIYCoqqTk5ja1ySruYAllBT2LPIJVQ4NZaB2IFSvLjLEpYqQA==} + eslint-plugin-vue-scoped-css@3.1.1: + resolution: {integrity: sha512-GIskMvLPnDtiu88rWXQHy2b2QZ4j959N5UgghML64jH0sg3Km+HRa9m7nkpcEBGLD4iA4vtMDbBIoLdFcbT8lQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: '>=9.38.0' @@ -2790,8 +2738,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.4.0: - resolution: {integrity: sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==} + eslint@10.4.1: + resolution: {integrity: sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3411,6 +3359,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} @@ -4238,13 +4190,13 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown-license-plugin@3.0.8: - resolution: {integrity: sha512-q4nvGtimBIxValBXzkri+0jP2Sdf6PLztYykn7vCu0nVkJEzGsVRfcH/1X6qm0S8//4/gFt/XxwJqpaM6uSdJA==} + rolldown-license-plugin@3.0.9: + resolution: {integrity: sha512-40u0paM+f049toEj+/q8PlIQXbgkwPSqORtRJihoXr/v1V4amhVSi2uWOZSWyVp1+V/vkTyAV+Ib/fA+DeO3Ag==} peerDependencies: rolldown: '*' - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4688,8 +4640,8 @@ packages: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.60.0: - resolution: {integrity: sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==} + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4742,8 +4694,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - updates@17.17.2: - resolution: {integrity: sha512-gOwGrBYBvHVS+OiaUFRuilrmf/P8eYVmKUrkq7W3fvYUgpBsNKgfZo/CPhLKBa7xEFiEuMwMxTkiKsd9mn8pEw==} + updates@17.17.3: + resolution: {integrity: sha512-ZIhWarBUBmKG65d0AeOOMlZFonGWn6Ntol4/epga/xbQymEOh/2s07U+1UGM94y9JEPbk4CjowYgEo3F76ZxYA==} engines: {node: '>=22'} hasBin: true @@ -4781,8 +4733,8 @@ packages: peerDependencies: vite: '*' - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4824,20 +4776,20 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4883,8 +4835,8 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-tsc@3.3.2: - resolution: {integrity: sha512-n7nQoA3YWW/eiDR8jMiv/uJvlg0uLGs+YgUrsTrf9EZaYSt3tuvMZb5V8+7Mvh/EH5pnY/hoVdgfjH+XcK+wwA==} + vue-tsc@3.3.3: + resolution: {integrity: sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -5094,7 +5046,7 @@ snapshots: '@citation-js/plugin-yaml@0.6.2': dependencies: - js-yaml: 4.1.1 + js-yaml: 4.2.0 '@citation-js/plugin-zenodo@0.6.2': dependencies: @@ -5478,24 +5430,24 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.0(jiti@2.7.0))': + '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.1(jiti@2.7.0))': dependencies: escape-string-regexp: 4.0.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ignore: 7.0.5 - '@eslint-community/eslint-utils@4.9.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.7.0))': dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@1.4.1(eslint@10.4.0(jiti@2.7.0))': + '@eslint/compat@1.4.1(eslint@10.4.1(jiti@2.7.0))': dependencies: '@eslint/core': 0.17.0 optionalDependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) '@eslint/config-array@0.23.5': dependencies: @@ -5525,7 +5477,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5780,7 +5732,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.132.0': {} + '@oxc-project/types@0.133.0': {} '@package-json/types@0.0.12': {} @@ -5792,7 +5744,7 @@ snapshots: '@popperjs/core@2.11.8': {} - '@primer/octicons@19.27.0': + '@primer/octicons@19.28.0': dependencies: object-assign: 4.1.1 @@ -5838,53 +5790,53 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.2': + '@rolldown/binding-android-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@rolldown/binding-darwin-x64@1.0.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -5925,11 +5877,11 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0))': + '@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0))': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@typescript-eslint/types': 8.60.1 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 @@ -6166,15 +6118,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.60.0 - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -6182,15 +6134,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.60.0 - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 10.4.1(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -6198,77 +6150,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.60.1 - '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.60.1 - eslint: 10.4.0(jiti@2.7.0) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.60.1 - '@typescript-eslint/type-utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.60.1 - eslint: 10.4.0(jiti@2.7.0) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - optional: true - - '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.60.0 - debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.60.0 - debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.60.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - '@typescript-eslint/types': 8.60.0 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/project-service@8.60.0(typescript@6.0.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) - '@typescript-eslint/types': 8.60.0 debug: 4.4.3 + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -6291,24 +6192,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.60.0': - dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 - '@typescript-eslint/scope-manager@8.60.1': dependencies: '@typescript-eslint/types': 8.60.1 '@typescript-eslint/visitor-keys': 8.60.1 - '@typescript-eslint/tsconfig-utils@8.60.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - - '@typescript-eslint/tsconfig-utils@8.60.0(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -6317,89 +6205,32 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.60.1 '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.60.1 '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - optional: true - - '@typescript-eslint/types@8.60.0': {} '@typescript-eslint/types@8.60.1': {} - '@typescript-eslint/typescript-estree@8.60.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.60.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@5.9.3) - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.1 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@8.60.0(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.60.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.60.0(typescript@6.0.3) - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/visitor-keys': 8.60.0 - debug: 4.4.3 - minimatch: 10.2.5 - semver: 7.8.1 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) @@ -6430,55 +6261,28 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - eslint: 10.4.0(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/types': 8.60.0 - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/types': 8.60.1 '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/types': 8.60.1 '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.60.0': - dependencies: - '@typescript-eslint/types': 8.60.0 - eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.60.1': dependencies: '@typescript-eslint/types': 8.60.1 @@ -6559,62 +6363,62 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) vue: 3.5.35(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.18(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': + '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': dependencies: '@typescript-eslint/scope-manager': 8.60.1 - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 - vitest: 4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.1.7': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.7': + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@4.1.8': {} - '@vitest/utils@4.1.7': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.7 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -6660,7 +6464,7 @@ snapshots: '@vue/compiler-dom': 3.5.35 '@vue/shared': 3.5.35 - '@vue/language-core@3.3.2': + '@vue/language-core@3.3.3': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.35 @@ -7080,7 +6884,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -7613,9 +7417,9 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: @@ -7632,10 +7436,10 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)): + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) get-tsconfig: 4.14.0 is-bun-module: 2.0.0 @@ -7643,92 +7447,104 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.4.1(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.0(jiti@2.7.0)) + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-array-func@5.1.1(eslint@10.4.0(jiti@2.7.0)): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)))(eslint-plugin-import@2.32.0)(eslint@10.4.1(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + optional: true - eslint-plugin-de-morgan@2.1.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-array-func@5.1.1(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-escompat@3.11.4(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-de-morgan@2.1.2(eslint@10.4.1(jiti@2.7.0)): + dependencies: + eslint: 10.4.1(jiti@2.7.0) + + eslint-plugin-escompat@3.11.4(eslint@10.4.1(jiti@2.7.0)): dependencies: browserslist: 4.28.2 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-eslint-comments@3.2.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-eslint-comments@3.2.0(eslint@10.4.1(jiti@2.7.0)): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) ignore: 5.3.2 - eslint-plugin-filenames@1.3.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-filenames@1.3.2(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) lodash.camelcase: 4.3.0 lodash.kebabcase: 4.1.1 lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-github@6.0.0(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): dependencies: - '@eslint/compat': 1.4.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint/compat': 1.4.1(eslint@10.4.1(jiti@2.7.0)) '@eslint/eslintrc': 3.3.5 '@eslint/js': 9.39.4 '@github/browserslist-config': 1.0.0 - '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) aria-query: 5.3.2 - eslint: 10.4.0(jiti@2.7.0) - eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-escompat: 3.11.4(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-filenames: 1.3.2(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-i18n-text: 1.0.1(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.0(jiti@2.7.0)) + eslint: 10.4.1(jiti@2.7.0) + eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-escompat: 3.11.4(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-filenames: 1.3.2(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-i18n-text: 1.0.1(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-no-only-tests: 3.4.0 - eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3) eslint-rule-documentation: 1.0.23 globals: 16.5.0 jsx-ast-utils: 3.3.5 prettier: 3.8.3 svg-element-attributes: 1.3.1 typescript: 5.9.3 - typescript-eslint: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) + typescript-eslint: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - '@types/eslint' - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-i18n-text@1.0.1(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-i18n-text@1.0.1(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.4.1(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 '@typescript-eslint/types': 8.60.1 comment-parser: 1.4.7 debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-import-context: 0.1.9(unrs-resolver@1.12.2) is-glob: 4.0.3 minimatch: 10.2.5 @@ -7736,12 +7552,12 @@ snapshots: stable-hash-x: 0.2.0 unrs-resolver: 1.12.2 optionalDependencies: - '@typescript-eslint/utils': 8.60.1(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -7750,9 +7566,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.4)(eslint@10.4.0(jiti@2.7.0)) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -7764,13 +7580,43 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 10.4.1(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@4.4.5)(eslint@10.4.1(jiti@2.7.0)) + hasown: 2.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + optional: true + + eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -7780,7 +7626,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -7791,37 +7637,37 @@ snapshots: eslint-plugin-no-only-tests@3.4.0: {} - eslint-plugin-playwright@2.10.4(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-playwright@2.10.4(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) globals: 17.6.0 - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.7.0)))(eslint@10.4.1(jiti@2.7.0))(prettier@3.8.3): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) prettier: 3.8.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.4.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-regexp@3.1.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-regexp@3.1.0(eslint@10.4.1(jiti@2.7.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.7 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) jsdoc-type-pratt-parser: 7.2.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-sonarjs@4.0.3(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-sonarjs@4.0.3(eslint@10.4.1(jiti@2.7.0)): dependencies: '@eslint-community/regexpp': 4.12.2 builtin-modules: 3.3.0 bytes: 3.1.2 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) functional-red-black-tree: 1.0.1 globals: 17.6.0 jsx-ast-utils-x: 0.1.0 @@ -7832,15 +7678,15 @@ snapshots: ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 - eslint-plugin-unicorn@64.0.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-unicorn@64.0.0(eslint@10.4.1(jiti@2.7.0)): dependencies: '@babel/helper-validator-identifier': 7.29.7 - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) change-case: 5.4.4 ci-info: 4.4.0 clean-regexp: 1.0.0 core-js-compat: 3.49.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) find-up-simple: 1.0.1 globals: 17.6.0 indent-string: 5.0.0 @@ -7852,33 +7698,33 @@ snapshots: semver: 7.8.1 strip-indent: 4.1.1 - eslint-plugin-vue-scoped-css@3.1.0(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): + eslint-plugin-vue-scoped-css@3.1.1(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) es-toolkit: 1.47.0 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) postcss: 8.5.15 postcss-safe-parser: 7.0.1(postcss@8.5.15) postcss-selector-parser: 7.1.1 - vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0)) + vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.0(jiti@2.7.0)))(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0))): + eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) - eslint: 10.4.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) + eslint: 10.4.1(jiti@2.7.0) natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 semver: 7.8.1 - vue-eslint-parser: 10.4.0(eslint@10.4.0(jiti@2.7.0)) + vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: - '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) + '@stylistic/eslint-plugin': 5.10.0(eslint@10.4.1(jiti@2.7.0)) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-wc@3.1.0(eslint@10.4.0(jiti@2.7.0)): + eslint-plugin-wc@3.1.0(eslint@10.4.1(jiti@2.7.0)): dependencies: - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) is-valid-element-name: 1.0.0 js-levenshtein-esm: 2.0.0 @@ -7897,9 +7743,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.4.0(jiti@2.7.0): + eslint@10.4.1(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -8571,6 +8417,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + jsbn@0.1.1: {} jsdoc-type-pratt-parser@7.2.0: {} @@ -9490,30 +9340,30 @@ snapshots: robust-predicates@3.0.3: {} - rolldown-license-plugin@3.0.8(rolldown@1.0.2): + rolldown-license-plugin@3.0.9(rolldown@1.0.3): dependencies: - rolldown: 1.0.2 + rolldown: 1.0.3 - rolldown@1.0.2: + rolldown@1.0.3: dependencies: - '@oxc-project/types': 0.132.0 + '@oxc-project/types': 0.133.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 roughjs@4.6.6: dependencies: @@ -10060,24 +9910,24 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3): + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@5.9.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3))(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript-eslint@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.60.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.4.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.4.1(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -10140,7 +9990,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - updates@17.17.2: {} + updates@17.17.3: {} uri-js@4.4.1: dependencies: @@ -10169,16 +10019,16 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-string-plugin@2.0.4(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) - vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): + vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.2 + rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.1 @@ -10186,15 +10036,15 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.7(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.9.0)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -10206,7 +10056,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 @@ -10228,10 +10078,10 @@ snapshots: chart.js: 4.5.1 vue: 3.5.35(typescript@6.0.3) - vue-eslint-parser@10.4.0(eslint@10.4.0(jiti@2.7.0)): + vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0)): dependencies: debug: 4.4.3 - eslint: 10.4.0(jiti@2.7.0) + eslint: 10.4.1(jiti@2.7.0) eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 espree: 11.2.0 @@ -10240,10 +10090,10 @@ snapshots: transitivePeerDependencies: - supports-color - vue-tsc@3.3.2(typescript@6.0.3): + vue-tsc@3.3.3(typescript@6.0.3): dependencies: '@volar/typescript': 2.4.28 - '@vue/language-core': 3.3.2 + '@vue/language-core': 3.3.3 typescript: 6.0.3 vue@3.5.35(typescript@6.0.3): diff --git a/public/assets/img/svg/octicon-vscode.svg b/public/assets/img/svg/octicon-vscode.svg index 04ac8cacd51..81e0f7cbb07 100644 --- a/public/assets/img/svg/octicon-vscode.svg +++ b/public/assets/img/svg/octicon-vscode.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 60f66a9bfdb60414ca8470b9b9a77fe56e1f2b83 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 8 Jun 2026 00:39:06 -0600 Subject: [PATCH 32/88] enhance(actions): improve reusable workflow `uses` handling and cancellation (#37991) Follow up #37478 ## Changes 1. #37478 doesn't support absolute URL in `uses`. This PR provides partial support for URL-style reusable workflow references. A reusable workflow can now be referenced by an absolute URL, as long as it points to the local Gitea instance: ```yaml jobs: call: uses: https://your-gitea.example.com/OWNER/REPO/.gitea/workflows/ci.yaml@v1 ``` 2. Show an error message in the UI for invalid `uses`. image 3. Fix reusable caller cancellation issue. A reusable caller's status is aggregated from its children, so cancellation should processes a caller's descendants deepest-first. --------- Signed-off-by: Zettat123 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: bircni Co-authored-by: Giteabot --- models/actions/run_job.go | 21 ++++--- models/actions/run_job_test.go | 66 ++++++++++++++++++++++ options/locale/locale_en-US.json | 1 + routers/web/repo/actions/actions.go | 45 ++++++++++----- services/actions/reusable_workflow.go | 26 ++++++++- services/actions/reusable_workflow_test.go | 44 +++++++++++++++ 6 files changed, 179 insertions(+), 24 deletions(-) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index caf66ca451c..df01546fd84 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -4,6 +4,7 @@ package actions import ( + "cmp" "context" "fmt" "slices" @@ -671,18 +672,18 @@ func cancelOneJob(ctx context.Context, job *ActionRunJob) (*ActionRunJob, error) func cancelReusableCaller(ctx context.Context, caller *ActionRunJob) ([]*ActionRunJob, error) { cancelledJobs := make([]*ActionRunJob, 0) - if c, err := cancelOneJob(ctx, caller); err != nil { - return cancelledJobs, err - } else if c != nil { - cancelledJobs = append(cancelledJobs, c) - } - attemptJobs, err := GetRunJobsByRunAndAttemptID(ctx, caller.RunID, caller.RunAttemptID) if err != nil { return cancelledJobs, err } - for _, c := range CollectAllDescendantJobs(caller, attemptJobs) { + // Cancel descendants deepest-first, then the caller: a caller's status is aggregated from its children, + // so each child must reach its final state before its parent caller is re-aggregated. + // A child's ID always exceeds its parent's, so descending ID is a valid deepest-first order. + descendants := CollectAllDescendantJobs(caller, attemptJobs) + slices.SortFunc(descendants, func(a, b *ActionRunJob) int { return cmp.Compare(b.ID, a.ID) }) + + for _, c := range descendants { cancelled, err := cancelOneJob(ctx, c) if err != nil { return cancelledJobs, err @@ -691,5 +692,11 @@ func cancelReusableCaller(ctx context.Context, caller *ActionRunJob) ([]*ActionR cancelledJobs = append(cancelledJobs, cancelled) } } + + if c, err := cancelOneJob(ctx, caller); err != nil { + return cancelledJobs, err + } else if c != nil { + cancelledJobs = append(cancelledJobs, c) + } return cancelledJobs, nil } diff --git a/models/actions/run_job_test.go b/models/actions/run_job_test.go index a9e07ce0cf0..4437b5906df 100644 --- a/models/actions/run_job_test.go +++ b/models/actions/run_job_test.go @@ -131,3 +131,69 @@ func TestGetPriorAttemptChildrenByParent(t *testing.T) { assertAttempt1Children(t, out) }) } + +// A reusable caller subtree with a Blocked descendant (e.g. a nested caller stuck on an invalid `uses:`) must aggregate to Cancelled, when the run is cancelled. +func TestCancelJobs_NestedBlockedReusableCaller(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + run := &ActionRun{ + Title: "cancel-nested-caller", + RepoID: 4, + Index: 9701, + OwnerID: 1, + WorkflowID: "caller.yaml", + TriggerUserID: 1, + Ref: "refs/heads/master", + CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", + Event: "push", + TriggerEvent: "push", + EventPayload: "{}", + Status: StatusBlocked, + } + require.NoError(t, db.Insert(ctx, run)) + + attempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1, Status: StatusBlocked} + require.NoError(t, db.Insert(ctx, attempt)) + run.LatestAttemptID = attempt.ID + require.NoError(t, UpdateRun(ctx, run, "latest_attempt_id")) + + newJob := func(name string, attemptJobID, parentID int64, callUses string) *ActionRunJob { + job := &ActionRunJob{ + RunID: run.ID, + RunAttemptID: attempt.ID, + RepoID: run.RepoID, + OwnerID: run.OwnerID, + CommitSHA: run.CommitSHA, + Name: name, + JobID: name, + Attempt: 1, + Status: StatusBlocked, + AttemptJobID: attemptJobID, + IsReusableCaller: true, + CallUses: callUses, + ParentJobID: parentID, + } + require.NoError(t, db.Insert(ctx, job)) + return job + } + + // outer: a valid top-level caller that expanded; inner: a nested caller stuck Blocked (invalid uses, never expands). + outer := newJob("outer", 1, 0, "./.gitea/workflows/lib.yml") + inner := newJob("inner", 2, outer.ID, "https://other.example.com/o/r/.gitea/workflows/ci.yml@v1") + + // Cancel all jobs of the attempt, ordered by id (parent before child). + jobs, err := GetRunJobsByRunAndAttemptID(ctx, run.ID, attempt.ID) + require.NoError(t, err) + _, err = CancelJobs(ctx, jobs) + require.NoError(t, err) + + for _, j := range []*ActionRunJob{outer, inner} { + got := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: j.ID}) + assert.Equal(t, StatusCancelled, got.Status, "job %q should be cancelled", j.JobID) + } + gotAttempt := unittest.AssertExistsAndLoadBean(t, &ActionRunAttempt{ID: attempt.ID}) + assert.Equal(t, StatusCancelled, gotAttempt.Status, "attempt must aggregate to Cancelled") + gotRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: run.ID}) + assert.Equal(t, StatusCancelled, gotRun.Status, "run must aggregate to Cancelled, not stay Blocked") +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 51a97977421..9595baebed9 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3774,6 +3774,7 @@ "actions.runs.no_matching_online_runner_helper": "No matching online runner with label: %s", "actions.runs.no_job_without_needs": "The workflow must contain at least one job without dependencies.", "actions.runs.no_job": "The workflow must contain at least one job", + "actions.runs.invalid_reusable_workflow_uses": "Invalid reusable workflow \"uses\": %s", "actions.runs.actor": "Actor", "actions.runs.status": "Status", "actions.runs.actors_no_select": "All actors", diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 5ae0d14a6fc..9c5e1664de0 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -27,6 +27,7 @@ import ( "gitea.dev/modules/templates" "gitea.dev/modules/util" shared_user "gitea.dev/routers/web/shared/user" + actions_service "gitea.dev/services/actions" "gitea.dev/services/context" "gitea.dev/services/convert" @@ -208,12 +209,20 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow if !hasJobWithoutNeeds && len(j.Needs()) == 0 { hasJobWithoutNeeds = true } + if j.Uses != "" { + if _, err := actions_service.ResolveUses(ctx, j.Uses); err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error()) + break + } + } } - if !hasJobWithoutNeeds { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") - } - if emptyJobsNumber == len(wf.Jobs) { - workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + if workflow.ErrMsg == "" { + if !hasJobWithoutNeeds { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs") + } + if emptyJobsNumber == len(wf.Jobs) { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job") + } } workflows = append(workflows, workflow) } @@ -352,7 +361,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo return } for _, run := range runs { - if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) { + if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) { continue } jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID) @@ -361,23 +370,31 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWo return } for _, job := range jobs { - if !job.Status.IsWaiting() { + if !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked) { continue } if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil { runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) break } - hasOnlineRunner := false - for _, runner := range runners { - if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { - hasOnlineRunner = true + if job.CallUses != "" { + if _, err := actions_service.ResolveUses(ctx, job.CallUses); err != nil { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error()) break } } - if !hasOnlineRunner { - runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) - break + if job.Status.IsWaiting() { + hasOnlineRunner := false + for _, runner := range runners { + if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) { + hasOnlineRunner = true + break + } + } + if !hasOnlineRunner { + runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ",")) + break + } } } } diff --git a/services/actions/reusable_workflow.go b/services/actions/reusable_workflow.go index 9b5ecdef6f4..65a6acfbd03 100644 --- a/services/actions/reusable_workflow.go +++ b/services/actions/reusable_workflow.go @@ -6,6 +6,7 @@ package actions import ( "context" "fmt" + "strings" actions_model "gitea.dev/models/actions" "gitea.dev/models/db" @@ -15,7 +16,9 @@ import ( "gitea.dev/modules/actions/jobparser" "gitea.dev/modules/container" "gitea.dev/modules/gitrepo" + "gitea.dev/modules/httplib" "gitea.dev/modules/json" + "gitea.dev/modules/setting" api "gitea.dev/modules/structs" "gitea.dev/modules/util" "gitea.dev/services/convert" @@ -149,10 +152,10 @@ func expandReusableWorkflowCaller(ctx context.Context, run *actions_model.Action return fmt.Errorf("parse caller job %d: %w", caller.ID, err) } - // 3. Load called-workflow source. - ref, err := jobparser.ParseUses(parsedJob.Uses) + // 3. Resolve `uses` and load called-workflow source. + ref, err := ResolveUses(ctx, parsedJob.Uses) if err != nil { - return fmt.Errorf("parse uses %q: %w", parsedJob.Uses, err) + return fmt.Errorf("resolve uses %q: %w", parsedJob.Uses, err) } content, contentSourceRepoID, contentSourceCommitSHA, err := loadReusableWorkflowSource(ctx, run, caller, ref) if err != nil { @@ -340,3 +343,20 @@ func insertCallerChildren(ctx context.Context, run *actions_model.ActionRun, att } return nil } + +// ResolveUses normalizes and parses a reusable workflow `uses:` value. +// It first rewrites an absolute URL pointing to this instance into the cross-repo form (rejecting external URLs), +// then validates the syntax via jobparser.ParseUses. +func ResolveUses(ctx context.Context, uses string) (*jobparser.UsesRef, error) { + // Rewrite a local-instance URL to the equivalent cross-repo form "owner/repo/.gitea/workflows/file.yml@ref". + if strings.HasPrefix(uses, "http://") || strings.HasPrefix(uses, "https://") { + // ParseGiteaSiteURL returns nil for URLs that do not belong to this instance. + gsu := httplib.ParseGiteaSiteURL(ctx, uses) + if gsu == nil { + return nil, fmt.Errorf("unsupported reusable workflow URL %q: an absolute URL must point to this Gitea instance (%s)", uses, setting.AppURL) + } + // RoutePath is the instance-relative path (AppSubURL already stripped), e.g. "/owner/repo/.gitea/workflows/file.yml@ref". + uses = strings.TrimPrefix(gsu.RoutePath, "/") + } + return jobparser.ParseUses(uses) +} diff --git a/services/actions/reusable_workflow_test.go b/services/actions/reusable_workflow_test.go index cadb26a851b..a7bb41ba8ae 100644 --- a/services/actions/reusable_workflow_test.go +++ b/services/actions/reusable_workflow_test.go @@ -10,6 +10,9 @@ import ( actions_model "gitea.dev/models/actions" "gitea.dev/models/db" "gitea.dev/models/unittest" + "gitea.dev/modules/actions/jobparser" + "gitea.dev/modules/setting" + "gitea.dev/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -132,3 +135,44 @@ func buildCallerChain(t *testing.T, callerUses ...string) []*actions_model.Actio } return jobs } + +func TestResolveUses(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")() + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + ctx := t.Context() + + t.Run("LocalForms", func(t *testing.T) { + // Same-repo and cross-repo forms are not URLs and are parsed as-is. + ref, err := ResolveUses(ctx, "./.gitea/workflows/build.yml") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalSameRepo, Path: ".gitea/workflows/build.yml"}, *ref) + + ref, err = ResolveUses(ctx, "owner/repo/.gitea/workflows/build.yml@v1") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/build.yml", Ref: "v1"}, *ref) + }) + + t.Run("LocalInstanceURL", func(t *testing.T) { + // An absolute URL on this instance (incl. AppSubURL) resolves to the equivalent cross-repo ref. + ref, err := ResolveUses(ctx, "https://gitea.example.com/sub/owner/repo/.gitea/workflows/ci.yml@refs/heads/main") + require.NoError(t, err) + assert.Equal(t, jobparser.UsesRef{Kind: jobparser.UsesKindLocalCrossRepo, Owner: "owner", Repo: "repo", Path: ".gitea/workflows/ci.yml", Ref: "refs/heads/main"}, *ref) + }) + + t.Run("InvalidSyntax", func(t *testing.T) { + for _, in := range []string{ + "owner/.gitea/workflows/foo.yml", // missing repo segment + "owner/repo/.gitea/workflows/foo.yml", // missing @ref + "https://gitea.example.com/sub/repo/.gitea/workflows/ci.yml@refs/heads/main", // local absolute URL but missing owner + "not a valid uses at all", + } { + _, err := ResolveUses(ctx, in) + require.Error(t, err, "in = %s", in) + } + }) + + t.Run("ForeignURL", func(t *testing.T) { + _, err := ResolveUses(ctx, "https://other.gitea-example.com/owner/repo/.gitea/workflows/ci.yaml@v1") + assert.ErrorContains(t, err, "must point to this Gitea instance") + }) +} From 136f7d18aa0ac9fd16aa05bd6fd95589076f7d58 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 8 Jun 2026 16:58:42 +0800 Subject: [PATCH 33/88] fix: api error message (#38031) Fix various abuses and mistakes --- routers/api/v1/api.go | 8 ++-- routers/api/v1/org/org.go | 4 +- routers/api/v1/repo/action.go | 24 ++++------- routers/api/v1/repo/actions_run.go | 15 +------ routers/api/v1/repo/branch.go | 6 +-- routers/api/v1/repo/commits.go | 2 +- routers/api/v1/repo/file.go | 25 ++++-------- routers/api/v1/repo/issue.go | 14 +------ routers/api/v1/repo/issue_comment.go | 18 ++------- routers/api/v1/repo/issue_dependency.go | 24 ++--------- routers/api/v1/repo/issue_lock.go | 12 +----- routers/api/v1/repo/issue_reaction.go | 12 +----- routers/api/v1/repo/issue_tracked_time.go | 38 ++++-------------- routers/api/v1/repo/notes.go | 6 +-- routers/api/v1/repo/pull.go | 6 +-- routers/api/v1/repo/pull_review.go | 44 +++++---------------- routers/api/v1/repo/release_attachment.go | 2 +- routers/api/v1/repo/wiki.go | 18 ++------- routers/api/v1/shared/block.go | 6 +-- routers/api/v1/shared/runners.go | 6 +-- routers/api/v1/user/gpg_key.go | 5 +-- routers/api/v1/user/helper.go | 2 +- routers/api/v1/user/key.go | 5 +-- routers/api/v1/user/user.go | 2 +- services/context/api.go | 24 ++--------- services/wiki/wiki.go | 4 +- tests/integration/api_user_org_perm_test.go | 4 +- 27 files changed, 80 insertions(+), 256 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 4248faea5d4..618181eb1c1 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -151,7 +151,7 @@ func repoAssignment() func(ctx *context.APIContext) { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorNotFound() } else { ctx.APIErrorInternal(err) } @@ -626,7 +626,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { if err == nil { context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.APIErrorNotFound("GetOrgByName", err) + ctx.APIErrorNotFound() } else { ctx.APIErrorInternal(err) } @@ -862,12 +862,12 @@ func individualPermsChecker(ctx *context.APIContext) { switch ctx.ContextUser.Visibility { case api.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { - ctx.APIErrorNotFound("Visit Project", nil) + ctx.APIErrorNotFound() return } case api.VisibleTypeLimited: if ctx.Doer == nil { - ctx.APIErrorNotFound("Visit Project", nil) + ctx.APIErrorNotFound() return } } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 16d3e230a01..8f4d19719fd 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -146,7 +146,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op := api.OrganizationPermissions{} if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) { - ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound() return } @@ -312,7 +312,7 @@ func Get(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { - ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 5fc2e97d7a3..60077474120 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1164,11 +1164,8 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun { runID := ctx.PathParamInt64("run") run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - return nil - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) return nil } run.Repo = ctx.Repo.Repository @@ -1198,11 +1195,8 @@ func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_m attemptNum := ctx.PathParamInt64("attempt") attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - return nil, nil - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) return nil, nil } return run, attempt @@ -1454,7 +1448,7 @@ func RerunWorkflowJob(ctx *context.APIContext) { jobID := ctx.PathParamInt64("job_id") jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID }) if jobIdx == -1 { - ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID)) + ctx.APIErrorNotFound("workflow job not found") return } @@ -1566,11 +1560,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) { run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID. @@ -1674,7 +1664,7 @@ func GetWorkflowJob(ctx *context.APIContext) { } if !has || job.RepoID != ctx.Repo.Repository.ID { - ctx.APIErrorNotFound(util.ErrNotExist) + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go index d1d98ff21ab..1765ed564d6 100644 --- a/routers/api/v1/repo/actions_run.go +++ b/routers/api/v1/repo/actions_run.go @@ -4,10 +4,7 @@ package repo import ( - "errors" - actions_model "gitea.dev/models/actions" - "gitea.dev/modules/util" "gitea.dev/routers/common" "gitea.dev/services/context" ) @@ -45,11 +42,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { jobID := ctx.PathParamInt64("job_id") curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if err = curJob.LoadRepo(ctx); err != nil { @@ -59,10 +52,6 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) { err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) } } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 3b6575d6763..0806858b4da 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -64,7 +64,7 @@ func GetBranch(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if !exist { - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() return } @@ -153,7 +153,7 @@ func DeleteBranch(ctx *context.APIContext) { if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { switch { case git.IsErrBranchNotExist(err): - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() case errors.Is(err, repo_service.ErrBranchIsDefault): ctx.APIError(http.StatusForbidden, "can not delete default or pull request target branch") case errors.Is(err, git_model.ErrBranchIsProtected): @@ -446,7 +446,7 @@ func UpdateBranch(ctx *context.APIContext) { if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil { switch { case git_model.IsErrBranchNotExist(err): - ctx.APIErrorNotFound(err) + ctx.APIErrorNotFound() case errors.Is(err, util.ErrInvalidArgument): ctx.APIError(http.StatusUnprocessableEntity, err.Error()) case git.IsErrPushRejected(err): diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index d83686083e5..6f8aaefb6d9 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -258,7 +258,7 @@ func GetAllCommits(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } else if commitsCountTotal == 0 { - ctx.APIErrorNotFound("FileCommitsCount", nil) + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 244d9393ce6..5f10c4fbd71 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -213,7 +213,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn } if entry.IsDir() || entry.IsSubModule() { - ctx.APIErrorNotFound("getBlobForEntry", nil) + ctx.APIErrorNotFound() return nil, nil, nil } @@ -301,18 +301,14 @@ func GetEditorconfig(ctx *context.APIContext) { ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } fileName := ctx.PathParam("filename") def, err := ec.GetDefinitionForFilename(fileName) - if def == nil { - ctx.APIErrorNotFound(err) + if err != nil { + ctx.APIErrorNotFound(err.Error()) return } ctx.JSON(http.StatusOK, def) @@ -699,10 +695,8 @@ func DeleteFile(ctx *context.APIContext) { func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit { ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch) refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...) - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound(err) - } else if err != nil { - ctx.APIErrorInternal(err) + if err != nil { + ctx.APIErrorAuto(err) } return refCommit } @@ -828,11 +822,8 @@ func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrLi } ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound("GetContentsOrList", err) - return nil - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) + return nil } return &ret } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index a075c68f749..9946afc8b7f 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -540,16 +540,10 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 { } user, err := user_model.GetUserByName(ctx, userName) - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound(err) - return 0 - } - if err != nil { - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return 0 } - return user.ID } @@ -969,11 +963,7 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/notFound" issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 02a0f702ce9..6cece7e5cee 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -447,11 +447,7 @@ func GetIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -572,11 +568,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -681,11 +673,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { func deleteIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 4912737c911..ff4e7cd5b46 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -63,11 +63,7 @@ func GetIssueDependencies(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -487,11 +483,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) { func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } issue.Repo = ctx.Repo.Repository @@ -508,11 +500,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is var err error repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) if err != nil { - if repo_model.IsErrRepoNotExist(err) { - ctx.APIErrorNotFound("IsErrRepoNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } } else { @@ -521,11 +509,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound("IsErrIssueNotExist", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } issue.Repo = repo diff --git a/routers/api/v1/repo/issue_lock.go b/routers/api/v1/repo/issue_lock.go index 75247593fd3..2a4f75a9373 100644 --- a/routers/api/v1/repo/issue_lock.go +++ b/routers/api/v1/repo/issue_lock.go @@ -53,11 +53,7 @@ func LockIssue(ctx *context.APIContext) { reason := web.GetForm(ctx).(*api.LockIssueOption).Reason issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -120,11 +116,7 @@ func UnlockIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index c9fa39e93d2..6ad44ead619 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -53,11 +53,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -190,11 +186,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 1af649bfd79..33af841fbd1 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -71,16 +71,12 @@ func ListTrackedTimes(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.APIErrorNotFound("Timetracker is disabled") + ctx.APIErrorNotFound("timetracker is disabled") return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -182,11 +178,7 @@ func AddTime(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.AddTimeOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -265,11 +257,7 @@ func ResetIssueTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -338,11 +326,7 @@ func DeleteTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrIssueNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -357,11 +341,7 @@ func DeleteTime(ctx *context.APIContext) { time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id")) if err != nil { - if db.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - return - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return } if time.Deleted { @@ -423,11 +403,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { } user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername")) if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } if user == nil { diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index d8fea56c4cd..d2bd708aa48 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -68,11 +68,7 @@ func getNote(ctx *context.APIContext, identifier string) { commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 54e7b78a1b4..dcfaa3d3ac8 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -927,11 +927,7 @@ func MergePullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 54919cef90f..9778dc416ac 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -63,11 +63,7 @@ func ListPullReviews(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -389,11 +385,7 @@ func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) { func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment { comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrCommentNotExist(err) { - ctx.APIErrorNotFound("GetCommentByID", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil } @@ -510,11 +502,7 @@ func CreatePullReview(ctx *context.APIContext) { opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } @@ -737,33 +725,25 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil, true } review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id")) if err != nil { - if issues_model.IsErrReviewNotExist(err) { - ctx.APIErrorNotFound("GetReviewByID", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil, true } // validate the review is for the given PR if review.IssueID != pr.IssueID { - ctx.APIErrorNotFound("ReviewNotInPR") + ctx.APIErrorNotFound() return nil, nil, true } // make sure that the user has access to this review if it is pending if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.APIErrorNotFound("GetReviewByID") + ctx.APIErrorNotFound() return nil, nil, true } @@ -870,7 +850,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) + ctx.APIErrorNotFound("user doesn't exist: " + r) return nil, nil } ctx.APIErrorInternal(err) @@ -886,7 +866,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.APIErrorNotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) + ctx.APIErrorNotFound("team doesn't exist: " + t) return nil, nil } ctx.APIErrorInternal(err) @@ -902,11 +882,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - if issues_model.IsErrPullRequestNotExist(err) { - ctx.APIErrorNotFound("GetPullRequestByIndex", err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 715552d72c7..915ac725b06 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -205,7 +205,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // Check if attachments are enabled if !setting.Attachment.Enabled { - ctx.APIErrorNotFound("Attachment is not enabled") + ctx.APIErrorNotFound("attachment is not enabled") return } diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index dad0bfbbce0..67209fa1271 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -245,11 +245,7 @@ func DeleteWikiPage(ctx *context.APIContext) { wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { - if err.Error() == "file does not exist" { - ctx.APIErrorNotFound(err) - return - } - ctx.APIErrorInternal(err) + ctx.APIErrorAuto(err) return } @@ -474,21 +470,13 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { - if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, nil } commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if err != nil { - if git.IsErrNotExist(err) { - ctx.APIErrorNotFound(err) - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return wikiRepo, nil } return wikiRepo, commit diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index 8b2a207ccb2..c2a5fe8a4ef 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -45,7 +45,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } @@ -62,7 +62,7 @@ func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { func BlockUser(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } @@ -81,7 +81,7 @@ func BlockUser(ctx *context.APIContext, blocker *user_model.User) { func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorAuto(err) return } diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index 329f17736eb..fbb02627689 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -77,11 +77,7 @@ func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*a runner, err := actions_model.GetRunnerByID(ctx, runnerID) if err != nil { - if errors.Is(err, util.ErrNotExist) { - ctx.APIErrorNotFound("Runner not found") - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return nil, false } diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 562e70b5c0b..0148c7f5dac 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -4,7 +4,6 @@ package user import ( - "errors" "net/http" "strings" @@ -135,7 +134,7 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed") return } @@ -276,7 +275,7 @@ func DeleteGPGKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("gpg keys setting is not allowed to be changed") return } diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index ce051e2d168..ee7b8b17275 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -18,7 +18,7 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil { context.RedirectToUser(ctx.Base, ctx.Doer, username, redirectUserID) } else { - ctx.APIErrorNotFound("GetUserByName", err) + ctx.APIErrorNotFound() } } else { ctx.APIErrorInternal(err) diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 1a932c94af4..c12e98ca4c8 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -6,7 +6,6 @@ package user import ( std_ctx "context" - "errors" "net/http" asymkey_model "gitea.dev/models/asymkey" @@ -201,7 +200,7 @@ func GetPublicKey(ctx *context.APIContext) { // CreateUserPublicKey creates new public key to given user by ID. func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("ssh keys setting is not allowed to be changed") return } @@ -271,7 +270,7 @@ func DeletePublicKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("ssh keys setting is not allowed to be changed") return } diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index d45ca11348d..8343e380773 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -117,7 +117,7 @@ func GetInfo(ctx *context.APIContext) { if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { // fake ErrUserNotExist error message to not leak information about existence - ctx.APIErrorNotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) + ctx.APIErrorNotFound() return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) diff --git a/services/context/api.go b/services/context/api.go index 7731b5692fc..02ec2b71384 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -138,26 +138,10 @@ func (ctx *APIContext) apiErrorInternal(skip int, err error) { } // APIErrorNotFound handles 404s for APIContext -// String will replace message, errors will be added to a slice -func (ctx *APIContext) APIErrorNotFound(objs ...any) { - var message string - var errs []string - for _, obj := range objs { - // Ignore nil - if obj == nil { - continue - } - - if err, ok := obj.(error); ok { - errs = append(errs, err.Error()) - } else { - message = obj.(string) - } - } - ctx.JSON(http.StatusNotFound, map[string]any{ - "message": util.IfZero(message, "not found"), // do not use locale in API - "url": setting.API.SwaggerURL, - "errors": errs, +func (ctx *APIContext) APIErrorNotFound(msg ...string) { + ctx.JSON(http.StatusNotFound, APIError{ + Message: util.OptionalArg(msg, "not found"), + URL: setting.API.SwaggerURL, }) } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index abe846ae063..d1006abf7e8 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -7,7 +7,6 @@ package wiki import ( "context" "fmt" - "os" "gitea.dev/models/db" repo_model "gitea.dev/models/repo" @@ -21,6 +20,7 @@ import ( "gitea.dev/modules/graceful" "gitea.dev/modules/log" repo_module "gitea.dev/modules/repository" + "gitea.dev/modules/util" asymkey_service "gitea.dev/services/asymkey" repo_service "gitea.dev/services/repository" ) @@ -304,7 +304,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model return err } } else { - return os.ErrNotExist + return util.ErrNotExist } // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here diff --git a/tests/integration/api_user_org_perm_test.go b/tests/integration/api_user_org_perm_test.go index d31abdfeb37..63d69fab717 100644 --- a/tests/integration/api_user_org_perm_test.go +++ b/tests/integration/api_user_org_perm_test.go @@ -151,9 +151,7 @@ func testUnknownOrganization(t *testing.T) { req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknown/permissions"). AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusNotFound) - apiError := DecodeJSON(t, resp, &api.APIError{}) - assert.Equal(t, "GetUserByName", apiError.Message) + MakeRequest(t, req, http.StatusNotFound) } func testHiddenMemberPermissionsForbidden(t *testing.T) { From 2a84831400aedaaa7075225928a73a822940ec8e Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 8 Jun 2026 09:53:12 -0700 Subject: [PATCH 34/88] chore(deps): update astral-sh/setup-uv action to v8.2.0 (#38036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) | action | minor | `v8.1.0` → `v8.2.0` | --- ### Release Notes
    astral-sh/setup-uv (astral-sh/setup-uv) ### [`v8.2.0`](https://redirect.github.com/astral-sh/setup-uv/releases/tag/v8.2.0): 🌈 New inputs `quiet` and `download-from-astral-mirror` [Compare Source](https://redirect.github.com/astral-sh/setup-uv/compare/v8.1.0...v8.2.0) #### Changes This release brings two new inputs and a few bug fixes. ##### New inputs Lets talk about the new inputs first. ##### quiet Pretty simple. It turns of all `info` loggings. Useful if you use this in a composite action and are not interested in all the details. In the upcoming releases we will add log groups to fully implement support for "less noise" > \[!NOTE]\ > Warnings and errors are always logged. ##### download-from-astral-mirror In some cases you may want to directly use the fallback of checking for available versions and downloading releases from GitHub instead of using the astral.sh mirror. Setting `download-from-astral-mirror: false` allows you to do that. ##### Bugfixes When using the astral.sh mirror to query available versions and download releases (done by default) we now stop sending the GitHub token in the header. The mirror never looked at it but we shouldn't be handing out that data even if it is just a short lived token. All other bugfixes try to limit the impact of failed GitHub queries due to retries and other faults. We couldn't pinpoint all rootcauses yet but added more logging for error cases to track them down. #### 🐛 Bug fixes - fix: report unexpected cache save failures [@​eifinger](https://redirect.github.com/eifinger) ([#​896](https://redirect.github.com/astral-sh/setup-uv/issues/896)) - fix: report unexpected setup failures [@​eifinger](https://redirect.github.com/eifinger) ([#​895](https://redirect.github.com/astral-sh/setup-uv/issues/895)) - fix: add timeout to fetch to prevent silent hangs [@​eifinger-bot](https://redirect.github.com/eifinger-bot) ([#​883](https://redirect.github.com/astral-sh/setup-uv/issues/883)) - Limit GitHub tokens to github.com download URLs [@​zsol](https://redirect.github.com/zsol) ([#​878](https://redirect.github.com/astral-sh/setup-uv/issues/878)) - increase libuv-workaround timeout to 100ms [@​eifinger](https://redirect.github.com/eifinger) ([#​880](https://redirect.github.com/astral-sh/setup-uv/issues/880)) #### 🚀 Enhancements - Add quiet input to suppress info-level log output [@​eifinger](https://redirect.github.com/eifinger) ([#​898](https://redirect.github.com/astral-sh/setup-uv/issues/898)) - feat: add `download-from-astral-mirror` input [@​eifinger](https://redirect.github.com/eifinger) ([#​897](https://redirect.github.com/astral-sh/setup-uv/issues/897)) #### 🧰 Maintenance - docs: update dependabot rollup biome guidance [@​eifinger](https://redirect.github.com/eifinger) ([#​902](https://redirect.github.com/astral-sh/setup-uv/issues/902)) - chore: update known checksums for 0.11.18 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​899](https://redirect.github.com/astral-sh/setup-uv/issues/899)) - chore: update known checksums for 0.11.17 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​892](https://redirect.github.com/astral-sh/setup-uv/issues/892)) - chore: update known checksums for 0.11.16 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​889](https://redirect.github.com/astral-sh/setup-uv/issues/889)) - chore: update known checksums for 0.11.15 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​885](https://redirect.github.com/astral-sh/setup-uv/issues/885)) - chore: update known checksums for 0.11.14 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​879](https://redirect.github.com/astral-sh/setup-uv/issues/879)) - chore: update known checksums for 0.11.13 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​877](https://redirect.github.com/astral-sh/setup-uv/issues/877)) - chore: update known checksums for 0.11.12 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​876](https://redirect.github.com/astral-sh/setup-uv/issues/876)) - chore: update known checksums for 0.11.11 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​873](https://redirect.github.com/astral-sh/setup-uv/issues/873)) - chore: update known checksums for 0.11.9/0.11.10 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​871](https://redirect.github.com/astral-sh/setup-uv/issues/871)) - chore: update known checksums for 0.11.8 @​[github-actions\[bot\]](https://redirect.github.com/apps/github-actions) ([#​867](https://redirect.github.com/astral-sh/setup-uv/issues/867)) - Bump setup-uv references to v8.1.0 SHA in docs [@​eifinger](https://redirect.github.com/eifinger) ([#​862](https://redirect.github.com/astral-sh/setup-uv/issues/862)) - Add update-docs.yml workflow [@​eifinger](https://redirect.github.com/eifinger) ([#​861](https://redirect.github.com/astral-sh/setup-uv/issues/861)) #### ⬆️ Dependency updates - chore(deps): roll up dependabot updates [@​eifinger](https://redirect.github.com/eifinger) ([#​903](https://redirect.github.com/astral-sh/setup-uv/issues/903)) - chore(deps): roll up dependabot updates [@​eifinger](https://redirect.github.com/eifinger) ([#​901](https://redirect.github.com/astral-sh/setup-uv/issues/901)) - chore(deps): bump release-drafter/release-drafter from 7.3.0 to 7.3.1 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​900](https://redirect.github.com/astral-sh/setup-uv/issues/900)) - chore(deps): bump eifinger/actionlint-action from 1.10.1 to 1.10.2 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​842](https://redirect.github.com/astral-sh/setup-uv/issues/842)) - chore(deps): bump github/codeql-action from 4.35.4 to 4.36.0 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​893](https://redirect.github.com/astral-sh/setup-uv/issues/893)) - chore(deps): bump zizmorcore/zizmor-action from 0.5.5 to 0.5.6 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​891](https://redirect.github.com/astral-sh/setup-uv/issues/891)) - chore(deps): bump release-drafter/release-drafter from 7.2.0 to 7.3.0 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​884](https://redirect.github.com/astral-sh/setup-uv/issues/884)) - chore(deps): bump zizmorcore/zizmor-action from 0.5.3 to 0.5.5 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​888](https://redirect.github.com/astral-sh/setup-uv/issues/888)) - chore(deps): bump github/codeql-action from 4.35.3 to 4.35.4 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​881](https://redirect.github.com/astral-sh/setup-uv/issues/881)) - chore(deps): bump github/codeql-action from 4.32.2 to 4.35.3 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​875](https://redirect.github.com/astral-sh/setup-uv/issues/875)) - chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​866](https://redirect.github.com/astral-sh/setup-uv/issues/866)) - chore(deps): bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​864](https://redirect.github.com/astral-sh/setup-uv/issues/864)) - chore(deps): bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 @​[dependabot\[bot\]](https://redirect.github.com/apps/dependabot) ([#​863](https://redirect.github.com/astral-sh/setup-uv/issues/863))
    --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- .github/workflows/pull-compliance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 6c41b6b4c11..d8129fd5b7a 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -42,7 +42,7 @@ jobs: - run: make lint-spell - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' || needs.files-changed.outputs.actions == 'true' - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: python-version: 3.14 - if: needs.files-changed.outputs.templates == 'true' || needs.files-changed.outputs.yaml == 'true' From 54916f708e77e9cfe9d2e85164e436383f5faf9a Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 8 Jun 2026 19:16:22 +0200 Subject: [PATCH 35/88] feat: Add avatar stacks (#37594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse `Co-authored-by:` trailers from commit messages and surface contributors as an avatar stack across the commit page, commits list, PR commits tab, latest-commit row, blame, graph, and dashboard feed. - Up to 10 visible 20px avatars, GitHub-style overlap (6px first stride, 4px between subsequent), `+N` chip for the rest. - Label: 1 → name; 2 → ` and `; 3+ → ` people` opens a Tippy popup with all participants. - Names and avatars link to the repo's commits-by-author search; fall back to profile or `mailto:`. - Trailer parsing uses `net/mail.ParseAddress`, scans only the trailing paragraph, filters out the commit's own author/committer. - Drops the non-standard `Co-committed-by:` emission on squash merge and web edits. Devtest: `/devtest/coauthor-avatars`. Fixes #25521 ---- image --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: silverwind Co-authored-by: wxiaoguang Co-authored-by: Giteabot --- models/asymkey/gpg_key_commit_verification.go | 5 +- models/gituser/avatar_stack.go | 44 ++++++ models/gituser/gituser.go | 64 ++++++++ models/user/user.go | 38 +---- modules/git/commit.go | 32 ---- modules/git/commit_message.go | 131 +++++++++++++++++ modules/git/commit_message_test.go | 80 ++++++++++ modules/git/commit_test.go | 9 -- modules/repository/commits.go | 27 +--- modules/repository/commits_test.go | 34 ----- modules/setting/config/value.go | 5 + modules/templates/util_render.go | 139 +++++++++++++++++- modules/templates/util_render_test.go | 53 +++++++ options/locale/locale_en-US.json | 7 +- routers/web/devtest/devtest.go | 84 +++++++++-- routers/web/repo/blame.go | 35 ++--- routers/web/repo/commit.go | 10 +- routers/web/repo/compare.go | 4 +- routers/web/repo/compare_test.go | 6 +- routers/web/repo/view.go | 6 +- routers/web/repo/wiki.go | 2 +- services/git/commit.go | 19 +-- services/issue/comments.go | 2 +- services/pull/merge_squash.go | 4 +- services/pull/pull.go | 2 +- services/repository/files/temp_repo.go | 7 +- services/repository/gitgraph/graph_models.go | 55 ++++--- templates/devtest/avatar-stack.tmpl | 18 +++ templates/devtest/badge-commit-sign.tmpl | 2 +- templates/repo/blame.tmpl | 2 +- templates/repo/commit_page.tmpl | 23 +++ templates/repo/commit_sign_badge.tmpl | 4 +- templates/repo/commits_list.tmpl | 50 +++---- templates/repo/commits_list_small.tmpl | 25 ++-- templates/repo/graph/commits.tmpl | 9 +- templates/repo/latest_commit.tmpl | 10 +- templates/user/dashboard/feeds.tmpl | 6 +- tests/integration/repo_commits_test.go | 16 +- web_src/css/avatar.css | 125 ++++++++++++++++ web_src/css/base.css | 8 - web_src/css/index.css | 1 + web_src/css/repo.css | 12 +- web_src/js/features/repo-commit.ts | 16 ++ web_src/js/index.ts | 3 +- 44 files changed, 912 insertions(+), 322 deletions(-) create mode 100644 models/gituser/avatar_stack.go create mode 100644 models/gituser/gituser.go create mode 100644 modules/git/commit_message.go create mode 100644 modules/git/commit_message_test.go create mode 100644 templates/devtest/avatar-stack.tmpl create mode 100644 web_src/css/avatar.css diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go index 17244076dd2..251d8eff11c 100644 --- a/models/asymkey/gpg_key_commit_verification.go +++ b/models/asymkey/gpg_key_commit_verification.go @@ -8,6 +8,7 @@ import ( "fmt" "hash" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/log" @@ -32,8 +33,8 @@ type CommitVerification struct { // SignCommit represents a commit with validation of signature. type SignCommit struct { - Verification *CommitVerification - *user_model.UserCommit + Verification *CommitVerification + *gituser.UserCommit // TODO: need to use a explicit field name, avoid anonymous field } const ( diff --git a/models/gituser/avatar_stack.go b/models/gituser/avatar_stack.go new file mode 100644 index 00000000000..d38c94bc7bf --- /dev/null +++ b/models/gituser/avatar_stack.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gituser + +import ( + "context" + + "gitea.dev/models/user" + "gitea.dev/modules/git" + "gitea.dev/modules/log" +) + +// AvatarStackData is the view-model for the AvatarStack render helpers. Participants[0] is +// the primary participant (commit author), painted on top; the rest follow. +type AvatarStackData struct { + Participants []*CommitParticipant + SearchByEmailLink string +} + +func BuildAvatarStackData(ctx context.Context, allParticipants []*git.CommitIdentity, emailUserMap *user.EmailUserMap) *AvatarStackData { + if emailUserMap == nil { + emails := make([]string, len(allParticipants)) + for i, sig := range allParticipants { + emails[i] = sig.Email + } + var err error + emailUserMap, err = user.GetUsersByEmails(ctx, emails) + if err != nil { + log.Error("GetUsersByEmails failed: %v", err) + } + } + ret := &AvatarStackData{ + Participants: make([]*CommitParticipant, 0, len(allParticipants)), + } + for _, p := range allParticipants { + var giteaUser *user.User + if emailUserMap != nil { + giteaUser = emailUserMap.GetByEmail(p.Email) + } + ret.Participants = append(ret.Participants, &CommitParticipant{GiteaUser: giteaUser, GitIdentity: p}) + } + return ret +} diff --git a/models/gituser/gituser.go b/models/gituser/gituser.go new file mode 100644 index 00000000000..81a94a3a85b --- /dev/null +++ b/models/gituser/gituser.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gituser + +import ( + "context" + "net/url" + + "gitea.dev/models/user" + "gitea.dev/modules/container" + "gitea.dev/modules/git" +) + +// CommitParticipant is one participant of a commit (its author or a co-author): +// a git identity, optionally matched to a Gitea user. +type CommitParticipant struct { + GitIdentity *git.CommitIdentity // git identity (name/email), never nil + GiteaUser *user.User // matched Gitea user, nil if unmatched +} + +// UserCommit represents a commit with matched of database "author" user. +type UserCommit struct { + GitCommit *git.Commit + AuthorUser *user.User + AvatarStackData *AvatarStackData +} + +func RepoCommitSearchByEmailLink(repoLink string, ref git.RefName) string { + if curRefWebLinkPath := ref.RefWebLinkPath(); curRefWebLinkPath != "" { + return repoLink + "/commits/" + curRefWebLinkPath + "/search?q=" + url.QueryEscape("author:") + "{email}" + } + return "" +} + +// GetUserCommitsByGitCommits checks if authors' e-mails of commits are corresponding to users. +func GetUserCommitsByGitCommits(ctx context.Context, gitCommits []*git.Commit, repoLink string, currentRef git.RefName) ([]*UserCommit, error) { + userCommits := make([]*UserCommit, 0, len(gitCommits)) + emailSet := make(container.Set[string]) + for _, c := range gitCommits { + emailSet.Add(c.Author.Email) + emailSet.Add(c.Committer.Email) + for _, p := range c.AllParticipantIdentities() { + emailSet.Add(p.Email) + } + } + + emailUserMap, err := user.GetUsersByEmails(ctx, emailSet.Values()) + if err != nil { + return nil, err + } + + searchByEmailLink := RepoCommitSearchByEmailLink(repoLink, currentRef) + for _, c := range gitCommits { + uc := &UserCommit{ + AuthorUser: emailUserMap.GetByEmail(c.Author.Email), // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"? + GitCommit: c, + AvatarStackData: BuildAvatarStackData(ctx, c.AllParticipantIdentities(), emailUserMap), + } + uc.AvatarStackData.SearchByEmailLink = searchByEmailLink + userCommits = append(userCommits, uc) + } + return userCommits, nil +} diff --git a/models/user/user.go b/models/user/user.go index 66e8d49b420..4e6227de9e7 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1148,14 +1148,7 @@ func GetUsersBySource(ctx context.Context, s *auth.Source) ([]*User, error) { return users, err } -// UserCommit represents a commit with validation of user. -type UserCommit struct { //revive:disable-line:exported - User *User - *git.Commit -} - -// ValidateCommitWithEmail check if author's e-mail of commit is corresponding to a user. -func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User { +func GetUserByGitAuthor(ctx context.Context, c *git.Commit) *User { if c.Author == nil { return nil } @@ -1166,33 +1159,6 @@ func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User { return u } -// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. -func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([]*UserCommit, error) { - var ( - newCommits = make([]*UserCommit, 0, len(oldCommits)) - emailSet = make(container.Set[string]) - ) - for _, c := range oldCommits { - if c.Author != nil { - emailSet.Add(c.Author.Email) - } - } - - emailUserMap, err := GetUsersByEmails(ctx, emailSet.Values()) - if err != nil { - return nil, err - } - - for _, c := range oldCommits { - user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? - newCommits = append(newCommits, &UserCommit{ - User: user, - Commit: c, - }) - } - return newCommits, nil -} - type EmailUserMap struct { m map[string]*User } @@ -1203,7 +1169,7 @@ func (eum *EmailUserMap) GetByEmail(email string) *User { func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) { if len(emails) == 0 { - return nil, nil //nolint:nilnil // return nil when there are no emails to look up + return &EmailUserMap{}, nil } needCheckEmails := make(container.Set[string]) diff --git a/modules/git/commit.go b/modules/git/commit.go index 0231770a293..21288ad8459 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -11,18 +11,10 @@ import ( "os/exec" "strings" - "gitea.dev/modules/charset" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/util" ) -type CommitMessage struct { - MessageRaw string - messageUTF8 *string - messageTitle *string - messageBody *string -} - // Commit represents a git commit. type Commit struct { Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" @@ -44,30 +36,6 @@ type CommitSignature struct { Payload string } -func (c *CommitMessage) MessageUTF8() string { - if c.messageUTF8 == nil { - bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}}) - c.messageUTF8 = new(util.UnsafeBytesToString(bs)) - } - return *c.messageUTF8 -} - -func (c *CommitMessage) MessageTitle() string { - if c.messageTitle == nil { - s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") - c.messageTitle = new(strings.TrimSpace(s)) - } - return *c.messageTitle -} - -func (c *CommitMessage) MessageBody() string { - if c.messageBody == nil { - _, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") - c.messageBody = new(strings.TrimSpace(s)) - } - return *c.messageBody -} - // ParentID returns oid of n-th parent (0-based index). // It returns nil if no such parent exists. func (c *Commit) ParentID(n int) (ObjectID, error) { diff --git a/modules/git/commit_message.go b/modules/git/commit_message.go new file mode 100644 index 00000000000..8fd3601f0d0 --- /dev/null +++ b/modules/git/commit_message.go @@ -0,0 +1,131 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "net/mail" + "regexp" + "strings" + "sync" + + "gitea.dev/modules/charset" + "gitea.dev/modules/container" + "gitea.dev/modules/util" +) + +// CoAuthoredByTrailer is the canonical token for the `Co-authored-by:` git trailer. +const CoAuthoredByTrailer = "Co-authored-by" + +type CommitIdentity struct { + Name string + Email string +} + +// CommitMessageTrailerValues keys are all in lower-case +type CommitMessageTrailerValues map[string][]string + +type CommitMessage struct { + MessageRaw string + messageUTF8 *string + messageTitle *string + messageBody *string + + trailerValues CommitMessageTrailerValues + + allParticipants []*CommitIdentity +} + +func (c *CommitMessage) MessageUTF8() string { + if c.messageUTF8 == nil { + bs := charset.ToUTF8(util.UnsafeStringToBytes(c.MessageRaw), charset.ConvertOpts{ErrorReplacement: []byte{'?'}}) + c.messageUTF8 = new(util.UnsafeBytesToString(bs)) + } + return *c.messageUTF8 +} + +func (c *CommitMessage) MessageTitle() string { + if c.messageTitle == nil { + s, _, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageTitle = new(strings.TrimSpace(s)) + } + return *c.messageTitle +} + +func (c *CommitMessage) MessageBody() string { + if c.messageBody == nil { + _, s, _ := strings.Cut(strings.TrimSpace(c.MessageUTF8()), "\n") + c.messageBody = new(strings.TrimSpace(s)) + } + return *c.messageBody +} + +func (c *CommitMessage) MessageTrailer() CommitMessageTrailerValues { + if c.trailerValues == nil { + _, _, trailer := CommitMessageSplitTrailer(c.MessageUTF8()) + c.trailerValues = CommitMessageParseTrailer(trailer) + } + return c.trailerValues +} + +var commitMessageTrailerSplit = sync.OnceValue(func() *regexp.Regexp { + // the sep is either something like "\n---\n" or "\n\n" in the body, or at the start of the body like "---\n" + return regexp.MustCompile(`(?s)^(?P.*?)(?P^|^\n|^-{3,}\n|\n-{3,}\n|\n\n)(?P(?:[A-Za-z0-9][-A-Za-z0-9]*:[^\n]*\n?)*)$`) +}) + +func CommitMessageSplitTrailer(s string) (content, sep, trailer string) { + s = util.NormalizeStringEOL(s) + re := commitMessageTrailerSplit() + v := re.FindStringSubmatch(s) + if v == nil { + return s, "", "" + } + return v[re.SubexpIndex("content")], v[re.SubexpIndex("sep")], v[re.SubexpIndex("trailer")] +} + +func CommitMessageParseTrailer(s string) CommitMessageTrailerValues { + ret := CommitMessageTrailerValues{} + for line := range strings.SplitSeq(util.NormalizeStringEOL(s), "\n") { + k, v, ok := strings.Cut(line, ":") + if !ok { + continue + } + k, v = strings.TrimSpace(k), strings.TrimSpace(v) + kLower := strings.ToLower(k) + ret[kLower] = append(ret[kLower], v) + } + return ret +} + +// AllParticipantIdentities returns all the participants in the commit, the first one is the commit's author +func (c *Commit) AllParticipantIdentities() []*CommitIdentity { + if c.allParticipants != nil { + return c.allParticipants + } + + exclude := container.Set[string]{} + c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: c.Author.Name, Email: c.Author.Email}) + exclude.Add(strings.ToLower(c.Author.Email)) + + addParticipant := func(name, email string) { + if name == "" && email == "" { + return + } + emailLower := strings.ToLower(email) + if emailLower != "" && exclude.Contains(emailLower) { + return + } + c.allParticipants = append(c.allParticipants, &CommitIdentity{Name: name, Email: email}) + exclude.Add(emailLower) + } + addParticipant(c.Committer.Name, c.Committer.Email) + for _, coAuthorValue := range c.MessageTrailer()["co-authored-by"] { + addr, err := mail.ParseAddress(coAuthorValue) + if err == nil { + addParticipant(addr.Name, addr.Address) + } else { + addParticipant(coAuthorValue, "") + } + } + return c.allParticipants +} diff --git a/modules/git/commit_message_test.go b/modules/git/commit_message_test.go new file mode 100644 index 00000000000..049f1c03f78 --- /dev/null +++ b/modules/git/commit_message_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { + commit := &Commit{ + CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"}, + } + assert.Equal(t, "title ÿ", commit.MessageTitle()) + assert.Equal(t, "body ÿ", commit.MessageBody()) + assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8()) +} + +func TestCommitMessageTrailer(t *testing.T) { + cases := []struct { + msg, body, sep, trailer string + }{ + {"", "", "", ""}, + {"a", "a", "", ""}, + {"a\n\nk", "a\n\nk", "", ""}, + {"a\n\nk:v", "a", "\n\n", "k:v"}, + {"a\n--\nk:v", "a\n--\nk:v", "", ""}, + {"a\n---\nk:v", "a", "\n---\n", "k:v"}, + + {"k: v", "", "", "k: v"}, + {"\nk:v", "", "\n", "k:v"}, + {"\n\nk:v", "", "\n\n", "k:v"}, + + {"---\nk:v", "", "---\n", "k:v"}, + {"\n---\nk:v", "", "\n---\n", "k:v"}, + {"a:b\n---\nk:v", "a:b", "\n---\n", "k:v"}, + } + for _, c := range cases { + body, sep, trailer := CommitMessageSplitTrailer(c.msg) + assert.Equal(t, c.body, body, "input=%q", c.msg) + assert.Equal(t, c.sep, sep, "input=%q", c.msg) + assert.Equal(t, c.trailer, trailer, "input=%q", c.msg) + } +} + +func TestCommitMessageAllParticipantIdentities(t *testing.T) { + sig := func(n, e string) *Signature { return &Signature{Name: n, Email: e} } + idt := func(n, e string) *CommitIdentity { return &CommitIdentity{Name: n, Email: e} } + cases := []struct { + commit *Commit + participant []*CommitIdentity + }{ + { + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("c", "c@m.com"), + CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: x@m.com"}, + }, + []*CommitIdentity{idt("a", "a@m.com"), idt("c", "c@m.com"), idt("", "x@m.com")}, + }, + { + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("a", "A@M.com"), + CommitMessage: CommitMessage{MessageRaw: "CO-Authored-BY: a@m.com"}, + }, + []*CommitIdentity{idt("a", "a@m.com")}, + }, + { + &Commit{ + Author: sig("a", "a@m.com"), Committer: sig("", ""), + CommitMessage: CommitMessage{MessageRaw: "Co-authored-by: Full Name "}, + }, + []*CommitIdentity{idt("a", "a@m.com"), idt("Full Name", "X@M.com")}, + }, + } + for _, c := range cases { + assert.Equal(t, c.participant, c.commit.AllParticipantIdentities()) + } +} diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index a7668e4debe..5e3d2fba71b 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -159,15 +159,6 @@ ISO-8859-1`, commitFromReader.Signature.Payload) assert.Equal(t, commitFromReader, commitFromReader2) } -func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { - commit := &Commit{ - CommitMessage: CommitMessage{MessageRaw: "title \xff\n\n\n\nbody \xff\n\n\n"}, - } - assert.Equal(t, "title ÿ", commit.MessageTitle()) - assert.Equal(t, "body ÿ", commit.MessageBody()) - assert.Equal(t, "title ÿ\n\n\n\nbody ÿ\n\n\n", commit.MessageUTF8()) -} - func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") diff --git a/modules/repository/commits.go b/modules/repository/commits.go index d40af3d82c4..318b052ef18 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -9,19 +9,15 @@ import ( "net/url" "time" - "gitea.dev/models/avatars" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" - "gitea.dev/modules/cache" - "gitea.dev/modules/cachegroup" "gitea.dev/modules/git" "gitea.dev/modules/gitrepo" - "gitea.dev/modules/log" - "gitea.dev/modules/setting" api "gitea.dev/modules/structs" ) // PushCommit represents a commit in a push operation. +// This struct is marshaled as JSON (see ActionContent2Commits) type PushCommit struct { Sha1 string Message string @@ -33,6 +29,7 @@ type PushCommit struct { } // PushCommits represents list of commits in a push operation. +// This struct is marshaled as JSON (see ActionContent2Commits) type PushCommits struct { Commits []*PushCommit HeadCommit *PushCommit @@ -128,26 +125,6 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repo *repo_model return commits, headCommit, nil } -// AvatarLink tries to match user in database with e-mail -// in order to show custom avatar, and falls back to general avatar link. -func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { - size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor - - v, _ := cache.GetWithContextCache(ctx, cachegroup.EmailAvatarLink, email, func(ctx context.Context, email string) (string, error) { - u, err := user_model.GetUserByEmail(ctx, email) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - log.Error("GetUserByEmail: %v", err) - return "", err - } - return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil - } - return u.AvatarLinkWithSize(ctx, size), nil - }) - - return v -} - // CommitToPushCommit transforms a git.Commit to PushCommit type. func CommitToPushCommit(commit *git.Commit) *PushCommit { return &PushCommit{ diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index c0f00337b92..5e6266d9f21 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -4,14 +4,12 @@ package repository import ( - "strconv" "testing" "time" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" "gitea.dev/modules/git" - "gitea.dev/modules/setting" "github.com/stretchr/testify/assert" ) @@ -99,38 +97,6 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { assert.Equal(t, []string{"readme.md"}, headCommit.Modified) } -func TestPushCommits_AvatarLink(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - pushCommits := NewPushCommits() - pushCommits.Commits = []*PushCommit{ - { - Sha1: "abcdef1", - CommitterEmail: "user2@example.com", - CommitterName: "User Two", - AuthorEmail: "user4@example.com", - AuthorName: "User Four", - Message: "message1", - }, - { - Sha1: "abcdef2", - CommitterEmail: "user2@example.com", - CommitterName: "User Two", - AuthorEmail: "user2@example.com", - AuthorName: "User Two", - Message: "message2", - }, - } - - assert.Equal(t, - "/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), - pushCommits.AvatarLink(t.Context(), "user2@example.com")) - - assert.Equal(t, - "/assets/img/avatar_default.png", - pushCommits.AvatarLink(t.Context(), "nonexistent@example.com")) -} - func TestCommitToPushCommit(t *testing.T) { now := time.Now() sig := &git.Signature{ diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index 655120c1804..240f48243cc 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -78,11 +78,16 @@ func isZeroOrEmpty(v any) bool { return false } +var SkipDatabaseConfig bool + func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) { dg := GetDynGetter() if dg == nil { // this is an edge case: the database is not initialized but the system setting is going to be used // it should panic to avoid inconsistent config values (from config / system setting) and fix the code + if SkipDatabaseConfig { + return opt.DefaultValue(), 0, false + } panic("no config dyn value getter") } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 81941902aee..46f90be78ad 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -10,8 +10,10 @@ import ( "math" "net/url" "regexp" + "slices" "strings" + user_model "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" "gitea.dev/models/repo" @@ -22,6 +24,7 @@ import ( "gitea.dev/modules/log" "gitea.dev/modules/markup" "gitea.dev/modules/markup/markdown" + "gitea.dev/modules/repository" "gitea.dev/modules/reqctx" "gitea.dev/modules/setting" "gitea.dev/modules/svg" @@ -31,11 +34,12 @@ import ( ) type RenderUtils struct { - ctx reqctx.RequestContext + ctx reqctx.RequestContext + avatarUtils *AvatarUtils } func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { - return &RenderUtils{ctx: ctx} + return &RenderUtils{ctx: ctx, avatarUtils: NewAvatarUtils(ctx)} } // RenderCommitMessage renders commit message title (only title) @@ -291,3 +295,134 @@ func (ut *RenderUtils) RenderUnicodeEscapeToggleTd(combined, escapeStatus *chars } return `` + ut.RenderUnicodeEscapeToggleButton(escapeStatus) + `` } + +func renderAvatarStackViewEmailLink(data *user_model.AvatarStackData, email string) template.URL { + if data.SearchByEmailLink != "" && email != "" { + return template.URL(strings.ReplaceAll(data.SearchByEmailLink, "{email}", url.QueryEscape(email))) + } + return "" +} + +func (ut *RenderUtils) participantHref(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.URL { + if href := renderAvatarStackViewEmailLink(data, participant.GitIdentity.Email); href != "" { + return href + } + if participant.GiteaUser != nil { + return template.URL(participant.GiteaUser.HomeLink()) + } else if participant.GitIdentity.Email != "" { + return template.URL("mailto:" + participant.GitIdentity.Email) + } + return "" +} + +func (ut *RenderUtils) participantAvatar(participant *user_model.CommitParticipant) template.HTML { + if participant.GiteaUser != nil { + return ut.avatarUtils.Avatar(participant.GiteaUser, 20) + } + return ut.avatarUtils.AvatarByEmail(participant.GitIdentity.Email, participant.GitIdentity.Name, 20) +} + +func participantName(participant *user_model.CommitParticipant) string { + if participant.GiteaUser != nil { + return participant.GiteaUser.GetDisplayName() + } + return participant.GitIdentity.Name +} + +const renderAvatarStackMaxVisible = 10 + +// AvatarStack renders overlapping avatars for the stack participants. It emits children in reverse +// so CSS `flex-direction: row-reverse` places the primary (Participants[0]) leftmost and last-painted (on top). +func (ut *RenderUtils) AvatarStack(data *user_model.AvatarStackData) template.HTML { + visible := data.Participants + overflow := len(visible) - renderAvatarStackMaxVisible + if overflow > 0 { + visible = visible[:renderAvatarStackMaxVisible] + } + + var b htmlutil.HTMLBuilder + b.WriteHTML(``) + if overflow > 0 { + b.WriteFormat(`+%d`, overflow, overflow) + } + + // FIXME: such "backward" breaks a11y like screen readers + for _, participant := range slices.Backward(visible) { + ut.writeAvatarStackItem(&b, data, participant) + } + b.WriteHTML(``) + return b.HTMLString() +} + +func (ut *RenderUtils) writeAvatarStackItem(b *htmlutil.HTMLBuilder, data *user_model.AvatarStackData, participant *user_model.CommitParticipant) { + avatar := ut.participantAvatar(participant) + if href := ut.participantHref(data, participant); href != "" { + b.WriteFormat(`%s`, href, avatar) + } else { + b.WriteFormat(`%s`, avatar) + } +} + +func (ut *RenderUtils) AvatarStackPushCommit(pushCommit *repository.PushCommit) template.HTML { + fakeGitCommit := git.Commit{ + CommitMessage: git.CommitMessage{MessageRaw: pushCommit.Message}, + Author: &git.Signature{Name: pushCommit.AuthorName, Email: pushCommit.AuthorEmail}, + // there is no way to know the real committer, but the field can't be nil + Committer: &git.Signature{Name: pushCommit.AuthorName, Email: pushCommit.AuthorEmail}, + } + data := user_model.BuildAvatarStackData(ut.ctx, fakeGitCommit.AllParticipantIdentities(), nil) + return ut.AvatarStack(data) +} + +// AvatarStackWithNames renders the avatar stack plus a label: `name` / `a and b` / `N people` (opens popup). +func (ut *RenderUtils) AvatarStackWithNames(data *user_model.AvatarStackData) template.HTML { + locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) + participants := data.Participants + + var b htmlutil.HTMLBuilder + b.WriteHTML(``) + b.WriteHTML(ut.AvatarStack(data)) + + switch len(participants) { + case 1: + b.WriteHTML(ut.participantNameLink(data, participants[0])) + case 2: + b.WriteHTML(ut.participantNameLink(data, participants[0])) + b.WriteFormat(`%s`, locale.Tr("repo.commits.avatar_stack_and")) + b.WriteHTML(ut.participantNameLink(data, participants[1])) + default: + b.WriteFormat(``, + locale.Tr("repo.commits.avatar_stack_people", len(participants))) + b.WriteHTML(`
    `) + for _, participant := range participants { + b.WriteHTML(ut.participantPopupRow(data, participant)) + } + b.WriteHTML(`
    `) + } + + b.WriteHTML(`
    `) + return b.HTMLString() +} + +// participantNameLink prefers (in order): commits-by-author search, `GetShortDisplayNameLinkHTML` (keeps alt-name tooltip), `mailto:`, bare name. +func (ut *RenderUtils) participantNameLink(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.HTML { + if href := renderAvatarStackViewEmailLink(data, participant.GitIdentity.Email); href != "" { + return htmlutil.HTMLFormat(`%s`, href, participantName(participant)) + } + if participant.GiteaUser != nil { + return participant.GiteaUser.GetShortDisplayNameLinkHTML() + } + if participant.GitIdentity.Email != "" { + return htmlutil.HTMLFormat(`%s`, participant.GitIdentity.Email, participant.GitIdentity.Name) + } + return template.HTML(template.HTMLEscapeString(participant.GitIdentity.Name)) +} + +func (ut *RenderUtils) participantPopupRow(data *user_model.AvatarStackData, participant *user_model.CommitParticipant) template.HTML { + avatar := ut.participantAvatar(participant) + name := participantName(participant) + if href := ut.participantHref(data, participant); href != "" { + return htmlutil.HTMLFormat(`%s%s`, href, avatar, name) + } + return htmlutil.HTMLFormat(`%s%s`, avatar, name) +} diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 5a28a1feba8..1db87feb798 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -7,15 +7,19 @@ import ( "context" "html/template" "os" + "strconv" "strings" "testing" + "gitea.dev/models/gituser" "gitea.dev/models/issues" "gitea.dev/models/repo" user_model "gitea.dev/models/user" + "gitea.dev/modules/git" "gitea.dev/modules/markup" "gitea.dev/modules/reqctx" "gitea.dev/modules/setting" + "gitea.dev/modules/setting/config" "gitea.dev/modules/test" "gitea.dev/modules/translation" @@ -298,3 +302,52 @@ func TestUserMention(t *testing.T) { rendered := newTestRenderUtils(t).MarkdownToHtml("@no-such-user @mention-user @mention-user") assert.Equal(t, `

    @no-such-user @mention-user @mention-user

    `, strings.TrimSpace(string(rendered))) } + +func TestAvatarStack(t *testing.T) { + defer test.MockVariableValue(&config.SkipDatabaseConfig, true)() + + ut := newTestRenderUtils(t) + mkCo := func(name, email string) *git.CommitIdentity { + return &git.CommitIdentity{Name: name, Email: email} + } + authorSig := mkCo("Alice", "alice@example.com") + mkData := func(co ...*git.CommitIdentity) *gituser.AvatarStackData { + all := append([]*git.CommitIdentity{authorSig}, co...) + return gituser.BuildAvatarStackData(t.Context(), all, &user_model.EmailUserMap{}) + } + + t.Run("lone author renders bare name, no label", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData())) + assert.Contains(t, got, ``) + assert.Contains(t, got, "Alice") + assert.NotContains(t, got, "avatar_stack_and") + assert.NotContains(t, got, "avatar_stack_people") + }) + + t.Run("two participants use and label", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData(mkCo("Bob", "bob@example.com")))) + assert.Contains(t, got, "repo.commits.avatar_stack_and") + assert.Contains(t, got, "Bob") + assert.NotContains(t, got, "avatar_stack_people") + assert.Contains(t, got, ``) + }) + + t.Run("three participants switch to N people label with tippy popup", func(t *testing.T) { + got := string(ut.AvatarStackWithNames(mkData(mkCo("Bob", "bob@example.com"), mkCo("Carol", "carol@example.com")))) + assert.Contains(t, got, "repo.commits.avatar_stack_people:3") + assert.NotContains(t, got, "repo.commits.avatar_stack_and") + assert.Contains(t, got, `data-global-init="initAvatarStackPopup"`) + assert.Contains(t, got, `
    `) + assert.Contains(t, got, `class="avatar-stack-popup"`) + }) + + t.Run("overflow chip renders beyond 10 participants", func(t *testing.T) { + cos := make([]*git.CommitIdentity, 0, renderAvatarStackMaxVisible+1) + for i := range renderAvatarStackMaxVisible + 1 { + cos = append(cos, mkCo("X", strconv.Itoa(i)+"@example.com")) + } + got := ut.AvatarStack(gituser.BuildAvatarStackData(t.Context(), cos, &user_model.EmailUserMap{})) + assert.Contains(t, got, `class="avatar-stack-overflow-chip`) + assert.Contains(t, got, "+1") + }) +} diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9595baebed9..d9139f17e31 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2205,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\", whether they match the committer or not. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" if not.", "repo.settings.trust_model.committer": "Committer", "repo.settings.trust_model.committer.long": "Committer: Trust signatures that match committers. This matches GitHub's behavior and will force commits signed by Gitea to have Gitea as the committer.", - "repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces Gitea to be the committer on signed commits, with the actual committer marked as Co-authored-by: and Co-committed-by: trailer in the commit. The default Gitea key must match a user in the database.", + "repo.settings.trust_model.committer.desc": "Valid signatures will only be marked \"trusted\" if they match the committer, otherwise they will be marked \"unmatched\". This forces Gitea to be the committer on signed commits, with the actual committer marked as a Co-authored-by: trailer in the commit. The default Gitea key must match a user in the database.", "repo.settings.trust_model.collaboratorcommitter": "Collaborator+Committer", "repo.settings.trust_model.collaboratorcommitter.long": "Collaborator+Committer: Trust signatures by collaborators which match the committer", - "repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force Gitea to be marked as the committer on signed commits, with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a user in the database.", + "repo.settings.trust_model.collaboratorcommitter.desc": "Valid signatures by collaborators of this repository will be marked \"trusted\" if they match the committer. Otherwise, valid signatures will be marked \"untrusted\" if the signature matches the committer and \"unmatched\" otherwise. This will force Gitea to be marked as the committer on signed commits, with the actual committer marked as a Co-Authored-By: trailer in the commit. The default Gitea key must match a user in the database.", "repo.settings.wiki_delete": "Delete Wiki Data", "repo.settings.wiki_delete_desc": "Deleting repository wiki data is permanent and cannot be undone.", "repo.settings.wiki_delete_notices_1": "- This will permanently delete and disable the repository wiki for %s.", @@ -2599,6 +2599,9 @@ "repo.diff.review.reject": "Request changes", "repo.diff.review.self_approve": "Pull request authors can't approve their own pull request", "repo.diff.committed_by": "committed by", + "repo.diff.coauthored_by": "co-authored by", + "repo.commits.avatar_stack_and": "and", + "repo.commits.avatar_stack_people": "%d people", "repo.diff.protected": "Protected", "repo.diff.image.side_by_side": "Side by Side", "repo.diff.image.swipe": "Swipe", diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 87c1ffdb2af..294bd7e1cb6 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -15,6 +15,7 @@ import ( "gitea.dev/models/asymkey" "gitea.dev/models/db" + "gitea.dev/models/gituser" user_model "gitea.dev/models/user" "gitea.dev/modules/badge" "gitea.dev/modules/charset" @@ -61,8 +62,8 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { mockUser := mockUsers[0] commits = append(commits, &asymkey.SignCommit{ Verification: &asymkey.CommitVerification{}, - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -73,9 +74,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningKey: &asymkey.GPGKey{KeyID: "12345678"}, TrustStatus: "trusted", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -86,9 +87,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, TrustStatus: "untrusted", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -99,9 +100,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"}, TrustStatus: "other(unmatch)", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) commits = append(commits, &asymkey.SignCommit{ @@ -110,9 +111,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) { Reason: "gpg.error", SigningEmail: "test@example.com", }, - UserCommit: &user_model.UserCommit{ - User: mockUser, - Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, + UserCommit: &gituser.UserCommit{ + AuthorUser: mockUser, + GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()}, }, }) @@ -159,6 +160,59 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { ctx.Data["SelectedStyle"] = selectedStyle } +func prepareMockDataAvatarStack(ctx *context.Context) { + /* + mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 3}}) + if len(mockUsers) == 0 { + return + } + u0 := mockUsers[0] + u1, u2 := u0, u0 + if len(mockUsers) >= 2 { + u1 = mockUsers[1] + } + if len(mockUsers) >= 3 { + u2 = mockUsers[2] + } + + authorSig := func(u *user_model.User) *git.Signature { + return &git.Signature{Name: u.Name, Email: u.Email} + } + coLinked := func(u *user_model.User) *gituser.CommitParticipant { + return &gituser.CommitParticipant{GiteaUser: u, GitIdentity: authorSig(u)} + } + coUnlinked := func(name, email string) *gituser.CommitParticipant { + return &gituser.CommitParticipant{GitIdentity: &git.Signature{Name: name, Email: email}} + } + nUnlinked := func(n int) []*gituser.CommitParticipant { + out := make([]*gituser.CommitParticipant, n) + for i := range out { + out[i] = coUnlinked(fmt.Sprintf("Contributor %d", i+1), fmt.Sprintf("contrib%d@example.com", i+1)) + } + return out + } + + type scenario struct { + Label string + Data *gituser.AvatarStackData + } + mk := gituser.BuildAvatarStackData() + extSig := &git.Signature{Name: "External Contributor", Email: "external@example.com"} + ctx.Data["AvatarStackScenarios"] = []scenario{ + {Label: "linked author, no co-authors", Data: mk(u0, authorSig(u0), nil)}, + {Label: "unlinked author, no co-authors", Data: mk(nil, extSig, nil)}, + {Label: "1 linked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1)})}, + {Label: "1 unlinked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "2 co-authors (3 people), u1 author", Data: mk(u1, authorSig(u1), []*gituser.CommitParticipant{coLinked(u0), coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "3 co-authors mixed (4 people)", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1), coLinked(u2), coUnlinked("Bob Smith", "bob@example.com")})}, + {Label: "9 co-authors (max visible, no overflow), u2 author", Data: mk(u2, authorSig(u2), nUnlinked(9))}, + {Label: "10 co-authors (overflow +1)", Data: mk(u0, authorSig(u0), nUnlinked(10))}, + {Label: "15 co-authors (overflow +6), unlinked author", Data: mk(nil, extSig, nUnlinked(15))}, + {Label: "30 co-authors (overflow +21)", Data: mk(u0, authorSig(u0), nUnlinked(30))}, + } + */ +} + func prepareMockDataRelativeTime(ctx *context.Context) { now := time.Now() ctx.Data["TimeNow"] = now @@ -196,6 +250,8 @@ func prepareMockData(ctx *context.Context) { prepareMockDataToastAndMessage(ctx) case "/devtest/unicode-escape": prepareMockDataUnicodeEscape(ctx) + case "/devtest/avatar-stack": + prepareMockDataAvatarStack(ctx) } } diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 6b97a58c17e..457f9795a23 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -12,8 +12,8 @@ import ( "path" "strconv" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" - user_model "gitea.dev/models/user" "gitea.dev/modules/charset" "gitea.dev/modules/git" "gitea.dev/modules/git/languagestats" @@ -29,13 +29,14 @@ import ( type blameRow struct { RowNumber int - Avatar template.HTML PreviousSha string PreviousShaURL string CommitURL string CommitMessage string CommitSince template.HTML + AvatarStackData *gituser.AvatarStackData + Code template.HTML EscapeStatus *charset.EscapeStatus } @@ -174,9 +175,9 @@ func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error { return nil } -func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit { +func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*gituser.UserCommit { // store commit data by SHA to look up avatar info etc - commitNames := make(map[string]*user_model.UserCommit) + commitNames := make(map[string]*gituser.UserCommit) // and as blameParts can reference the same commits multiple // times, we cache the lookup work locally commits := make([]*git.Commit, 0, len(blameParts)) @@ -209,33 +210,28 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma } // populate commit email addresses to later look up avatars. - validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits) + userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, ctx.Repo.RepoLink, ctx.Repo.RefFullName) if err != nil { - ctx.ServerError("ValidateCommitsWithEmails", err) + ctx.ServerError("GetUserCommitsByGitCommits", err) return nil } - for _, c := range validatedCommits { - commitNames[c.ID.String()] = c + for _, c := range userCommits { + commitNames[c.GitCommit.ID.String()] = c } return commitNames } -func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) { - if commit.User != nil { - br.Avatar = avatarUtils.Avatar(commit.User, 18) - } else { - br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18) - } - +func renderBlameFillFirstBlameRow(ctx *context.Context, repoLink string, part *gitrepo.BlamePart, commit *gituser.UserCommit, br *blameRow) { + br.AvatarStackData = gituser.BuildAvatarStackData(ctx, commit.GitCommit.AllParticipantIdentities(), nil) br.PreviousSha = part.PreviousSha br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath)) br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha)) - br.CommitMessage = commit.MessageUTF8() - br.CommitSince = templates.TimeSince(commit.Author.When) + br.CommitMessage = commit.GitCommit.MessageUTF8() + br.CommitSince = templates.TimeSince(commit.GitCommit.Author.When) } -func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) { +func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*gituser.UserCommit) { language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) if err != nil { log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) @@ -243,7 +239,6 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa buf := &bytes.Buffer{} rows := make([]*blameRow, 0) - avatarUtils := templates.NewAvatarUtils(ctx) rowNumber := 0 // will be 1-based for _, part := range blameParts { for partLineIdx, line := range part.Lines { @@ -258,7 +253,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa } if partLineIdx == 0 { - renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br) + renderBlameFillFirstBlameRow(ctx, ctx.Repo.RepoLink, part, commitNames[part.Sha], br) } } } diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index ff3496629fa..d8953878184 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -14,6 +14,7 @@ import ( asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" repo_model "gitea.dev/models/repo" @@ -49,7 +50,7 @@ func RefCommits(ctx *context.Context) { switch { case len(ctx.Repo.TreePath) == 0: Commits(ctx) - case ctx.Repo.TreePath == "search": + case ctx.Repo.TreePath == "search": // FIXME: legacy dirty design, it conflicts with the FileHistory SearchCommits(ctx) default: FileHistory(ctx) @@ -396,7 +397,8 @@ func Diff(ctx *context.Context) { verification := asymkey_service.ParseCommitWithSignature(ctx, commit) ctx.Data["Verification"] = verification - ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit) + ctx.Data["Author"] = user_model.GetUserByGitAuthor(ctx, commit) + ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.AllParticipantIdentities(), nil).Participants[1:] ctx.Data["Parents"] = parents ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0 @@ -411,7 +413,7 @@ func Diff(ctx *context.Context) { err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note) if err == nil { ctx.Data["NoteCommit"] = note.Commit - ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) + ctx.Data["NoteAuthor"] = user_model.GetUserByGitAuthor(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)}) htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage) @@ -461,7 +463,7 @@ func RawDiff(ctx *context.Context) { } func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) { - commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository) + commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository, ctx.Repo.RefFullName) if err != nil { return nil, err } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 2b2848afd15..45735fc8fe3 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -391,14 +391,14 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g if useFirstCommitAsTitle { // the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one c := commits[len(commits)-1] - title = c.UserCommit.MessageTitle() + title = c.UserCommit.GitCommit.MessageTitle() } else { title = autoTitleFromBranchName(ci.HeadRef.ShortName()) } if len(commits) == 1 { c := commits[0] - content = c.MessageBody() + content = c.GitCommit.MessageBody() } var titleTrailer string diff --git a/routers/web/repo/compare_test.go b/routers/web/repo/compare_test.go index d5b67ebe56e..af0a735227a 100644 --- a/routers/web/repo/compare_test.go +++ b/routers/web/repo/compare_test.go @@ -9,8 +9,8 @@ import ( asymkey_model "gitea.dev/models/asymkey" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" issues_model "gitea.dev/models/issues" - user_model "gitea.dev/models/user" "gitea.dev/modules/git" "gitea.dev/modules/setting" git_service "gitea.dev/services/git" @@ -52,8 +52,8 @@ func TestNewPullRequestTitleContent(t *testing.T) { mockCommit := func(msg string) *git_model.SignCommitWithStatuses { return &git_model.SignCommitWithStatuses{ SignCommit: &asymkey_model.SignCommit{ - UserCommit: &user_model.UserCommit{ - Commit: &git.Commit{ + UserCommit: &gituser.UserCommit{ + GitCommit: &git.Commit{ CommitMessage: git.CommitMessage{MessageRaw: msg}, }, }, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 437eedc57f1..6df4e738c99 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -21,6 +21,7 @@ import ( asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" unit_model "gitea.dev/models/unit" user_model "gitea.dev/models/user" @@ -132,8 +133,11 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { ctx.ServerError("CalculateTrustStatus", err) return false } + + avatarStackData := gituser.BuildAvatarStackData(ctx, latestCommit.AllParticipantIdentities(), nil) + avatarStackData.SearchByEmailLink = gituser.RepoCommitSearchByEmailLink(ctx.Repo.RepoLink, ctx.Repo.RefFullName) + ctx.Data["LatestCommitAvatarStackData"] = avatarStackData ctx.Data["LatestCommitVerification"] = verification - ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit) statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll) if err != nil { diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index e0881dc9f1d..6e2133a9452 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -361,7 +361,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } - ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) + ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository, "") // no current ref sub path for wiki commit list if err != nil { ctx.ServerError("ConvertFromGitCommit", err) return nil, nil diff --git a/services/git/commit.go b/services/git/commit.go index 5708e162b99..bc0516b5d6f 100644 --- a/services/git/commit.go +++ b/services/git/commit.go @@ -9,6 +9,7 @@ import ( asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/container" @@ -17,14 +18,14 @@ import ( ) // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. -func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType) ([]*asymkey_model.SignCommit, error) { +func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, oldCommits []*gituser.UserCommit, repoTrustModel repo_model.TrustModelType) ([]*asymkey_model.SignCommit, error) { newCommits := make([]*asymkey_model.SignCommit, 0, len(oldCommits)) keyMap := map[string]bool{} emails := make(container.Set[string]) for _, c := range oldCommits { - if c.Committer != nil { - emails.Add(c.Committer.Email) + if c.GitCommit.Committer != nil { + emails.Add(c.GitCommit.Committer.Email) } } @@ -34,10 +35,10 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, } for _, c := range oldCommits { - committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? + committerUser := emailUsers.GetByEmail(c.GitCommit.Committer.Email) // FIXME: why GetUserCommitsByGitCommits uses "Author", but ParseCommitsWithSignature uses "Committer"? signCommit := &asymkey_model.SignCommit{ UserCommit: c, - Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser), + Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.GitCommit, committerUser), } isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) { @@ -52,15 +53,15 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, } // ConvertFromGitCommit converts git commits into SignCommitWithStatuses -func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) ([]*git_model.SignCommitWithStatuses, error) { - validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits) +func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository, currentRef git.RefName) ([]*git_model.SignCommitWithStatuses, error) { + userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, repo.Link(), currentRef) if err != nil { return nil, err } signedCommits, err := ParseCommitsWithSignature( ctx, repo, - validatedCommits, + userCommits, repo.GetTrustModel(), ) if err != nil { @@ -77,7 +78,7 @@ func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.Sig commit := &git_model.SignCommitWithStatuses{ SignCommit: c, } - statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptionsAll) + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.GitCommit.ID.String(), db.ListOptionsAll) if err != nil { return nil, err } diff --git a/services/issue/comments.go b/services/issue/comments.go index 46964f5f1a3..ff6c9fccf56 100644 --- a/services/issue/comments.go +++ b/services/issue/comments.go @@ -184,7 +184,7 @@ func LoadCommentPushCommits(ctx context.Context, c *issues_model.Comment) error } defer closer.Close() - c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo) + c.Commits, err = git_service.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo, "") // no current ref sub path for PR commit list if err != nil { log.Debug("ConvertFromGitCommit: %v", err) // no need to show 500 error to end user when the commit does not exist } else { diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 67ec5bb81cf..229730257f2 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -65,9 +65,7 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { } if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { - // add trailer - message = AddCommitMessageTailer(message, "Co-authored-by", sig.String()) - message = AddCommitMessageTailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used + message = AddCommitMessageTailer(message, git.CoAuthoredByTrailer, sig.String()) } cmdCommit := gitcmd.NewCommand("commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). diff --git a/services/pull/pull.go b/services/pull/pull.go index 0d4a10ae9ef..a601450ff25 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -917,7 +917,7 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ } for _, author := range authors { - stringBuilder.WriteString("Co-authored-by: ") + stringBuilder.WriteString(git.CoAuthoredByTrailer + ": ") stringBuilder.WriteString(author) stringBuilder.WriteRune('\n') } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index b36f5f192a8..553f4232e2a 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -300,12 +300,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit cmdCommitTree.AddOptionFormat("-S%s", key.KeyID) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { - // Add trailers - _, _ = messageBytes.WriteString("\n") - _, _ = messageBytes.WriteString("Co-authored-by: ") - _, _ = messageBytes.WriteString(committerSig.String()) - _, _ = messageBytes.WriteString("\n") - _, _ = messageBytes.WriteString("Co-committed-by: ") + _, _ = messageBytes.WriteString("\n" + git.CoAuthoredByTrailer + ": ") _, _ = messageBytes.WriteString(committerSig.String()) _, _ = messageBytes.WriteString("\n") } diff --git a/services/repository/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go index 82092f71f36..99f8222ca7c 100644 --- a/services/repository/gitgraph/graph_models.go +++ b/services/repository/gitgraph/graph_models.go @@ -13,8 +13,10 @@ import ( asymkey_model "gitea.dev/models/asymkey" "gitea.dev/models/db" git_model "gitea.dev/models/git" + "gitea.dev/models/gituser" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" + "gitea.dev/modules/container" "gitea.dev/modules/git" "gitea.dev/modules/log" asymkey_service "gitea.dev/services/asymkey" @@ -93,9 +95,7 @@ func (graph *Graph) AddCommit(row, column int, flowID int64, data []byte) error // before finally retrieving the latest status func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_model.Repository, gitRepo *git.Repository) error { var err error - var ok bool - - emails := map[string]*user_model.User{} + emailSet := make(container.Set[string]) keyMap := map[string]bool{} for _, c := range graph.Commits { @@ -106,14 +106,26 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_ if err != nil { return fmt.Errorf("GetCommit: %s Error: %w", c.Rev, err) } - if c.Commit.Author != nil { - email := c.Commit.Author.Email - if c.User, ok = emails[email]; !ok { - c.User, _ = user_model.GetUserByEmail(ctx, email) - emails[email] = c.User - } + emailSet.Add(c.Commit.Author.Email) } + for _, sig := range c.Commit.AllParticipantIdentities() { + emailSet.Add(sig.Email) + } + } + + emailUserMap, err := user_model.GetUsersByEmails(ctx, emailSet.Values()) + if err != nil { + log.Error("GetUsersByEmails: %v", err) + } + + for _, c := range graph.Commits { + if c.Commit == nil { + continue + } + + c.User = emailUserMap.GetByEmail(c.Commit.Author.Email) + c.AvatarStackData = gituser.BuildAvatarStackData(ctx, c.Commit.AllParticipantIdentities(), emailUserMap) c.Verification = asymkey_service.ParseCommitWithSignature(ctx, c.Commit) @@ -246,18 +258,19 @@ func newRefsFromRefNames(refNames []byte) []git.Reference { // Commit represents a commit at coordinate X, Y with the data type Commit struct { - Commit *git.Commit - User *user_model.User - Verification *asymkey_model.CommitVerification - Status *git_model.CommitStatus - Flow int64 - Row int - Column int - Refs []git.Reference - Rev string - Date time.Time - ShortRev string - Subject string + Commit *git.Commit + User *user_model.User // author + AvatarStackData *gituser.AvatarStackData + Verification *asymkey_model.CommitVerification + Status *git_model.CommitStatus + Flow int64 + Row int + Column int + Refs []git.Reference + Rev string + Date time.Time // author date from "%ad" + ShortRev string + Subject string } // OnlyRelation returns whether this a relation only commit diff --git a/templates/devtest/avatar-stack.tmpl b/templates/devtest/avatar-stack.tmpl new file mode 100644 index 00000000000..12d1ff953bf --- /dev/null +++ b/templates/devtest/avatar-stack.tmpl @@ -0,0 +1,18 @@ +{{template "devtest/devtest-header"}} +
    +
    +

    Avatar Stack

    + + + + {{range $s := .AvatarStackScenarios}} + + + + + {{end}} + +
    ScenarioRendered
    {{$s.Label}}{{ctx.RenderUtils.AvatarStackWithNames $s.Data}}
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/templates/devtest/badge-commit-sign.tmpl b/templates/devtest/badge-commit-sign.tmpl index a6677c44958..8cfb63a0832 100644 --- a/templates/devtest/badge-commit-sign.tmpl +++ b/templates/devtest/badge-commit-sign.tmpl @@ -4,7 +4,7 @@

    Commit Sign Badges

    {{range $commit := .MockCommits}}
    - {{template "repo/commit_sign_badge" dict "Commit" $commit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}} + {{template "repo/commit_sign_badge" dict "Commit" $commit.GitCommit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}} {{template "repo/commit_sign_badge" dict "CommitSignVerification" $commit.Verification}}
    {{end}} diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 8bdefa5d43e..d108ea33797 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -43,7 +43,7 @@
    - {{$row.Avatar}} + {{if $row.AvatarStackData}}{{ctx.RenderUtils.AvatarStack $row.AvatarStackData}}{{end}}
    + {{if .CommitOtherParticipants}} +
    + {{ctx.Locale.Tr "repo.diff.coauthored_by"}} + {{range $participant := .CommitOtherParticipants}} + {{$user := $participant.GiteaUser}} + {{$gitIdentity := $participant.GitIdentity}} + {{if $user}} + {{ctx.AvatarUtils.Avatar $user 20}} + {{$user.GetDisplayName}} + {{else}} + {{$gitName := $gitIdentity.Name}} + {{$gitEmail := $gitIdentity.Email}} + {{ctx.AvatarUtils.AvatarByEmail $gitEmail $gitName 20}} + {{if $gitEmail}} + {{$gitName}} + {{else}} + {{$gitName}} + {{end}} + {{end}} + {{end}} +
    + {{end}} + {{if .Verification}} {{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}} {{end}} diff --git a/templates/repo/commit_sign_badge.tmpl b/templates/repo/commit_sign_badge.tmpl index f63e4ec899d..bf8185fd0b5 100644 --- a/templates/repo/commit_sign_badge.tmpl +++ b/templates/repo/commit_sign_badge.tmpl @@ -64,10 +64,10 @@ so this template should be kept as small as possible, DO NOT put large component {{- if $verified -}} {{- if and $signingUser $signingUser.ID -}} {{svg "gitea-lock"}} - {{ctx.AvatarUtils.Avatar $signingUser 16}} + {{ctx.AvatarUtils.Avatar $signingUser 20}} {{- else -}} {{svg "gitea-lock-cog"}} - {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}} + {{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 20}} {{- end -}} {{- else -}} {{svg "gitea-unlock"}} diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index e79d189b8d3..7a99bc7ac2e 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -2,27 +2,21 @@ - + - + {{$commitRepoLink := $.RepoLink}}{{if $.CommitRepoLink}}{{$commitRepoLink = $.CommitRepoLink}}{{end}} - {{range $commit := .Commits}} + {{range $commit := $.Commits}} + {{$gitCommit := $commit.GitCommit}} + {{$commitID := $gitCommit.ID.String}} - {{if .Committer}} - - {{else}} - - {{end}} +
    {{ctx.Locale.Tr "repo.commits.author"}}{{ctx.Locale.Tr "repo.commits.author"}} {{StringUtils.ToUpper $.Repository.ObjectFormatName}}{{ctx.Locale.Tr "repo.commits.message"}}{{ctx.Locale.Tr "repo.commits.message"}} {{ctx.Locale.Tr "repo.commits.date"}}
    - - {{- if .User -}} - {{- ctx.AvatarUtils.Avatar .User 20 "tw-mr-2" -}} - {{- .User.GetShortDisplayNameLinkHTML -}} - {{- else -}} - {{- ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20 "tw-mr-2" -}} - {{- .Author.Name -}} - {{- end -}} - + {{ctx.RenderUtils.AvatarStackWithNames $commit.AvatarStackData}} {{$commitBaseLink := ""}} @@ -33,52 +27,48 @@ {{else}} {{$commitBaseLink = printf "%s/commit" $commitRepoLink}} {{end}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + {{template "repo/commit_sign_badge" dict "Commit" $gitCommit "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} {{if $.PageIsWiki}} - - {{$commit.MessageTitle | ctx.RenderUtils.RenderEmoji}} + + {{$gitCommit.MessageTitle | ctx.RenderUtils.RenderEmoji}} {{else}} - {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape $commit.ID.String)}} - - {{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.MessageUTF8 $commitLink $.Repository}} + {{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape $commitID)}} + + {{ctx.RenderUtils.RenderCommitMessageLinkSubject $gitCommit.MessageUTF8 $commitLink $.Repository}} {{end}} - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}} {{end}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{if $commit.MessageBody}} -
    {{ctx.RenderUtils.RenderCommitBody $commit.MessageUTF8 $.Repository}}
    + {{if $gitCommit.MessageBody}} +
    {{ctx.RenderUtils.RenderCommitBody $gitCommit.MessageUTF8 $.Repository}}
    {{end}} {{if $.CommitsTagsMap}} - {{range (index $.CommitsTagsMap .ID.String)}} + {{range (index $.CommitsTagsMap $commitID)}} {{- template "repo/tag/name" dict "AdditionalClasses" "tw-py-0" "RepoLink" $.Repository.Link "TagName" .TagName "IsRelease" (not .IsTag) -}} {{end}} {{end}}
    {{DateUtils.TimeSince .Committer.When}}{{DateUtils.TimeSince .Author.When}}{{DateUtils.TimeSince $gitCommit.Committer.When}} - + {{/* at the moment, wiki doesn't support these "view" links like "view at history point" */}} {{if not $.PageIsWiki}} {{/* view single file diff */}} {{if $.FileTreePath}} {{svg "octicon-file-diff"}} {{end}} {{/* view at history point */}} - {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape .ID.String)}} + {{$viewCommitLink := printf "%s/src/commit/%s" $commitRepoLink (PathEscape $commitID)}} {{if $.FileTreePath}}{{$viewCommitLink = printf "%s/%s" $viewCommitLink (PathEscapeSegments $.FileTreePath)}}{{end}} {{svg "octicon-file-code"}} {{end}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index c5f0d5b5900..2dd6727e93c 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -1,35 +1,32 @@ {{$index := 0}}
    -{{range $commit := .comment.Commits}} +{{range $commit := $.comment.Commits}} + {{$gitCommit := $commit.GitCommit}} {{$tag := printf "%s-%d" $.comment.HashTag $index}} {{$index = Eval $index "+" 1}}
    {{/*singular-commit*/}} {{svg "octicon-git-commit"}} - {{if .User}} - {{ctx.AvatarUtils.Avatar .User 20}} - {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}} - {{end}} + {{ctx.RenderUtils.AvatarStack $commit.AvatarStackData}} {{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}} - {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}} + {{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape $gitCommit.ID.String)}} - - {{- ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.MessageUTF8 $commitLink $.comment.Issue.PullRequest.BaseRepo -}} + + {{- ctx.RenderUtils.RenderCommitMessageLinkSubject $gitCommit.MessageUTF8 $commitLink $.comment.Issue.PullRequest.BaseRepo -}} - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}} {{end}} - {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}} - {{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}} + {{template "repo/commit_statuses" dict "Status" $commit.Status "Statuses" $commit.Statuses}} + {{template "repo/commit_sign_badge" dict "Commit" $gitCommit "CommitBaseLink" $commitBaseLink "CommitSignVerification" $commit.Verification}}
    - {{if $commit.MessageBody}} + {{if $gitCommit.MessageBody}}
    -		{{- ctx.RenderUtils.RenderCommitBody $commit.MessageUTF8 $.comment.Issue.PullRequest.BaseRepo -}}
    +		{{- ctx.RenderUtils.RenderCommitBody $gitCommit.MessageUTF8 $.comment.Issue.PullRequest.BaseRepo -}}
     	
    {{end}} {{end}} diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl index d86f73fe654..b9288da911c 100644 --- a/templates/repo/graph/commits.tmpl +++ b/templates/repo/graph/commits.tmpl @@ -41,14 +41,7 @@ - {{if $commit.User}} - {{ctx.AvatarUtils.Avatar $commit.User 18}} - {{$commit.User.GetShortDisplayNameLinkHTML}} - {{else}} - {{$gitUserName := $commit.Commit.Author.Name}} - {{ctx.AvatarUtils.AvatarByEmail $commit.Commit.Author.Email $gitUserName 18}} - {{$gitUserName}} - {{end}} + {{ctx.RenderUtils.AvatarStackWithNames $commit.AvatarStackData}} {{DateUtils.FullTime $commit.Date}} diff --git a/templates/repo/latest_commit.tmpl b/templates/repo/latest_commit.tmpl index c0518189b85..b2aebf0d42c 100644 --- a/templates/repo/latest_commit.tmpl +++ b/templates/repo/latest_commit.tmpl @@ -2,15 +2,7 @@ {{if not .LatestCommit}} … {{else}} - - {{- if .LatestCommitUser -}} - {{- ctx.AvatarUtils.Avatar .LatestCommitUser 20 "tw-mr-2" -}} - {{.LatestCommitUser.GetShortDisplayNameLinkHTML}} - {{- else if .LatestCommit.Author -}} - {{- ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 20 "tw-mr-2" -}} - {{.LatestCommit.Author.Name}} - {{- end -}} - + {{ctx.RenderUtils.AvatarStackWithNames .LatestCommitAvatarStackData}} {{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}} diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 9afb61887cf..ab02522c20f 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -89,10 +89,10 @@ {{$repo := .Repo}}
    {{range $pushCommit := $push.Commits}} - {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} + {{$commitLink := printf "%s/commit/%s" $repoLink $pushCommit.Sha1}}
    - - {{ShortSha .Sha1}} + {{ctx.RenderUtils.AvatarStackPushCommit $pushCommit}} + {{ShortSha $pushCommit.Sha1}} {{ctx.RenderUtils.RenderCommitMessage $pushCommit.Message $repo}} diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index 9335ef70656..b55e1372c95 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -38,11 +38,15 @@ func TestRepoCommits(t *testing.T) { doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) { commits = append(commits, path.Base(s.AttrOr("href", ""))) }) - doc.doc.Find("#commits-table .author-wrapper a").Each(func(i int, s *goquery.Selection) { + doc.doc.Find("#commits-table .avatar-stack-names a.muted").Each(func(i int, s *goquery.Selection) { userHrefs = append(userHrefs, s.AttrOr("href", "")) }) assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits) - assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs) + assert.Equal(t, []string{ + "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", + "/user2/repo16/commits/branch/master/search?q=author%3Auser21%40example.com", + "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", + }, userHrefs) }) t.Run("LastCommit", func(t *testing.T) { @@ -50,9 +54,9 @@ func TestRepoCommits(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") - authorHref := doc.doc.Find(".latest-commit .author-wrapper a").AttrOr("href", "") + authorHref := doc.doc.Find(".latest-commit .avatar-stack-names a").AttrOr("href", "") assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref) - assert.Equal(t, "/user2", authorHref) + assert.Equal(t, "/user2/repo16/commits/branch/master/search?q=author%3Auser2%40example.com", authorHref) }) t.Run("CommitListNonExistingCommiter", func(t *testing.T) { @@ -65,7 +69,7 @@ func TestRepoCommits(t *testing.T) { doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "") assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) - authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper") + authorElem := doc.doc.Find("#commits-table tr:first-child .avatar-stack-names") assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text())) }) @@ -97,7 +101,7 @@ func TestRepoCommits(t *testing.T) { doc := NewHTMLParser(t, resp.Body) commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "") assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref) - authorElem := doc.doc.Find(".latest-commit .author-wrapper") + authorElem := doc.doc.Find(".latest-commit .avatar-stack-names") assert.Equal(t, "6543", strings.TrimSpace(authorElem.Text())) }) } diff --git a/web_src/css/avatar.css b/web_src/css/avatar.css new file mode 100644 index 00000000000..3b34b8207f4 --- /dev/null +++ b/web_src/css/avatar.css @@ -0,0 +1,125 @@ +img.ui.avatar, +.ui.avatar img, +.ui.avatar svg { + border-radius: var(--border-radius); + object-fit: contain; + aspect-ratio: 1; +} + +.avatar-stack-names { + display: inline-flex; + align-items: center; + align-self: center; + gap: 4px; + white-space: nowrap; + vertical-align: middle; +} + +.avatar-stack-names > a.muted, +.avatar-stack-names > .avatar-stack-popup-trigger { + overflow: hidden; + text-overflow: ellipsis; + max-width: 240px; +} + +/* use semibold for latest commit author */ +.latest-commit .avatar-stack-names > a, +.latest-commit .avatar-stack-names > .avatar-stack-popup-trigger { + font-weight: var(--font-weight-semibold); +} + +/* template emits children reversed; row-reverse re-orders visually and keeps the author last-painted (on top) */ +.avatar-stack { + display: inline-flex; + align-items: center; + flex-direction: row-reverse; +} + +.avatar-stack > * { + margin-left: -16px; + transition: transform 0.15s ease, opacity 0.15s ease; + position: relative; + display: inline-flex; +} + +.avatar-stack > *:last-child { margin-left: 0; } +.avatar-stack > *:nth-last-child(2) { margin-left: -14px; } + +/* hover spreads via transform (no layout shift); positions count from visual-left = last DOM child = :nth-last-child */ +.avatar-stack:hover > *:nth-last-child(2) { transform: translateX(14px); } +.avatar-stack:hover > *:nth-last-child(3) { transform: translateX(30px); } +.avatar-stack:hover > *:nth-last-child(4) { transform: translateX(46px); } +.avatar-stack:hover > *:nth-last-child(5) { transform: translateX(62px); } +.avatar-stack:hover > *:nth-last-child(6) { transform: translateX(78px); } +.avatar-stack:hover > *:nth-last-child(7) { transform: translateX(94px); } +.avatar-stack:hover > *:nth-last-child(8) { transform: translateX(110px); } +.avatar-stack:hover > *:nth-last-child(9) { transform: translateX(126px); } +.avatar-stack:hover > *:nth-last-child(10) { transform: translateX(142px); } +.avatar-stack:hover > *:nth-last-child(11) { transform: translateX(158px); } + +.avatar-stack .avatar { + border: 1px solid var(--color-body); + background: var(--color-body); + transition: border-color 0.15s ease, background-color 0.15s ease; +} + +.avatar-stack:hover .avatar { + background-color: var(--color-body); +} + +.avatar-stack-overflow-chip { + align-items: center; + justify-content: center; + width: 0; + height: 20px; + margin-left: 0; + border: 0 solid var(--color-body); + border-radius: var(--border-radius); + color: var(--color-text); + font-weight: var(--font-weight-semibold); + overflow: hidden; + opacity: 0; + transition: all 0.15s ease; +} + +.avatar-stack:hover .avatar-stack-overflow-chip { + width: 20px; + margin-left: -16px; + border-width: 1px; + opacity: 1; +} + +.avatar-stack-popup-trigger { + cursor: pointer; + background: none; + border: none; + padding: 0; + font: inherit; + color: inherit; +} + +.avatar-stack-popup-trigger:hover { + color: var(--color-primary); +} + +.avatar-stack-popup { + min-width: 200px; + display: flex; + flex-direction: column; + padding: 4px 0; +} + +.avatar-stack-popup > a { + padding: 6px 12px; + gap: 8px; +} + +.avatar-stack-popup > a:hover { + background: var(--color-hover); +} + +@media (max-width: 767.98px) { + .avatar-stack-names { + max-width: 80px; + } +} diff --git a/web_src/css/base.css b/web_src/css/base.css index 0716ad1913e..8b8cfac2d1f 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -386,14 +386,6 @@ a.label, color: var(--color-text-light-2); } -img.ui.avatar, -.ui.avatar img, -.ui.avatar svg { - border-radius: var(--border-radius); - object-fit: contain; - aspect-ratio: 1; -} - .full.height { flex-grow: 1; padding-bottom: var(--page-space-bottom); diff --git a/web_src/css/index.css b/web_src/css/index.css index a56982efcf7..6d9280c67f8 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -56,6 +56,7 @@ @import "./font_i18n.css"; @import "./base.css"; +@import "./avatar.css"; @import "./home.css"; @import "./install.css"; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index b7cb5e1dcd1..0682290e1a6 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1386,8 +1386,7 @@ tbody.commit-list { vertical-align: baseline; } -.message-wrapper, -.author-wrapper { +.message-wrapper { overflow: hidden; text-overflow: ellipsis; max-width: 100%; @@ -1395,12 +1394,6 @@ tbody.commit-list { vertical-align: middle; } -.author-wrapper { - max-width: 180px; - align-self: center; - white-space: nowrap; -} - .latest-commit .message-wrapper { max-width: calc(100% - 2.5rem); } @@ -1415,9 +1408,6 @@ tbody.commit-list { tr.commit-list { width: 100%; } - .author-wrapper { - max-width: 80px; - } } @media (min-width: 768px) and (max-width: 991.98px) { diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts index 17130427a62..6103766202a 100644 --- a/web_src/js/features/repo-commit.ts +++ b/web_src/js/features/repo-commit.ts @@ -25,6 +25,22 @@ export function initCommitStatuses() { }); } +export function initAvatarStackPopup() { + registerGlobalInitFunc('initAvatarStackPopup', (el: HTMLElement) => { + const nextEl = el.nextElementSibling!; + if (!nextEl.matches('.tippy-target')) throw new Error('Expected next element to be a tippy target'); + createTippy(el, { + content: nextEl, + placement: 'bottom-start', + interactive: true, + role: 'dialog', + theme: 'menu', + trigger: 'click', + hideOnClick: true, + }); + }); +} + export function initCommitFileHistoryFollowRename() { registerGlobalInitFunc('initCommitHistoryFollowRename', (el: HTMLInputElement) => { el.addEventListener('change', () => { diff --git a/web_src/js/index.ts b/web_src/js/index.ts index a2994d6912d..6f081f30202 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -21,7 +21,7 @@ import {initMarkupContent} from './markup/content.ts'; import {initRepoFileView} from './features/file-view.ts'; import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth.ts'; import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses, initCommitFileHistoryFollowRename} from './features/repo-commit.ts'; +import {initRepoEllipsisButton, initCommitStatuses, initAvatarStackPopup, initCommitFileHistoryFollowRename} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; import {initAdminCommon} from './features/admin/common.ts'; import {initRepoCodeView} from './features/repo-code.ts'; @@ -146,6 +146,7 @@ const initPerformanceTracer = callInitFunctions([ initRepoRecentCommits, initCommitStatuses, + initAvatarStackPopup, initCaptcha, initUserCheckAppUrl, From ade76fe83868e9004be632148f7c625a91f3c1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Schl=C3=B6mer?= Date: Mon, 8 Jun 2026 19:58:41 +0200 Subject: [PATCH 36/88] enhance: allow MathML core elements (#38034) Fixes #36352. --------- Co-authored-by: wxiaoguang --- modules/markup/sanitizer_default.go | 32 ++++++++++++++++++++++++ modules/markup/sanitizer_default_test.go | 5 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index e38852a3d5a..9c7259dc8cc 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -63,6 +63,38 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("loading").OnElements("img") + // MathML Core (https://www.w3.org/TR/mathml-core/) + mathMLElements := []string{ + "math", + // token elements + "mi", "mn", "mo", "mtext", "mspace", "ms", + // layout elements + "mrow", "mfrac", "msqrt", "mroot", "mstyle", "merror", "mpadded", "mphantom", + // scripting elements + "msub", "msup", "msubsup", "munder", "mover", "munderover", "mmultiscripts", "mprescripts", "none", + // tabular elements + "mtable", "mtr", "mtd", + // semantic annotations + "semantics", "annotation", "annotation-xml", + } + policy.AllowAttrs("display", "alttext").OnElements("math") + policy.AllowAttrs( + // global presentation attributes + "dir", "displaystyle", "mathbackground", "mathcolor", "mathsize", "mathvariant", "scriptlevel", + // operator attributes + "accent", "accentunder", "fence", "form", "largeop", "lspace", "maxsize", "minsize", "movablelimits", "rspace", "separator", "stretchy", "symmetric", + // space and padding attributes + "depth", "height", "voffset", "width", + // fraction attribute + "linethickness", + // table attributes + "columnalign", "columnlines", "columnspacing", "frame", "framespacing", "rowalign", "rowlines", "rowspacing", + // cell attributes + "columnspan", + // annotation attribute + "encoding", + ).OnElements(mathMLElements...) + // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index e66f00c02f6..e344a96722f 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -61,6 +61,9 @@ func TestSanitizer(t *testing.T) { // picture `c`, `c`, + // MathML + ``, ``, + // Disallow dangerous url schemes `bad`, `bad`, `bad`, `bad`, @@ -72,6 +75,6 @@ func TestSanitizer(t *testing.T) { } for i := 0; i < len(testCases); i += 2 { - assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i]))) + assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i])), "input: %s", testCases[i]) } } From d76a974b241cea7d9320bf0fdd4eb1c1e1271da7 Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Mon, 8 Jun 2026 20:18:58 +0200 Subject: [PATCH 37/88] feat(ssh): auto generate additional ssh keys (#33974) adds capabilities for gitea to generate ecdsa and ed25519 keys by default adds cli for built-in ssh key generation helpers closes: https://github.com/go-gitea/gitea/issues/33783 --------- Co-authored-by: Nicolas Co-authored-by: wxiaoguang Co-authored-by: Giteabot --- cmd/generate.go | 76 +++++++++++ cmd/helper.go | 17 +-- cmd/mailer.go | 8 +- modules/consts/asymkey.go | 12 ++ modules/generate/generate.go | 80 ++++++++++++ modules/setting/ssh.go | 5 +- modules/ssh/ssh.go | 101 +++++++------- modules/ssh/ssh_test.go | 123 ++++++++++++++++++ .../git_helper_for_declarative_test.go | 3 +- 9 files changed, 351 insertions(+), 74 deletions(-) create mode 100644 modules/consts/asymkey.go create mode 100644 modules/ssh/ssh_test.go diff --git a/cmd/generate.go b/cmd/generate.go index 01be73c2d11..239b75ba9c2 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -6,10 +6,12 @@ package cmd import ( "context" + "errors" "fmt" "os" "gitea.dev/modules/generate" + "gitea.dev/modules/ssh" "github.com/mattn/go-isatty" "github.com/urfave/cli/v3" @@ -21,6 +23,7 @@ func newGenerateCommand() *cli.Command { Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ newGenerateSecretCommand(), + newGenerateSSHCommand(), }, } } @@ -37,6 +40,17 @@ func newGenerateSecretCommand() *cli.Command { } } +func newGenerateSSHCommand() *cli.Command { + return &cli.Command{ + Name: "ssh", + Usage: "Generate ssh keys", + Commands: []*cli.Command{ + newGenerateSSHKeyCommand(), + newGenerateSSHHostKeysCommand(), + }, + } +} + func newGenerateInternalTokenCommand() *cli.Command { return &cli.Command{ Name: "INTERNAL_TOKEN", @@ -62,6 +76,30 @@ func newGenerateSecretKeyCommand() *cli.Command { } } +func newGenerateSSHKeyCommand() *cli.Command { + return &cli.Command{ + Name: "key", + Usage: "Generate a new ssh key", + Flags: []cli.Flag{ + &cli.IntFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"}, + &cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Specifies the type of key to create."}, + &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the path or base directory for the key file", Required: true}, + }, + Action: runGenerateKeyPair, + } +} + +func newGenerateSSHHostKeysCommand() *cli.Command { + return &cli.Command{ + Name: "host-keys", + Usage: "Generate host keys of all default key types (rsa, ecdsa, and ed25519) if they do not already exist.", + Flags: []cli.Flag{ + &cli.StringFlag{Name: "dir", Aliases: []string{"d"}, Usage: "Specifies the base directory for the key files", Required: true}, + }, + Action: runGenerateHostKey, + } +} + func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() if err != nil { @@ -103,3 +141,41 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error { return nil } + +func runGenerateHostKey(_ context.Context, c *cli.Command) error { + file := c.String("dir") + info, err := os.Stat(file) + if errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(file, 0o644); err != nil { + return err + } + } else if err != nil { + return err + } else if !info.IsDir() { + return errors.New("file already exists and is not a directory") + } + fmt.Fprintf(c.Writer, "Generating host keys in %s\n", file) + _, err = ssh.InitDefaultHostKeys(file) + return err +} + +func runGenerateKeyPair(_ context.Context, c *cli.Command) error { + file := c.String("file") + keyType := c.String("type") + + fmt.Fprintf(c.Writer, "Generating public/private %s key pair.\n", keyType) + + // Check if file exists to prevent overwriting + if _, err := os.Stat(file); err == nil { + if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) { + fmt.Println("Aborting") + return nil + } + } + bits := c.Int("bits") + err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits) + if err == nil { + fmt.Printf("Your SSH key has been saved in %s\n", file) + } + return err +} diff --git a/cmd/helper.go b/cmd/helper.go index 9150e1c2338..37b20104379 100644 --- a/cmd/helper.go +++ b/cmd/helper.go @@ -38,22 +38,15 @@ func argsSet(c *cli.Command, args ...string) error { } // confirm waits for user input which confirms an action -func confirm() (bool, error) { +func confirm(stdin io.Reader, stdout io.Writer, msg string, args ...any) bool { var response string - - _, err := fmt.Scanln(&response) - if err != nil { - return false, err - } - + _, _ = fmt.Fprintf(stdout, msg, args...) + _, _ = fmt.Fscanln(stdin, &response) switch strings.ToLower(response) { case "y", "yes": - return true, nil - case "n", "no": - return false, nil - default: - return false, errors.New(response + " isn't a correct confirmation string") + return true } + return false } func initDB(ctx context.Context) error { diff --git a/cmd/mailer.go b/cmd/mailer.go index 61bd66c963a..d7b2f6b7bb1 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -22,14 +22,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error { if !confirmSkipped { if len(body) == 0 { - fmt.Print("warning: Content is empty") + fmt.Println("warning: Content is empty") } - fmt.Print("Proceed with sending email? [Y/n] ") - isConfirmed, err := confirm() - if err != nil { - return err - } else if !isConfirmed { + if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") { fmt.Println("The mail was not sent") return nil } diff --git a/modules/consts/asymkey.go b/modules/consts/asymkey.go new file mode 100644 index 00000000000..d6f19b2c53c --- /dev/null +++ b/modules/consts/asymkey.go @@ -0,0 +1,12 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package consts + +const ( + AsymKeyMinBitsRsa = 3071 // 3072-1 to tolerate the leading zero + AsymKeyMinBitsEC = 256 + + AsymKeyDefaultBitsRsa = 4096 // ssh-keygen command defaults to 3072 + AsymKeyDefaultBitsEcdsa = 256 +) diff --git a/modules/generate/generate.go b/modules/generate/generate.go index f2a5b366d8f..ca132e0756c 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -5,15 +5,23 @@ package generate import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "encoding/base64" + "encoding/pem" "fmt" "io" "time" + "gitea.dev/modules/consts" "gitea.dev/modules/util" "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/ssh" ) // NewInternalToken generate a new value intended to be used by INTERNAL_TOKEN. @@ -67,3 +75,75 @@ func NewJwtSecretWithBase64() ([]byte, string) { func NewSecretKey() (string, error) { return util.CryptoRandomString(64), nil } + +type SSHKeyType string + +const ( + SSHKeyRSA SSHKeyType = "rsa" + SSHKeyECDSA SSHKeyType = "ecdsa" + SSHKeyED25519 SSHKeyType = "ed25519" +) + +func NewSSHKey(keyType SSHKeyType, bits int) (ssh.PublicKey, *pem.Block, error) { + pub, priv, err := commonKeyGen(keyType, bits) + if err != nil { + return nil, nil, err + } + pemPriv, err := ssh.MarshalPrivateKey(priv, "") + if err != nil { + return nil, nil, err + } + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + return nil, nil, err + } + + return sshPub, pemPriv, nil +} + +// commonKeyGen is an abstraction over rsa, ecdsa, and ed25519 generating functions +func commonKeyGen(keyType SSHKeyType, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch keyType { + case SSHKeyRSA: + bits = util.IfZero(bits, consts.AsymKeyDefaultBitsRsa) + if bits < consts.AsymKeyMinBitsRsa { + return nil, nil, util.NewInvalidArgumentErrorf("invalid rsa bits: %d", bits) + } + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil + case SSHKeyED25519: + return ed25519.GenerateKey(rand.Reader) + case SSHKeyECDSA: + bits = util.IfZero(bits, consts.AsymKeyDefaultBitsEcdsa) + if bits < consts.AsymKeyMinBitsEC { + return nil, nil, util.NewInvalidArgumentErrorf("invalid elliptic-curve bits: %d", bits) + } + curve, err := getEllipticCurve(bits) + if err != nil { + return nil, nil, err + } + privateKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil + default: + return nil, nil, util.NewInvalidArgumentErrorf("unknown key type: %s", keyType) + } +} + +func getEllipticCurve(bits int) (elliptic.Curve, error) { + switch bits { + case 256: + return elliptic.P256(), nil + case 384: + return elliptic.P384(), nil + case 521: + return elliptic.P521(), nil + default: + return nil, util.NewInvalidArgumentErrorf("unsupported elliptic-curve bits: %d", bits) + } +} diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index 948ce773c52..683c90f2243 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -9,6 +9,7 @@ import ( "text/template" "time" + "gitea.dev/modules/consts" "gitea.dev/modules/log" "gitea.dev/modules/util" @@ -52,8 +53,8 @@ var SSH = struct { Domain: "", Port: 22, MinimumKeySizeCheck: true, - MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071}, - ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"}, + MinimumKeySizes: map[string]int{"ed25519": consts.AsymKeyMinBitsEC, "ed25519-sk": consts.AsymKeyMinBitsEC, "ecdsa": consts.AsymKeyMinBitsEC, "ecdsa-sk": consts.AsymKeyMinBitsEC, "rsa": consts.AsymKeyMinBitsRsa}, + ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gitea.ed25519", "ssh/gitea.ecdsa", "ssh/gogs.rsa"}, AuthorizedKeysCommandTemplate: "{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}", PerWriteTimeout: PerWriteTimeout, PerWritePerKbTimeout: PerWritePerKbTimeout, diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 94e29969b02..78e4b0805b3 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -6,9 +6,6 @@ package ssh import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" "encoding/pem" "errors" "io" @@ -23,11 +20,11 @@ import ( "syscall" asymkey_model "gitea.dev/models/asymkey" + "gitea.dev/modules/generate" "gitea.dev/modules/graceful" "gitea.dev/modules/log" "gitea.dev/modules/process" "gitea.dev/modules/setting" - "gitea.dev/modules/util" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" @@ -59,7 +56,7 @@ func getExitStatusFromError(err error) int { return 0 } - exitErr, ok := err.(*exec.ExitError) + exitErr, ok := errors.AsType[*exec.ExitError](err) if !ok { return 1 } @@ -322,7 +319,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { } // sshConnectionFailed logs a failed connection -// - this mainly exists to give a nice function name in logging +// - this mainly exists to give a nice function name in logging func sshConnectionFailed(conn net.Conn, err error) { // Log the underlying error with a specific message log.Warn("Failed connection from %s with error: %v", conn.RemoteAddr(), err) @@ -351,40 +348,37 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) { }, } - keys := make([]string, 0, len(setting.SSH.ServerHostKeys)) + hostKeyFiles := make([]string, 0, len(setting.SSH.ServerHostKeys)) for _, key := range setting.SSH.ServerHostKeys { - isExist, err := util.IsExist(key) + _, err := os.Stat(key) if err != nil { - log.Fatal("Unable to check if %s exists. Error: %v", setting.SSH.ServerHostKeys, err) - } - if isExist { - keys = append(keys, key) + if !errors.Is(err, os.ErrNotExist) { + log.Fatal("Unable to check if %s exists. Error: %v", setting.SSH.ServerHostKeys, err) + } + continue } + hostKeyFiles = append(hostKeyFiles, key) } - if len(keys) == 0 { - filePath := filepath.Dir(setting.SSH.ServerHostKeys[0]) - - if err := os.MkdirAll(filePath, os.ModePerm); err != nil { - log.Error("Failed to create dir %s: %v", filePath, err) + if len(hostKeyFiles) == 0 { + hostKeyDir := filepath.Dir(setting.SSH.ServerHostKeys[0]) + err := os.MkdirAll(hostKeyDir, os.ModePerm) + if err != nil { + log.Error("Failed to create dir %s: %v", hostKeyDir, err) } - - err := GenKeyPair(setting.SSH.ServerHostKeys[0]) + hostKeyFiles, err = InitDefaultHostKeys(hostKeyDir) if err != nil { log.Fatal("Failed to generate private key: %v", err) } - log.Trace("New private key is generated: %s", setting.SSH.ServerHostKeys[0]) - keys = append(keys, setting.SSH.ServerHostKeys[0]) } - for _, key := range keys { - log.Info("Adding SSH host key: %s", key) - err := srv.SetOption(ssh.HostKeyFile(key)) + for _, keyFile := range hostKeyFiles { + log.Info("Adding SSH host key: %s", keyFile) + err := srv.SetOption(ssh.HostKeyFile(keyFile)) if err != nil { log.Error("Failed to set Host Key. %s", err) } } - go func() { _, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().HammerContext(), "Service: Built-in SSH server", process.SystemProcessType, true) defer finished() @@ -395,43 +389,44 @@ func Listen(host string, port int, ciphers, keyExchanges, macs []string) { // GenKeyPair make a pair of public and private keys for SSH access. // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. // Private Key generated is PEM encoded -func GenKeyPair(keyPath string) error { - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) +func GenKeyPair(keyPath string, keyType generate.SSHKeyType, bits int) error { + publicKey, privateKeyPEM, err := generate.NewSSHKey(keyType, bits) if err != nil { return err } - privateKeyPEM := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)} - f, err := os.OpenFile(keyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - defer func() { - if err = f.Close(); err != nil { - log.Error("Close: %v", err) - } - }() - - if err := pem.Encode(f, privateKeyPEM); err != nil { - return err - } - - // generate public key - pub, err := gossh.NewPublicKey(&privateKey.PublicKey) + public := gossh.MarshalAuthorizedKey(publicKey) + privateKeyBuf := &bytes.Buffer{} + err = pem.Encode(privateKeyBuf, privateKeyPEM) if err != nil { return err } - public := gossh.MarshalAuthorizedKey(pub) - p, err := os.OpenFile(keyPath+".pub", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) + err = os.WriteFile(keyPath, privateKeyBuf.Bytes(), 0o600) if err != nil { return err } - defer func() { - if err = p.Close(); err != nil { - log.Error("Close: %v", err) - } - }() - _, err = p.Write(public) - return err + + return os.WriteFile(keyPath+".pub", public, 0o644) +} + +// InitDefaultHostKeys mirrors how ssh-keygen -A operates +// it runs checks if public and private keys are already defined and creates new ones if not present +// key naming does not follow the OpenSSH convention due to existing settings being gitea.{KeyType} so generation follows gitea convention +func InitDefaultHostKeys(path string) (keyFiles []string, _ error) { + var errs []error + keyTypes := []generate.SSHKeyType{generate.SSHKeyRSA, generate.SSHKeyECDSA, generate.SSHKeyED25519} + for _, keyType := range keyTypes { + keyPath := filepath.Join(path, "gitea."+string(keyType)) + _, errStatPriv := os.Stat(keyPath) + if errStatPriv != nil { + err := GenKeyPair(keyPath, keyType, 0) + if err != nil { + errs = append(errs, err) + continue + } + } + keyFiles = append(keyFiles, keyPath) + } + return keyFiles, errors.Join(errs...) } diff --git a/modules/ssh/ssh_test.go b/modules/ssh/ssh_test.go new file mode 100644 index 00000000000..ad9ac813d4c --- /dev/null +++ b/modules/ssh/ssh_test.go @@ -0,0 +1,123 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package ssh + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "os" + "path/filepath" + "testing" + + "gitea.dev/modules/generate" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + gossh "golang.org/x/crypto/ssh" +) + +func TestGenKeyPair(t *testing.T) { + testCases := []struct { + keyType generate.SSHKeyType + expectedType any + }{ + { + keyType: generate.SSHKeyRSA, + expectedType: &rsa.PrivateKey{}, + }, + { + keyType: generate.SSHKeyED25519, + expectedType: &ed25519.PrivateKey{}, + }, + { + keyType: generate.SSHKeyECDSA, + expectedType: &ecdsa.PrivateKey{}, + }, + } + tmpDir := t.TempDir() + for _, tc := range testCases { + name := "gitea." + string(tc.keyType) + fn := filepath.Join(tmpDir, name) + t.Run("Generate "+name, func(t *testing.T) { + require.NoError(t, GenKeyPair(fn, tc.keyType, 0)) + + bytes, err := os.ReadFile(fn) + require.NoError(t, err) + + privateKey, err := gossh.ParseRawPrivateKey(bytes) + require.NoError(t, err) + assert.IsType(t, tc.expectedType, privateKey) + }) + } + t.Run("Generate unknown key type", func(t *testing.T) { + err := GenKeyPair(t.TempDir()+"gitea.badkey", "badkey", 0) + require.Error(t, err) + }) +} + +func TestInitKeys(t *testing.T) { + tempDir := t.TempDir() + + keyTypes := []string{"rsa", "ecdsa", "ed25519"} + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + assert.NoFileExists(t, privKeyPath) + assert.NoFileExists(t, pubKeyPath) + } + + // Test basic creation + keyFiles, err := InitDefaultHostKeys(tempDir) + require.NoError(t, err) + assert.Len(t, keyFiles, len(keyTypes)) + + metadata := map[string]os.FileInfo{} + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + info, err := os.Stat(privKeyPath) + require.NoError(t, err) + metadata[privKeyPath] = info + + info, err = os.Stat(pubKeyPath) + require.NoError(t, err) + metadata[pubKeyPath] = info + } + + // Test recreation on missing private key and noop for missing pub key + require.NoError(t, os.Remove(filepath.Join(tempDir, "gitea.ecdsa.pub"))) + require.NoError(t, os.Remove(filepath.Join(tempDir, "gitea.ed25519"))) + + keyFiles, err = InitDefaultHostKeys(tempDir) + require.NoError(t, err) + assert.Len(t, keyFiles, len(keyTypes)) + + for _, keyType := range keyTypes { + privKeyPath := filepath.Join(tempDir, "gitea."+keyType) + pubKeyPath := filepath.Join(tempDir, "gitea."+keyType+".pub") + + infoPriv, err := os.Stat(privKeyPath) + require.NoError(t, err) + + switch keyType { + case "rsa": + // No modification to RSA key + infoPub, err := os.Stat(pubKeyPath) + require.NoError(t, err) + assert.Equal(t, metadata[privKeyPath], infoPriv) + assert.Equal(t, metadata[pubKeyPath], infoPub) + case "ecdsa": + // ECDSA public key should be missing, private unchanged + assert.Equal(t, metadata[privKeyPath], infoPriv) + assert.NoFileExists(t, pubKeyPath) + case "ed25519": + // ed25519 private key was removed, so both keys regenerated + infoPub, err := os.Stat(pubKeyPath) + require.NoError(t, err) + assert.NotEqual(t, metadata[privKeyPath], infoPriv) + assert.NotEqual(t, metadata[pubKeyPath], infoPub) + } + } +} diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go index 3b374a094ee..bd0aedf6c91 100644 --- a/tests/integration/git_helper_for_declarative_test.go +++ b/tests/integration/git_helper_for_declarative_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "gitea.dev/modules/generate" "gitea.dev/modules/git" "gitea.dev/modules/git/gitcmd" "gitea.dev/modules/setting" @@ -33,7 +34,7 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) { assert.NoError(t, err) keyFile := filepath.Join(tmpDir, keyname) - err = ssh.GenKeyPair(keyFile) + err = ssh.GenKeyPair(keyFile, generate.SSHKeyECDSA, 0) assert.NoError(t, err) err = os.WriteFile(filepath.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+ From e01af366e2f7df7713f0a1448be7f2a01f90bfe8 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Mon, 8 Jun 2026 11:30:55 -0700 Subject: [PATCH 38/88] fix(deps): update npm dependencies (#38035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | @​codemirror/autocomplete | [`6.20.2` → `6.20.3`](https://renovatebot.com/diffs/npm/@codemirror%2fautocomplete/6.20.2/6.20.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@codemirror%2fautocomplete/6.20.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@codemirror%2fautocomplete/6.20.2/6.20.3?slim=true) | | [eslint-plugin-vue](https://eslint.vuejs.org) ([source](https://redirect.github.com/vuejs/eslint-plugin-vue)) | [`10.9.1` → `10.9.2`](https://renovatebot.com/diffs/npm/eslint-plugin-vue/10.9.1/10.9.2) | ![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-vue/10.9.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-vue/10.9.1/10.9.2?slim=true) | --- ### Release Notes
    vuejs/eslint-plugin-vue (eslint-plugin-vue) ### [`v10.9.2`](https://redirect.github.com/vuejs/eslint-plugin-vue/blob/HEAD/CHANGELOG.md#1092) [Compare Source](https://redirect.github.com/vuejs/eslint-plugin-vue/compare/v10.9.1...v10.9.2) ##### Patch Changes - Fixed [`vue/custom-event-name-casing`](https://eslint.vuejs.org/rules/custom-event-name-casing.html) to check segments of colon-separated event names like `update:foo-bar` ([#​3079](https://redirect.github.com/vuejs/eslint-plugin-vue/pull/3079)) - Fixed [`vue/one-component-per-file`](https://eslint.vuejs.org/rules/one-component-per-file.html) to not report functions not imported from Vue ([#​3063](https://redirect.github.com/vuejs/eslint-plugin-vue/pull/3063)) - Fixed [`vue/prefer-import-from-vue`](https://eslint.vuejs.org/rules/prefer-import-from-vue.html) to not report imports/exports of names that are not re-exported by `vue` ([#​3081](https://redirect.github.com/vuejs/eslint-plugin-vue/pull/3081)) - Fixed [`vue/return-in-computed-property`](https://eslint.vuejs.org/rules/return-in-computed-property.html) and [`vue/require-render-return`](https://eslint.vuejs.org/rules/require-render-return.html) to not report exhaustive switch statements when TypeScript type information is available ([#​3067](https://redirect.github.com/vuejs/eslint-plugin-vue/pull/3067))
    --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Only on Monday (`* * * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). --- package.json | 4 ++-- pnpm-lock.yaml | 60 +++++++++++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index cc6c270a842..139de62f5a4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@citation-js/plugin-bibtex": "0.7.21", "@citation-js/plugin-csl": "0.7.22", "@citation-js/plugin-software-formats": "0.6.2", - "@codemirror/autocomplete": "6.20.2", + "@codemirror/autocomplete": "6.20.3", "@codemirror/commands": "6.10.3", "@codemirror/lang-json": "6.0.2", "@codemirror/lang-markdown": "6.5.0", @@ -102,7 +102,7 @@ "eslint-plugin-regexp": "3.1.0", "eslint-plugin-sonarjs": "4.0.3", "eslint-plugin-unicorn": "64.0.0", - "eslint-plugin-vue": "10.9.1", + "eslint-plugin-vue": "10.9.2", "eslint-plugin-vue-scoped-css": "3.1.1", "eslint-plugin-wc": "3.1.0", "globals": "17.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce5d84c9823..d7489e4f595 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: 0.6.2 version: 0.6.2 '@codemirror/autocomplete': - specifier: 6.20.2 - version: 6.20.2 + specifier: 6.20.3 + version: 6.20.3 '@codemirror/commands': specifier: 6.10.3 version: 6.10.3 @@ -82,13 +82,13 @@ importers: version: 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) '@replit/codemirror-lang-nix': specifier: 6.0.1 - version: 6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) + version: 6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10) '@replit/codemirror-lang-svelte': specifier: 6.0.0 - version: 6.0.0(@codemirror/autocomplete@6.20.2)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) + version: 6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10) '@replit/codemirror-vscode-keymap': specifier: 6.0.2 - version: 6.0.2(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) + version: 6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) '@resvg/resvg-wasm': specifier: 2.6.2 version: 2.6.2 @@ -289,8 +289,8 @@ importers: specifier: 64.0.0 version: 64.0.0(eslint@10.4.1(jiti@2.7.0)) eslint-plugin-vue: - specifier: 10.9.1 - version: 10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) + specifier: 10.9.2 + version: 10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) eslint-plugin-vue-scoped-css: specifier: 3.1.1 version: 3.1.1(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))) @@ -446,8 +446,8 @@ packages: resolution: {integrity: sha512-3XQOO3u4WXY/7AWZyQ+9SuBzS8bYTlJ+NF1uCgrZO64g36nK5iIc5YV9cBl2TL2QhHF6S36nvAsXsj5fX9FeHw==} engines: {node: '>=14.0.0'} - '@codemirror/autocomplete@6.20.2': - resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} '@codemirror/commands@6.10.3': resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} @@ -2699,8 +2699,8 @@ packages: postcss-styl: optional: true - eslint-plugin-vue@10.9.1: - resolution: {integrity: sha512-cHB0Tf4Duvzwecwd/AqWzZvF/QszE13BhjVUpVXWCy9AeMR5GjkAjP3i85vqgLgOuTmkHR1OJ5oMeqLHtuw8zg==} + eslint-plugin-vue@10.9.2: + resolution: {integrity: sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -5053,7 +5053,7 @@ snapshots: '@citation-js/date': 0.5.1 '@citation-js/name': 0.4.2 - '@codemirror/autocomplete@6.20.2': + '@codemirror/autocomplete@6.20.3': dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -5083,7 +5083,7 @@ snapshots: '@codemirror/lang-css@6.3.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -5091,7 +5091,7 @@ snapshots: '@codemirror/lang-go@6.0.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -5099,7 +5099,7 @@ snapshots: '@codemirror/lang-html@6.4.11': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.12.3 @@ -5116,7 +5116,7 @@ snapshots: '@codemirror/lang-javascript@6.2.5': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.6 '@codemirror/state': 6.6.0 @@ -5126,7 +5126,7 @@ snapshots: '@codemirror/lang-jinja@6.0.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -5150,7 +5150,7 @@ snapshots: '@codemirror/lang-liquid@6.3.2': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -5161,7 +5161,7 @@ snapshots: '@codemirror/lang-markdown@6.5.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 @@ -5179,7 +5179,7 @@ snapshots: '@codemirror/lang-python@6.2.1': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -5200,7 +5200,7 @@ snapshots: '@codemirror/lang-sql@6.10.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -5225,7 +5225,7 @@ snapshots: '@codemirror/lang-xml@6.1.0': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 @@ -5234,7 +5234,7 @@ snapshots: '@codemirror/lang-yaml@6.1.3': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@lezer/common': 1.5.2 @@ -5754,9 +5754,9 @@ snapshots: '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 - '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.2)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': + '@replit/codemirror-lang-nix@6.0.1(@codemirror/autocomplete@6.20.3)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/lr@1.4.10)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 '@codemirror/view': 6.43.0 @@ -5764,9 +5764,9 @@ snapshots: '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 - '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.2)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': + '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.20.3)(@codemirror/lang-css@6.3.1)(@codemirror/lang-html@6.4.11)(@codemirror/lang-javascript@6.2.5)(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(@lezer/common@1.5.2)(@lezer/highlight@1.2.3)(@lezer/javascript@1.5.4)(@lezer/lr@1.4.10)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.5 @@ -5778,9 +5778,9 @@ snapshots: '@lezer/javascript': 1.5.4 '@lezer/lr': 1.4.10 - '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.2)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': + '@replit/codemirror-vscode-keymap@6.0.2(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.3)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.6)(@codemirror/search@6.7.0)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)': dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.3 '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.6 @@ -7708,7 +7708,7 @@ snapshots: postcss-selector-parser: 7.1.1 vue-eslint-parser: 10.4.0(eslint@10.4.1(jiti@2.7.0)) - eslint-plugin-vue@10.9.1(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): + eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.4.1(jiti@2.7.0)))(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@10.4.1(jiti@2.7.0))): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.7.0)) eslint: 10.4.1(jiti@2.7.0) From b1c088e9cf3fe587633c3d3a2554fe41c7bf7f51 Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 8 Jun 2026 20:49:06 +0200 Subject: [PATCH 39/88] enhance(actions): Make Summary UI more beautiful with more infos (#37824) ## Summary - Redesign the Actions run summary header to follow GitHub Actions layout: trigger info on the left, Status / Total duration / Artifacts columns inline on the right - Expose trigger user avatar, pull request link, and PR head branch info from the run view API - Update the workflow graph header to show the workflow filename (linked to the run workflow file) and `on: `, while keeping the jobs/dependencies/success stats line - Remove the redundant commit/workflow metadata row below the run title; that information now lives in the summary bar New: Old: Replaces https://github.com/go-gitea/gitea/pull/36721 --------- Co-authored-by: Giteabot --- options/locale/locale_en-US.json | 5 +- routers/web/devtest/mock_actions.go | 43 ++-- routers/web/repo/actions/view.go | 193 +++++++++++++--- routers/web/repo/actions/view_test.go | 62 ++++++ templates/repo/actions/view_component.tmpl | 4 + .../js/components/ActionRunSummaryView.vue | 206 ++++++++++++++++-- web_src/js/components/ActionRunView.ts | 2 + web_src/js/components/RepoActionView.vue | 73 ++++--- web_src/js/components/WorkflowGraph.vue | 39 +++- web_src/js/features/repo-actions.ts | 4 + web_src/js/modules/gitea-actions.ts | 6 + web_src/js/svg.ts | 2 + 12 files changed, 524 insertions(+), 115 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index d9139f17e31..a629f11adf1 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3804,7 +3804,10 @@ "actions.runs.latest": "Latest", "actions.runs.latest_attempt": "Latest attempt", "actions.runs.triggered_via": "Triggered via %s", - "actions.runs.total_duration": "Total duration:", + "actions.runs.rerun_triggered": "Re-run triggered", + "actions.runs.back_to_pull_request": "Back to pull request", + "actions.runs.back_to_workflow": "Back to workflow", + "actions.runs.total_duration": "Total duration", "actions.runs.workflow_dependencies": "Workflow Dependencies", "actions.runs.graph_jobs_count_1": "%d job", "actions.runs.graph_jobs_count_n": "%d jobs", diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 61f20a3ef48..185cdc8acbb 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -87,29 +87,39 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.TitleHTML = `mock run title link` resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10) resp.State.Run.CanDeleteArtifact = true - resp.State.Run.WorkflowID = "workflow-id" - resp.State.Run.WorkflowLink = "./workflow-link" + resp.State.Run.WorkflowID = "workflow-id.yml" resp.State.Run.TriggerEvent = "push" + user2, _ := user_model.GetUserByID(ctx, 2) + if user2 == nil { + user2 = &user_model.User{Name: "user2"} + } + user3, _ := user_model.GetUserByID(ctx, 3) + if user3 == nil { + user3 = &user_model.User{Name: "user3"} + } resp.State.Run.Commit = actions.ViewCommit{ ShortSha: "ccccdddd", Link: "./commit-link", Pusher: actions.ViewUser{ - DisplayName: "pusher user", - Link: "./pusher-link", + DisplayName: user2.GetDisplayName(), + Link: user2.HomeLink(), + AvatarLink: user2.AvatarLinkWithSize(ctx, 16), }, Branch: actions.ViewBranch{ - Name: "commit-branch", + Name: "user2:commit-branch", Link: "./branch-link", IsDeleted: false, }, } + resp.State.Run.PullRequest = &actions.ViewPullRequest{ + Index: "#37658", + Link: "./pull/37658", + } now := time.Now() currentAttemptNum := int64(1) if attemptID > 0 { currentAttemptNum = attemptID } - user2 := &user_model.User{Name: "user2"} - user3 := &user_model.User{Name: "user3"} attempts := []*actions_model.ActionRunAttempt{{ Attempt: 1, Status: actions_model.StatusSuccess, @@ -168,15 +178,16 @@ func MockActionsRunsJobs(ctx *context.Context) { } } resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{ - Attempt: attempt.Attempt, - Status: attempt.Status.String(), - Done: attempt.Status.IsDone(), - Link: link, - Current: current, - Latest: attempt.Attempt == latestAttempt.Attempt, - TriggeredAt: attempt.Created.AsTime().Unix(), - TriggerUserName: attempt.TriggerUser.GetDisplayName(), - TriggerUserLink: attempt.TriggerUser.HomeLink(), + Attempt: attempt.Attempt, + Status: attempt.Status.String(), + Done: attempt.Status.IsDone(), + Link: link, + Current: current, + Latest: attempt.Attempt == latestAttempt.Attempt, + TriggeredAt: attempt.Created.AsTime().Unix(), + TriggerUserName: attempt.TriggerUser.GetDisplayName(), + TriggerUserLink: attempt.TriggerUser.HomeLink(), + TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16), }) } isLatestAttempt := currentAttemptNum == latestAttempt.Attempt diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index da997444490..9f8477d4c03 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -20,14 +20,18 @@ import ( actions_model "gitea.dev/models/actions" "gitea.dev/models/db" git_model "gitea.dev/models/git" + issues_model "gitea.dev/models/issues" repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" "gitea.dev/modules/actions" "gitea.dev/modules/base" + "gitea.dev/modules/cache" "gitea.dev/modules/git" "gitea.dev/modules/httplib" + "gitea.dev/modules/json" "gitea.dev/modules/log" "gitea.dev/modules/storage" + api "gitea.dev/modules/structs" "gitea.dev/modules/templates" "gitea.dev/modules/translation" "gitea.dev/modules/util" @@ -306,6 +310,7 @@ type ViewResponse struct { Attempts []*ViewRunAttempt `json:"attempts"` Jobs []*ViewJob `json:"jobs"` Commit ViewCommit `json:"commit"` + PullRequest *ViewPullRequest `json:"pullRequest,omitempty"` // Summary view: run duration and trigger time/event Duration string `json:"duration"` TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time @@ -340,15 +345,21 @@ type ViewJob struct { } type ViewRunAttempt struct { - Attempt int64 `json:"attempt"` - Status string `json:"status"` - Done bool `json:"done"` - Link string `json:"link"` - Current bool `json:"current"` - Latest bool `json:"latest"` - TriggeredAt int64 `json:"triggeredAt"` - TriggerUserName string `json:"triggerUserName"` - TriggerUserLink string `json:"triggerUserLink"` + Attempt int64 `json:"attempt"` + Status string `json:"status"` + Done bool `json:"done"` + Link string `json:"link"` + Current bool `json:"current"` + Latest bool `json:"latest"` + TriggeredAt int64 `json:"triggeredAt"` + TriggerUserName string `json:"triggerUserName"` + TriggerUserLink string `json:"triggerUserLink"` + TriggerUserAvatar string `json:"triggerUserAvatar"` +} + +type ViewPullRequest struct { + Index string `json:"index"` + Link string `json:"link"` } type ViewCommit struct { @@ -361,6 +372,7 @@ type ViewCommit struct { type ViewUser struct { DisplayName string `json:"displayName"` Link string `json:"link"` + AvatarLink string `json:"avatarLink,omitempty"` } type ViewBranch struct { @@ -388,6 +400,132 @@ type ViewStepLogLine struct { Timestamp float64 `json:"timestamp"` } +func viewPullRequestFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) *ViewPullRequest { + if run.Repo == nil { + return nil + } + refName := git.RefName(run.Ref) + if refName.IsPull() { + return &ViewPullRequest{ + Index: "#" + refName.ShortName(), + Link: run.RefLink(), + } + } + if prPayload != nil && prPayload.Index > 0 { + return &ViewPullRequest{ + Index: fmt.Sprintf("#%d", prPayload.Index), + Link: fmt.Sprintf("%s/pulls/%d", run.Repo.Link(), prPayload.Index), + } + } + // Push-triggered run: surface an open PR whose head matches this branch so + // users coming from a PR's check details can navigate back to it. + if refName.IsBranch() { + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, run.RepoID, refName.ShortName()) + if err != nil { + log.Error("GetUnmergedPullRequestsByHeadInfo: %v", err) + } else if len(prs) == 1 { + pr := prs[0] + if err := pr.LoadBaseRepo(ctx); err != nil { + log.Error("LoadBaseRepo: %v", err) + return nil + } + return &ViewPullRequest{ + Index: fmt.Sprintf("#%d", pr.Index), + Link: fmt.Sprintf("%s/pulls/%d", pr.BaseRepo.Link(), pr.Index), + } + } + } + return nil +} + +func viewSummaryBranchFromRun(ctx context.Context, run *actions_model.ActionRun, prPayload *api.PullRequestPayload) ViewBranch { + refName := git.RefName(run.Ref) + if prPayload != nil && prPayload.PullRequest != nil && prPayload.PullRequest.Head != nil { + head := prPayload.PullRequest.Head + name := head.Name + if name == "" { + name = git.RefName(head.Ref).ShortName() + } + if head.Repository != nil && run.Repo != nil && head.RepoID > 0 && head.RepoID != run.Repo.ID { + ownerName := "" + if head.Repository.Owner != nil { + ownerName = head.Repository.Owner.UserName + } else if head.Repository.FullName != "" { + ownerName, _, _ = strings.Cut(head.Repository.FullName, "/") + } + if ownerName != "" && !strings.Contains(name, ":") { + name = ownerName + ":" + name + } + } + link := "" + if head.Repository != nil && head.Ref != "" { + repoLink := head.Repository.Link + if repoLink == "" { + repoLink = head.Repository.HTMLURL + } + if repoLink != "" { + link = repoLink + "/src/" + git.RefName(head.Ref).RefWebLinkPath() + } + } + return ViewBranch{Name: name, Link: link} + } + + branch := ViewBranch{ + Name: run.PrettyRef(), + Link: run.RefLink(), + } + if refName.IsBranch() { + b, err := git_model.GetBranch(ctx, run.RepoID, refName.ShortName()) + if err != nil && !git_model.IsErrBranchNotExist(err) { + log.Error("GetBranch: %v", err) + } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { + branch.IsDeleted = true + } + } + return branch +} + +// actionsSummaryRefCacheTTL bounds how long the resolved PR/branch summary is +// cached. ViewPost is polled every second, but this metadata is stable for a +// run, so a short TTL collapses the repeated DB lookups while staying fresh +// enough for the navigation links. +const actionsSummaryRefCacheTTL = 10 // seconds + +type viewSummaryRefInfo struct { + PullRequest *ViewPullRequest `json:"pullRequest"` + Branch ViewBranch `json:"branch"` +} + +// getViewSummaryRefInfo resolves the run's pull request and head branch summary, +// caching the result briefly so the per-second poll does not hit the database on +// every request (GetUnmergedPullRequestsByHeadInfo / GetBranch). +func getViewSummaryRefInfo(ctx context.Context, run *actions_model.ActionRun) viewSummaryRefInfo { + compute := func() viewSummaryRefInfo { + // parse the event payload once and share it between both resolvers + prPayload, _ := run.GetPullRequestEventPayload() // nil unless this is a pull request event + return viewSummaryRefInfo{ + PullRequest: viewPullRequestFromRun(ctx, run, prPayload), + Branch: viewSummaryBranchFromRun(ctx, run, prPayload), + } + } + c := cache.GetCache() + if c == nil { + return compute() + } + cacheKey := fmt.Sprintf("actions_run_summary_ref:%d", run.ID) + if cached, ok := c.Get(cacheKey); ok && cached != "" { + var info viewSummaryRefInfo + if err := json.Unmarshal([]byte(cached), &info); err == nil { + return info + } + } + info := compute() + if data, err := json.Marshal(info); err == nil { + _ = c.Put(cacheKey, string(data), actionsSummaryRefCacheTTL) + } + return info +} + func ViewPost(ctx *context_module.Context) { run, attempt, jobs := getCurrentRunJobsByPathParam(ctx) if ctx.Written() { @@ -482,42 +620,33 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, } for _, runAttempt := range attempts { resp.State.Run.Attempts = append(resp.State.Run.Attempts, &ViewRunAttempt{ - Attempt: runAttempt.Attempt, - Status: runAttempt.Status.String(), - Done: runAttempt.Status.IsDone(), - Link: getRunViewLink(run, runAttempt), - Current: runAttempt.ID == attempt.ID, - Latest: runAttempt.ID == run.LatestAttemptID, - TriggeredAt: runAttempt.Created.AsTime().Unix(), - TriggerUserName: runAttempt.TriggerUser.GetDisplayName(), - TriggerUserLink: runAttempt.TriggerUser.HomeLink(), + Attempt: runAttempt.Attempt, + Status: runAttempt.Status.String(), + Done: runAttempt.Status.IsDone(), + Link: getRunViewLink(run, runAttempt), + Current: runAttempt.ID == attempt.ID, + Latest: runAttempt.ID == run.LatestAttemptID, + TriggeredAt: runAttempt.Created.AsTime().Unix(), + TriggerUserName: runAttempt.TriggerUser.GetDisplayName(), + TriggerUserLink: runAttempt.TriggerUser.HomeLink(), + TriggerUserAvatar: runAttempt.TriggerUser.AvatarLinkWithSize(ctx, 16), }) } pusher := ViewUser{ DisplayName: run.TriggerUser.GetDisplayName(), Link: run.TriggerUser.HomeLink(), - } - branch := ViewBranch{ - Name: run.PrettyRef(), - Link: run.RefLink(), - } - refName := git.RefName(run.Ref) - if refName.IsBranch() { - b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName()) - if err != nil && !git_model.IsErrBranchNotExist(err) { - log.Error("GetBranch: %v", err) - } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { - branch.IsDeleted = true - } + AvatarLink: run.TriggerUser.AvatarLinkWithSize(ctx, 16), } + refInfo := getViewSummaryRefInfo(ctx, run) resp.State.Run.Commit = ViewCommit{ ShortSha: base.ShortSha(run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Pusher: pusher, - Branch: branch, + Branch: refInfo.Branch, } + resp.State.Run.PullRequest = refInfo.PullRequest resp.State.Run.TriggerEvent = run.TriggerEvent // Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts all share run_attempt_id=0, diff --git a/routers/web/repo/actions/view_test.go b/routers/web/repo/actions/view_test.go index 737ac5e1b3a..020930eb38c 100644 --- a/routers/web/repo/actions/view_test.go +++ b/routers/web/repo/actions/view_test.go @@ -7,6 +7,8 @@ import ( "testing" actions_model "gitea.dev/models/actions" + repo_model "gitea.dev/models/repo" + api "gitea.dev/modules/structs" "gitea.dev/modules/timeutil" "gitea.dev/modules/translation" @@ -14,6 +16,66 @@ import ( "github.com/stretchr/testify/require" ) +func TestViewPullRequestFromRun(t *testing.T) { + repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"} + + t.Run("pull ref", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/123/head"} + assert.Equal(t, &ViewPullRequest{Index: "#123", Link: "/owner/repo/pulls/123"}, viewPullRequestFromRun(t.Context(), run, nil)) + }) + + t.Run("pull request event payload", func(t *testing.T) { + // a non-pull ref forces the payload branch instead of the ref branch + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/heads/feature"} + payload := &api.PullRequestPayload{Index: 42} + assert.Equal(t, &ViewPullRequest{Index: "#42", Link: "/owner/repo/pulls/42"}, viewPullRequestFromRun(t.Context(), run, payload)) + }) + + t.Run("nil repo", func(t *testing.T) { + run := &actions_model.ActionRun{Ref: "refs/pull/1/head"} + assert.Nil(t, viewPullRequestFromRun(t.Context(), run, nil)) + }) +} + +func TestViewSummaryBranchFromRun(t *testing.T) { + repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"} + + t.Run("pull request event same repo", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"} + payload := &api.PullRequestPayload{ + PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{ + Name: "feature", + Ref: "refs/heads/feature", + RepoID: 1, + Repository: &api.Repository{Link: "/owner/repo"}, + }}, + } + assert.Equal(t, ViewBranch{Name: "feature", Link: "/owner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload)) + }) + + t.Run("pull request event from fork prefixes owner", func(t *testing.T) { + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"} + payload := &api.PullRequestPayload{ + PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{ + Name: "feature", + Ref: "refs/heads/feature", + RepoID: 2, + Repository: &api.Repository{ + Link: "/forkowner/repo", + Owner: &api.User{UserName: "forkowner"}, + }, + }}, + } + assert.Equal(t, ViewBranch{Name: "forkowner:feature", Link: "/forkowner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload)) + }) + + t.Run("push to tag does not query branch", func(t *testing.T) { + // a tag ref is not a branch, so no GetBranch DB lookup happens + run := &actions_model.ActionRun{Repo: repo, Ref: "refs/tags/v1.0.0"} + assert.Equal(t, ViewBranch{Name: "v1.0.0", Link: "/owner/repo/src/tag/v1.0.0"}, viewSummaryBranchFromRun(t.Context(), run, nil)) + }) +} + func TestConvertToViewModel(t *testing.T) { task := &actions_model.ActionTask{ Status: actions_model.StatusSuccess, diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 2ed47ad9df8..8b8a6dfeff0 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -18,6 +18,10 @@ data-locale-expand-caller-jobs="{{ctx.Locale.Tr "actions.runs.expand_caller_jobs"}}" data-locale-collapse-caller-jobs="{{ctx.Locale.Tr "actions.runs.collapse_caller_jobs"}}" data-locale-triggered-via="{{ctx.Locale.Tr "actions.runs.triggered_via"}}" + data-locale-rerun-triggered="{{ctx.Locale.Tr "actions.runs.rerun_triggered"}}" + data-locale-back-to-pull-request="{{ctx.Locale.Tr "actions.runs.back_to_pull_request"}}" + data-locale-back-to-workflow="{{ctx.Locale.Tr "actions.runs.back_to_workflow"}}" + data-locale-status-label="{{ctx.Locale.Tr "actions.runs.status"}}" data-locale-total-duration="{{ctx.Locale.Tr "actions.runs.total_duration"}}" data-locale-run-details="{{ctx.Locale.Tr "actions.runs.run_details"}}" data-locale-workflow-file="{{ctx.Locale.Tr "actions.runs.workflow_file"}}" diff --git a/web_src/js/components/ActionRunSummaryView.vue b/web_src/js/components/ActionRunSummaryView.vue index 17b7e2802e1..e3813e9e174 100644 --- a/web_src/js/components/ActionRunSummaryView.vue +++ b/web_src/js/components/ActionRunSummaryView.vue @@ -1,5 +1,4 @@ `+ @@ -127,10 +127,7 @@ func TestExternalMarkupRenderer(t *testing.T) { req = NewRequest(t, "GET", "/user2/repo1/render/branch/master/bin.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) assert.Equal(t, binaryContent, respSub.Body.String()) // raw content should keep the raw bytes (including invalid UTF-8 bytes), and no "external-render-iframe" helpers - - // no sandbox (disabled by RENDER_CONTENT_SANDBOX) - assert.Empty(t, iframe.AttrOr("sandbox", "")) - assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + assert.Empty(t, respSub.Header().Get("Content-Security-Policy"), "sandbox is disabled by RENDER_CONTENT_SANDBOX") }) t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { @@ -142,7 +139,7 @@ func TestExternalMarkupRenderer(t *testing.T) { ``, respSub.Body.String(), ) - assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) + assert.Empty(t, respSub.Header().Get("Content-Security-Policy")) }) }) }) diff --git a/types.d.ts b/types.d.ts index bdf35428bc6..d6325f5cbd2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -50,7 +50,7 @@ declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { declare module 'asciinema-player' { interface AsciinemaPlayer { - create(src: string, element: HTMLElement, options?: Record): void; + create(src: string | {data: string}, element: HTMLElement, options?: Record): void; } const exports: AsciinemaPlayer; export = exports; diff --git a/web_src/css/index.css b/web_src/css/index.css index 6d9280c67f8..2d3e118825d 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -52,7 +52,6 @@ @import "./markup/content.css"; @import "./markup/codeblock.css"; @import "./markup/codepreview.css"; -@import "./markup/asciicast.css"; @import "./font_i18n.css"; @import "./base.css"; diff --git a/web_src/css/markup/asciicast.css b/web_src/css/markup/asciicast.css deleted file mode 100644 index a45daaa8e8b..00000000000 --- a/web_src/css/markup/asciicast.css +++ /dev/null @@ -1,10 +0,0 @@ -.asciinema-player-container { - width: 100%; - height: auto; -} - -/* Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js :
    -Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 */ -.ap-term { - overflow: hidden !important; -} diff --git a/web_src/css/repo.css b/web_src/css/repo.css index ad9a43b098c..16337a9eadb 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -210,10 +210,6 @@ td .commit-summary { overflow: auto; } -.non-diff-file-content .asciicast { - padding: 0 !important; -} - .repo-editor-header { /* it should match ".repo-button-row" so the tree toggle button stays aligned */ margin: 8px 0; diff --git a/web_src/js/external-render-frontend.ts b/web_src/js/external-render-frontend.ts index 9d969bcf900..e7e3f4f1be9 100644 --- a/web_src/js/external-render-frontend.ts +++ b/web_src/js/external-render-frontend.ts @@ -8,6 +8,7 @@ type LazyLoadFunc = () => Promise<{frontendRender: FrontendRenderFunc}>; const frontendPlugins: Record = { 'viewer-3d': () => import('./render/plugins/frontend-viewer-3d.ts'), 'openapi-swagger': () => import('./render/plugins/frontend-openapi-swagger.ts'), + 'asciicast': () => import('./render/plugins/frontend-asciicast.ts'), }; class Options implements FrontendRenderOptions { @@ -44,23 +45,28 @@ async function initFrontendExternalRender() { const viewerContainer = document.querySelector('#frontend-render-viewer')!; const renderNames = viewerContainer.getAttribute('data-frontend-renders')!.split(' '); const fileTreePath = viewerContainer.getAttribute('data-file-tree-path')!; + viewerContainer.setAttribute('data-window-origin', window.origin); // mainly for testing purpose const fileDataElem = document.querySelector('#frontend-render-data')!; fileDataElem.remove(); const fileDataContent = fileDataElem.value; const fileDataEncoding = fileDataElem.getAttribute('data-content-encoding')!; const opts = new Options(viewerContainer, fileTreePath, fileDataEncoding, fileDataContent); - - let found = false; + let renderName = '', rendered = false; for (const name of renderNames) { if (!(name in frontendPlugins)) continue; const plugin = await frontendPlugins[name](); - found = true; - if (await plugin.frontendRender(opts)) break; + renderName = name; + rendered = await plugin.frontendRender(opts); + if (rendered) break; } - if (!found) { + if (!renderName) { viewerContainer.textContent = 'No frontend render plugin found for this file, but backend declares that there must be one, there must be a bug'; + } else if (!rendered) { + viewerContainer.textContent = `Failed to render by ${renderName}`; + } else { + viewerContainer.setAttribute('data-frontend-render-name', renderName); // succeeded render, mainly for testing purpose } } diff --git a/web_src/js/external-render-helper.test.ts b/web_src/js/external-render-helper.test.ts index 452d7f8f2d5..3bb524a0115 100644 --- a/web_src/js/external-render-helper.test.ts +++ b/web_src/js/external-render-helper.test.ts @@ -1,7 +1,7 @@ import './external-render-helper.ts'; test('isValidCssColor', async () => { - const isValidCssColor = window.testModules.externalRenderHelper!.isValidCssColor; + const isValidCssColor = window.giteaExternalRenderHelper!.isValidCssColor; expect(isValidCssColor(null)).toBe(false); expect(isValidCssColor('')).toBe(false); diff --git a/web_src/js/external-render-helper.ts b/web_src/js/external-render-helper.ts index f92aeb9c6c9..8a5122fe99c 100644 --- a/web_src/js/external-render-helper.ts +++ b/web_src/js/external-render-helper.ts @@ -50,12 +50,12 @@ body { background: ${backgroundColor}; } } const iframeId = queryParams.get('gitea-iframe-id'); -if (iframeId) { - // iframe is in different origin, so we need to use postMessage to communicate - const postIframeMsg = (cmd: string, data: Record = {}) => { - window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*'); - }; +// iframe is in different origin, so we need to use postMessage to communicate +const postIframeMsg = (cmd: string, data: Record = {}) => { + window.parent.postMessage({giteaIframeCmd: cmd, giteaIframeId: iframeId, ...data}, '*'); +}; +if (iframeId) { const updateIframeHeight = () => { if (!document.body) return; // the body might not be available when this function is called // Use scrollHeight to get the full content height, even when CSS sets html/body to height:100% @@ -90,6 +90,4 @@ if (iframeId) { }); } -if (window.testModules) { - window.testModules.externalRenderHelper = {isValidCssColor}; -} +window.giteaExternalRenderHelper = {isValidCssColor, queryParams, postIframeMsg}; diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 5398d407d1d..6579bb22037 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -68,13 +68,13 @@ interface Window { turnstile: any, hcaptcha: any, - // Make IIFE private functions can be tested in unit tests, without exposing the IIFE module to global scope. + // Make IIFE private functions can be managed by us in our scope, without exposing the IIFE module to global scope. // Otherwise, when using "export" in IIFE code, the compiled JS will inject global "var externalRenderHelper = ..." // which is not expected and may cause conflicts with other modules. - testModules: { - externalRenderHelper?: { - isValidCssColor(s: string | null): boolean, - } + giteaExternalRenderHelper?: { + isValidCssColor(s: string | null): boolean, + queryParams: URLSearchParams, + postIframeMsg(cmd: string, data: Record = {}), } // do not add more properties here unless it is a must diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts deleted file mode 100644 index 90515e1363c..00000000000 --- a/web_src/js/markup/asciicast.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {queryElems} from '../utils/dom.ts'; - -export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise { - queryElems(elMarkup, '.asciinema-player-container', async (el) => { - const [player] = await Promise.all([ - import('asciinema-player'), - import('asciinema-player/dist/bundle/asciinema-player.css'), - ]); - - player.create(el.getAttribute('data-asciinema-player-src')!, el, { - // poster (a preview frame) to display until the playback is started. - // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. - poster: 'npt:1:0:0', - }); - }); -} diff --git a/web_src/js/markup/content.ts b/web_src/js/markup/content.ts index 44c005a7c82..09dee98754d 100644 --- a/web_src/js/markup/content.ts +++ b/web_src/js/markup/content.ts @@ -1,7 +1,6 @@ import {initMarkupCodeMermaid} from './mermaid.ts'; import {initMarkupCodeMath} from './math.ts'; import {initMarkupCodeCopy} from './codecopy.ts'; -import {initMarkupRenderAsciicast} from './asciicast.ts'; import {initMarkupTasklist} from './tasklist.ts'; import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; import {initExternalRenderIframe} from './render-iframe.ts'; @@ -24,6 +23,5 @@ export function initMarkupContent(): void { initMarkupTasklist(el); initMarkupCodeMermaid(el); initMarkupCodeMath(el); - initMarkupRenderAsciicast(el); }); } diff --git a/web_src/js/markup/render-iframe.ts b/web_src/js/markup/render-iframe.ts index f05523943a8..6a191ac5c66 100644 --- a/web_src/js/markup/render-iframe.ts +++ b/web_src/js/markup/render-iframe.ts @@ -1,7 +1,6 @@ import {generateElemId} from '../utils/dom.ts'; import {errorMessage} from '../modules/errors.ts'; import {isDarkTheme} from '../utils.ts'; -import {GET} from '../modules/fetch.ts'; function safeRenderIframeLink(link: any): string | null { try { @@ -65,9 +64,31 @@ export async function initExternalRenderIframe(iframe: HTMLIFrameElement) { u.searchParams.set('gitea-iframe-id', iframe.id); u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe)); - // It must use "srcdoc" here, because our backend always sends CSP sandbox directive for the rendered content - // (to protect from XSS risks), so we can't use "src" to load the content directly, otherwise there will be console errors like: - // Unsafe attempt to load URL http://localhost:3000/test from frame with URL http://localhost:3000/test - const resp = await GET(u.href); - iframe.srcdoc = await resp.text(); + // There are 3 kinds of external render modes: + // * external frontend render: + // * parent page creates iframe, iframe navigates to render page + // * render generates frame page with external-render-helper (injected), external-render-frontend and file content (hidden textarea) + // * frame page executes external-render-frontend JS code to finds a frontend plugin to render + // * external backend render (HTML) + // * parent page creates iframe, iframe navigates to render page + // * render executes command to generate rendered HTML content with external-render-helper (injected) + // * frame page displays the rendered content + // * external backend render (non-HTML, e.g.: PDF, image) + // * parent page creates iframe, iframe navigates to render page + // * render executes command to generate rendered content + // * response header is automatically detected from rendered content + + // It must use "src" here, because the frame content should not inherit parent's CSP. + // Otherwise, "srcdoc" makes the frame content inherit the parent's CSP, + // then some renders like "asciicast (asciinema)" which require "unsafe-eval" won't work. + // + // When using "src", Chrome can report false-alarm error like: + // * Unsafe attempt to load URL http://localhost/owner/repo/render/branch/main/file from frame with URL http://localhost/owner/repo/render/branch/main/file. Domains, protocols and ports must match. + // (only for the first time that the developer opens the browser console) + // Such error log can also appear even if you access the link "http://.../owner/repo/render/branch/main/file" directly. + // Everything just works, it is just a false-alarm caused by Chrome's Developer Tools, so such error log can be ignored. + // + // Another reason for why "src" is a must: if the render outputs non-HTML contents like PDF or image, + // Only "src" can correctly load and display the rendered content, "srcdoc" won't work. + iframe.src = u.href; } diff --git a/web_src/js/render/plugins/frontend-asciicast.ts b/web_src/js/render/plugins/frontend-asciicast.ts new file mode 100644 index 00000000000..f8bdefb316a --- /dev/null +++ b/web_src/js/render/plugins/frontend-asciicast.ts @@ -0,0 +1,23 @@ +import type {FrontendRenderFunc} from '../plugin.ts'; + +export const frontendRender: FrontendRenderFunc = async (opts): Promise => { + try { + const [player] = await Promise.all([ + import('asciinema-player'), + import('asciinema-player/dist/bundle/asciinema-player.css'), + ]); + player.create({data: opts.contentString()}, opts.container, { + // poster (a preview frame) to display until the playback is started. + // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. + poster: 'npt:1:0:0', + }); + // Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js :
    + // Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 + opts.container.querySelector('.ap-term')!.style.overflow = 'hidden'; + opts.container.querySelector('.ap-player')!.style.borderRadius = '0'; + return true; + } catch (error) { + console.error(error); + return false; + } +}; diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index a6ec019ff8a..1c0c27a667e 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -14,5 +14,3 @@ window.config = { i18n: {}, frontendInited: false, }; - -window.testModules = {}; From f5a97b751883068621334f35fc9b9ae5de953b85 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 12 Jun 2026 13:35:59 +0800 Subject: [PATCH 68/88] fix: git cmd (#38084) --- modules/git/gitcmd/command.go | 11 +++++++++++ modules/gitrepo/gitrepo.go | 2 +- routers/private/hook_post_receive.go | 2 +- services/repository/files/temp_repo.go | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/git/gitcmd/command.go b/modules/git/gitcmd/command.go index b6f56af07a9..28ed20ece0e 100644 --- a/modules/git/gitcmd/command.go +++ b/modules/git/gitcmd/command.go @@ -445,6 +445,17 @@ func (c *Command) Start(ctx context.Context) (retErr error) { c.cmd.Stdout = c.cmdStdout c.cmd.Stdin = c.cmdStdin c.cmd.Stderr = c.cmdStderr + c.cmd.Cancel = func() error { + // Golang's default cmd.Cancel only calls Process.Kill(), but here we need to close the parent pipes together: + // * for some commands like "git --batch-xxx", Windows git might have 2 processes (a wrapper and a real git process) + // * on Windows, if parent process is killed (context canceled), the children process won't be killed, and the pipe handles are still open. + // * if we don't close the parent pipes here, the children process won't exit. + // + // There is no such problem on POSIX, while it won't make things worse by closing the parent pipes also on POSIX. + err := c.cmd.Process.Kill() + c.closePipeFiles(c.parentPipeFiles) + return err + } return c.cmd.Start() } diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 1af6f4406c6..17eabb2aad2 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -40,7 +40,7 @@ type contextKey struct { } // RepositoryFromContextOrOpen attempts to get the repository from the context or just opens it -// The caller must call "defer gitRepo.Close()" +// The caller must call Closer.Close() func RepositoryFromContextOrOpen(ctx context.Context, repo Repository) (*git.Repository, io.Closer, error) { reqCtx := reqctx.FromContext(ctx) if reqCtx != nil { diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 3cb9eac809b..7e6e06a5f79 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -47,7 +47,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { repo *repo_model.Repository gitRepo *git.Repository ) - defer gitRepo.Close() // it's safe to call Close on a nil pointer + defer func() { _ = gitRepo.Close() }() // it's safe to call Close on a nil pointer, but it needs to use the latest value updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs)) wasEmpty := false diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 553f4232e2a..bffd3525836 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -47,7 +47,8 @@ func NewTemporaryUploadRepository(repo *repo_model.Repository) (*TemporaryUpload // Close the repository cleaning up all files func (t *TemporaryUploadRepository) Close() { - defer t.gitRepo.Close() + // must stop the repo access before removal, otherwise Windows can't remove the directory occupied by other processes + t.gitRepo.Close() if t.cleanup != nil { t.cleanup() } From 15ae1bfc8ca0af8b6c641f7efa21724216dc08dd Mon Sep 17 00:00:00 2001 From: bircni Date: Fri, 12 Jun 2026 15:26:51 +0200 Subject: [PATCH 69/88] fix: keep literal "false" value displayed in workflow_dispatch choice dropdowns (#38080) --- web_src/fomantic/build/components/dropdown.js | 8 ++++---- web_src/js/modules/fomantic/dropdown.test.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 0faed1858c4..f971fecda4c 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -1953,8 +1953,8 @@ $.fn.dropdown = function(parameters) { $choice.find(selector.menu).remove(); $choice.find(selector.menuIcon).remove(); } - return ($choice.data(metadata.text) !== undefined) - ? $choice.data(metadata.text) + return ($choice.attr('data-' + metadata.text) !== undefined) // GITEA-PATCH: use "attr" but not "data", don't decode JSON like "false" + ? $choice.attr('data-' + metadata.text) : (preserveHTML) ? $choice.html().trim() : $choice.text().trim() @@ -2007,8 +2007,8 @@ $.fn.dropdown = function(parameters) { value = ( $option.attr('value') !== undefined ) ? $option.attr('value') : name, - text = ( $option.data(metadata.text) !== undefined ) - ? $option.data(metadata.text) + text = ( $option.attr('data-' + metadata.text) !== undefined ) // GITEA-PATCH: use "attr" but not "data", don't decode JSON like "false" + ? $option.attr('data-' + metadata.text) : name, group = $option.parent('optgroup') ; diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index 542cb854b84..4ea51b3d2ce 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -1,6 +1,23 @@ +import '../../../fomantic/build/fomantic.js'; import {createElementFromHTML} from '../../utils/dom.ts'; import {hideScopedEmptyDividers} from './dropdown.ts'; +test('dropdown-item-literal-text', () => { + // a "choice" workflow_dispatch input can offer the string "false" as an option. + // jQuery `.data()` would coerce `data-text="false"` to the boolean `false`, which then renders as empty text. + const $dropdown = $(``).dropdown(); + for (const value of ['1', '0', 'true', 'false']) { + $dropdown.dropdown('set selected', value); + expect($dropdown.dropdown('get text')).toEqual(value); + expect($dropdown.dropdown('get value')).toEqual(value); + } +}); + test('hideScopedEmptyDividers-simple', () => { const container = createElementFromHTML(`
    From ae49f6569254ff8ad456e987e771f84e31663c95 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 13 Jun 2026 02:27:38 +0800 Subject: [PATCH 70/88] fix: parse HEAD ref (#38088) fix #38086 --- modules/git/ref.go | 2 +- modules/git/repo_ref.go | 6 +++++- tests/integration/compare_test.go | 10 +++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/git/ref.go b/modules/git/ref.go index 0c9aeae8c0a..7d0bbcbae9b 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -168,7 +168,7 @@ func (ref RefName) ShortName() string { if ref.IsFor() { return ref.ForBranchName() } - return string(ref) // usually it is a commit ID + return string(ref) // usually it is a commit ID, or "HEAD" } // RefGroup returns the group type of the reference diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 5adb1e57353..11235c71b15 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -8,6 +8,7 @@ import ( "strings" "gitea.dev/modules/git/gitcmd" + "gitea.dev/modules/setting" "gitea.dev/modules/util" ) @@ -86,8 +87,11 @@ func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName { commit, err := repo.GetCommit(shortName) if err == nil { commitIDString := commit.ID.String() - if strings.HasPrefix(commitIDString, shortName) { + // make sure the "shortName" is either partial commit ID, or it is HEAD + if strings.HasPrefix(commitIDString, shortName) || shortName == RefNameHead { return RefName(commitIDString) + } else { + setting.PanicInDevOrTesting("abuse of UnstableGuessRefByShortName, queried %s, got %s", shortName, commitIDString) } } return "" diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 6c00b3fa0eb..ac2e014d92f 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -33,9 +33,17 @@ func TestCompareTag(t *testing.T) { // A dropdown for both base and head. assert.Lenf(t, selection.Nodes, 2, "The template has changed") + req = NewRequest(t, "GET", "/user2/repo1/compare/v1.1...HEAD") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) + + req = NewRequest(t, "GET", "/user2/repo1/compare/v1.1...NotExisting").SetHeader("Accept", "text/html") + resp = session.MakeRequest(t, req, http.StatusNotFound) + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) + req = NewRequest(t, "GET", "/user2/repo1/compare/invalid").SetHeader("Accept", "text/html") resp = session.MakeRequest(t, req, http.StatusNotFound) - assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "expect 404 page not 500") + assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) } // Compare with inferred default branch (master) From 275dee5bda00ce197bdcb5c65d75a5714593c0be Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 13 Jun 2026 01:21:50 +0000 Subject: [PATCH 71/88] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.json | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index ba52e2e79d7..03c6e417cee 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -1321,6 +1321,7 @@ "repo.editor.fork_branch_exists": "分支「%s」已存在于您的派生仓库中,请选择一个新的分支名称。", "repo.commits.desc": "浏览代码修改历史", "repo.commits.commits": "次代码提交", + "repo.commits.history_enable_follow_renames": "包含重命名", "repo.commits.no_commits": "没有共同的提交。「%s」和「%s」的历史完全不同。", "repo.commits.nothing_to_compare": "没有差异可显示。", "repo.commits.search.tooltip": "您可以在关键词前加上前缀,如「author:」、「committer:」、「after:」或「before:」,例如「retrin author:Alice before:2019-01-13」。", @@ -2204,10 +2205,10 @@ "repo.settings.trust_model.collaborator.desc": "此仓库中协作者的有效签名将被标记为「可信」(无论它们是否是提交者),签名只符合提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。", "repo.settings.trust_model.committer": "提交者", "repo.settings.trust_model.committer.long": "提交者: 信任与提交者相符的签名(这符合 GitHub 的行为并将强制 Gitea 签名的提交以 Gitea 为提交者)。", - "repo.settings.trust_model.committer.desc": "有效签名只有和提交者相匹配才会被标记为「受信任」,否则它们将被标记为「不匹配」。这强制 Gitea 成为签名提交的提交者,而实际提交者被加上 Co-authored-by: 和 Co-committed-by: 的标记。 默认的 Gitea 密钥必须匹配数据库中的一名用户。", + "repo.settings.trust_model.committer.desc": "有效签名只有和提交者相匹配才会被标记为「受信任」,否则它们将被标记为「不匹配」。这意味着在已签名的提交中,Gitea 必须作为提交者,而实际的提交者则在提交信息中通过 `Co-authored-by:` 字段进行标注。 默认的 Gitea 密钥必须与数据库中的用户相匹配。", "repo.settings.trust_model.collaboratorcommitter": "协作者+提交者", "repo.settings.trust_model.collaboratorcommitter.long": "协作者+提交者:信任协作者同时是提交者的签名", - "repo.settings.trust_model.collaboratorcommitter.desc": "此仓库中协作者的有效签名在他同时是提交者时将被标记为「可信」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这会强制 Gitea 成为签名者和提交者,实际的提交者将被标记于提交消息结尾处的「Co-Authored-By:」和「Co-Committed-By:」。默认的 Gitea 签名密钥必须匹配数据库中的一个用户密钥。", + "repo.settings.trust_model.collaboratorcommitter.desc": "此仓库中协作者的有效签名在他同时是提交者时将被标记为「受信任」,签名只匹配了提交者时将标记为「不可信」,都不匹配时标记为「不匹配」。这将强制使 Gitea 显示为已签名提交的提交者,而实际提交者则在提交信息中以 `Co-Authored-By:` 尾注的形式标出。默认的 Gitea 密钥必须与数据库中的用户相匹配。", "repo.settings.wiki_delete": "删除百科数据", "repo.settings.wiki_delete_desc": "删除仓库百科数据是永久性的,无法撤消。", "repo.settings.wiki_delete_notices_1": "- 这将永久删除和禁用 %s 的百科。", @@ -2598,6 +2599,9 @@ "repo.diff.review.reject": "请求变更", "repo.diff.review.self_approve": "合并请求作者不能批准自己的合并请求", "repo.diff.committed_by": "提交者", + "repo.diff.coauthored_by": "共同撰写人", + "repo.commits.avatar_stack_and": "和", + "repo.commits.avatar_stack_people": "%d 人", "repo.diff.protected": "受保护的", "repo.diff.image.side_by_side": "双排", "repo.diff.image.swipe": "滑动", @@ -2725,6 +2729,7 @@ "graphs.code_frequency.what": "代码频率", "graphs.contributors.what": "贡献", "graphs.recent_commits.what": "最近的提交", + "graphs.chart_zoom_hint": "拖动:缩放,Shift+拖动:平移,双击:重置缩放", "org.org_name_holder": "组织名称", "org.org_full_name_holder": "组织全名", "org.org_name_helper": "组织名字应该简单明了。", @@ -3772,6 +3777,7 @@ "actions.runs.no_matching_online_runner_helper": "没有匹配 %s 标签的在线运行器", "actions.runs.no_job_without_needs": "工作流必须包含至少一个没有依赖关系的作业。", "actions.runs.no_job": "工作流必须包含至少一个作业", + "actions.runs.invalid_reusable_workflow_uses": "无效的可复用工作流「uses」:%s", "actions.runs.actor": "操作者", "actions.runs.status": "状态", "actions.runs.actors_no_select": "所有操作者", @@ -3792,11 +3798,27 @@ "actions.runs.view_workflow_file": "查看工作流文件", "actions.runs.summary": "摘要", "actions.runs.all_jobs": "所有任务", + "actions.runs.job_summaries": "任务摘要", + "actions.runs.expand_caller_jobs": "显示此可复用工作流调用者的任务", + "actions.runs.collapse_caller_jobs": "隐藏此可复用工作流调用者的任务", "actions.runs.attempt": "尝试", "actions.runs.latest": "最新", "actions.runs.latest_attempt": "最新尝试", "actions.runs.triggered_via": "通过 %s 触发", - "actions.runs.total_duration": "总耗时:", + "actions.runs.rerun_triggered": "重新运行已触发", + "actions.runs.back_to_pull_request": "返回合并请求", + "actions.runs.back_to_workflow": "返回工作流", + "actions.runs.total_duration": "总耗时", + "actions.runs.workflow_dependencies": "工作流依赖项", + "actions.runs.graph_jobs_count_1": "%d 个任务", + "actions.runs.graph_jobs_count_n": "%d 个任务", + "actions.runs.graph_dependencies_count_1": "%d 个依赖项", + "actions.runs.graph_dependencies_count_n": "%d 个依赖项", + "actions.runs.graph_success_rate": "%s 成功", + "actions.runs.graph_zoom_in": "放大(在图上 Ctrl/Cmd + 滚动)", + "actions.runs.graph_zoom_max": "已为 100% 缩放", + "actions.runs.graph_zoom_out": "缩小(在图上 Ctrl/Cmd + 滚动)", + "actions.runs.graph_reset_view": "重置视图", "actions.workflow.disable": "禁用工作流", "actions.workflow.disable_success": "工作流「%s」已成功禁用。", "actions.workflow.enable": "启用工作流", From 9608cc212d77e5019f35091a6973bac4290f967d Mon Sep 17 00:00:00 2001 From: bircni Date: Sat, 13 Jun 2026 06:02:02 +0200 Subject: [PATCH 72/88] fix: allow git clone of private repos with anonymous code access (#38074) Fixes #38062. --------- Co-authored-by: wxiaoguang --- routers/web/repo/githttp.go | 43 ++++++++++++------------ tests/integration/git_smart_http_test.go | 34 +++++++++++++++++++ tests/sqlite.ini.tmpl | 1 - 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index c1c2ed5e86b..4ae2955f6d4 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -58,8 +58,6 @@ func CorsHandler() func(next http.Handler) http.Handler { // httpBase does the common work for git http services, // including early response, authentication, repository lookup and permission check. func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { - reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") - if ctx.FormString("go-get") == "1" { context.EarlyResponseForGoGetMeta(ctx) return nil @@ -93,11 +91,11 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { isWiki := false unitType := unit.TypeCode - - if strings.HasSuffix(reponame, ".wiki") { + repoName := strings.TrimSuffix(ctx.PathParam("reponame"), ".git") + if strings.HasSuffix(repoName, ".wiki") { isWiki = true unitType = unit.TypeWiki - reponame = reponame[:len(reponame)-5] + repoName = repoName[:len(repoName)-5] } owner := ctx.ContextUser @@ -107,14 +105,14 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { } repoExist := true - repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, reponame) + repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName) if err != nil { if !repo_model.IsErrRepoNotExist(err) { ctx.ServerError("GetRepositoryByName", err) return nil } - if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil { + if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName); err == nil { context.RedirectToRepo(ctx.Base, redirectRepoID) return nil } @@ -127,23 +125,26 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { return nil } - // Only public pull don't need auth. - isPublicPull := repoExist && !repo.IsPrivate && isPull - askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict - - // don't allow anonymous pulls if organization is not public - if isPublicPull { - if err := repo.LoadOwner(ctx); err != nil { - ctx.ServerError("LoadOwner", err) - return nil + // Only public pulls don't need auth: repo must exist, not require-sign-in + canAnonymousPull := false + if isPull && repoExist && !setting.Service.RequireSignInViewStrict { + // allow anonymous pulls if owner is public and repo is public (not private) + if owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate { + canAnonymousPull = true + } + // then check "public anonymous access" permission + if !canAnonymousPull && ctx.Doer == nil { + anonPerm, err := access_model.GetDoerRepoPermission(ctx, repo, nil) + if err != nil { + ctx.ServerError("GetDoerRepoPermission", err) + return nil + } + canAnonymousPull = anonPerm.CanAccess(accessMode, unitType) } - - askAuth = askAuth || (repo.Owner.Visibility != structs.VisibleTypePublic) } // check access - if askAuth { - // rely on the results of Contexter + if !canAnonymousPull { // not public pull, then either the pull needs auth, or the push needs "write" permission, so ask auth if !ctx.IsSigned { // TODO: support digit auth - which would be Authorization header with digit if setting.OAuth2.Enabled { @@ -229,7 +230,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler { return nil } - repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, reponame) + repo, err = repo_service.PushCreateRepo(ctx, ctx.Doer, owner, repoName) if err != nil { log.Error("pushCreateRepo: %v", err) ctx.Status(http.StatusNotFound) diff --git a/tests/integration/git_smart_http_test.go b/tests/integration/git_smart_http_test.go index dfbda5c701d..df8bc1caeb1 100644 --- a/tests/integration/git_smart_http_test.go +++ b/tests/integration/git_smart_http_test.go @@ -10,7 +10,9 @@ import ( "testing" auth_model "gitea.dev/models/auth" + "gitea.dev/models/perm" repo_model "gitea.dev/models/repo" + "gitea.dev/models/unit" "gitea.dev/models/unittest" "gitea.dev/modules/setting" "gitea.dev/modules/test" @@ -26,6 +28,8 @@ func TestGitSmartHTTP(t *testing.T) { testGitSmartHTTPTokenScopes(t) testRenamedRepoRedirect(t) testGitArchiveRemote(t, u) + t.Run("AnonymousAccess-Repo", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, false) }) + t.Run("AnonymousAccess-Wiki", func(t *testing.T) { testGitSmartHTTPPrivateRepoAnonymousAccess(t, true) }) }) } @@ -144,3 +148,33 @@ func testGitArchiveRemote(t *testing.T, u *url.URL) { t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test")) t.Run("list compression options", doGitRemoteArchive(u.String(), "--list")) } + +// testGitSmartHTTPPrivateRepoAnonymousAccess tests that a private repo with +// anonymous code access enabled can be cloned without credentials. +func testGitSmartHTTPPrivateRepoAnonymousAccess(t *testing.T, isWiki bool) { + // repo1 (ID=1) belongs to user2 and is public by default in fixtures + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerName: "user2", Name: "repo1"}) + unitType := util.Iif(isWiki, unit.TypeWiki, unit.TypeCode) + repoLink := "/" + repo.FullName() + util.Iif(isWiki, ".wiki", "") + gitPullPath := repoLink + "/info/refs?service=git-upload-pack" + gitPushPath := repoLink + "/info/refs?service=git-receive-pack" + + // make the repo private + require.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: repo.ID, IsPrivate: true}, "is_private")) + + // without anonymous access: anonymous pull must require auth + MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized) + + // enable anonymous read access on the unit + require.NoError(t, repo_model.UpdateRepoUnitPublicAccess(t.Context(), &repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, AnonymousAccessMode: perm.AccessModeRead})) + + // with anonymous code access: anonymous pull must succeed without credentials + MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusOK) + + // push (receive-pack) must still require auth even with anonymous code access + MakeRequest(t, NewRequest(t, "GET", gitPushPath), http.StatusUnauthorized) + + // RequireSignInViewStrict must override anonymous access + defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)() + MakeRequest(t, NewRequest(t, "GET", gitPullPath), http.StatusUnauthorized) +} diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index a12735e06d9..95a1df283fa 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -5,7 +5,6 @@ RUN_MODE = prod [database] DB_TYPE = sqlite3 PATH = gitea-test.db -SQLITE_JOURNAL_MODE = WAL [indexer] REPO_INDEXER_ENABLED = true From 1b3b4bdd034b93d230548955af27d5bb5e485f9d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 13 Jun 2026 12:43:25 +0800 Subject: [PATCH 73/88] fix: git push hook post receive (#38089) * fix incorrect delayWriter call (there is already a defer call) * split HookPostReceive into small functions * fix incorrect HookPostReceiveResult response for errors * fix incorrect AddRepoToLicenseUpdaterQueue call * make sure repo home and branches page can work without default branch * make sure default branch is always synchronized between database and git repo, and fix FIXME --- cmd/hook.go | 137 +++----- modules/git/repo_commit.go | 14 +- modules/private/hook.go | 4 +- modules/repository/push.go | 9 +- routers/private/hook_post_receive.go | 381 ++++++++++------------ routers/private/hook_post_receive_test.go | 4 +- routers/web/repo/branch.go | 11 +- routers/web/repo/repo.go | 17 - routers/web/web.go | 6 +- services/context/private.go | 9 + services/context/repo.go | 5 +- services/repository/branch.go | 21 +- services/repository/push.go | 67 ++-- templates/repo/branch/list.tmpl | 8 +- tests/integration/branches_test.go | 76 ++--- 15 files changed, 325 insertions(+), 444 deletions(-) diff --git a/cmd/hook.go b/cmd/hook.go index 26b3b56053c..f8e964d0c66 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -151,9 +151,6 @@ func (d *delayWriter) WriteString(s string) (n int, err error) { } func (d *delayWriter) Close() error { - if d == nil { - return nil - } stopped := d.timer.Stop() if stopped || d.buf == nil { return nil @@ -163,16 +160,6 @@ func (d *delayWriter) Close() error { return err } -type nilWriter struct{} - -func (n *nilWriter) Write(p []byte) (int, error) { - return len(p), nil -} - -func (n *nilWriter) WriteString(s string) (int, error) { - return len(s), nil -} - func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) { fields := strings.Split(line, " ") if len(fields) != 3 { @@ -227,8 +214,7 @@ Gitea or set your environment appropriately.`, "") total := 0 lastline := 0 - var out io.Writer - out = &nilWriter{} + out := io.Discard if setting.Git.VerbosePush { if setting.Git.VerbosePushDelay > 0 { dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) @@ -350,12 +336,10 @@ Gitea or set your environment appropriately.`, "") return nil } - var out io.Writer - var dWriter *delayWriter - out = &nilWriter{} + out := io.Discard if setting.Git.VerbosePush { if setting.Git.VerbosePushDelay > 0 { - dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) + dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) defer dWriter.Close() out = dWriter } else { @@ -382,101 +366,62 @@ Gitea or set your environment appropriately.`, "") PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), IsWiki: isWiki, } - oldCommitIDs := make([]string, hookBatchSize) - newCommitIDs := make([]string, hookBatchSize) - refFullNames := make([]git.RefName, hookBatchSize) - count := 0 - total := 0 - wasEmpty := false - masterPushed := false + + oldCommitIDs := make([]string, 0, hookBatchSize) + newCommitIDs := make([]string, 0, hookBatchSize) + refFullNames := make([]git.RefName, 0, hookBatchSize) results := make([]private.HookPostReceiveBranchResult, 0) + defer func() { + hookPrintResults(results) + }() + + processBatch := func() error { + if len(refFullNames) == 0 { + return nil + } + _, _ = fmt.Fprintf(out, " Processing %d references\n", len(refFullNames)) + hookOptions.OldCommitIDs = oldCommitIDs + hookOptions.NewCommitIDs = newCommitIDs + hookOptions.RefFullNames = refFullNames + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) + if extra.HasError() { + return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) + } + results = append(results, resp.Results...) + oldCommitIDs = oldCommitIDs[:0] + newCommitIDs = newCommitIDs[:0] + refFullNames = refFullNames[:0] + return nil + } + scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { - // TODO: support news feeds for wiki + // wiki doesn't need "post-receive" at the moment if isWiki { continue } - var ok bool - oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text()) + oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text()) if !ok { continue } + _, _ = fmt.Fprintf(out, ".") - fmt.Fprintf(out, ".") - commitID, _ := git.NewIDFromString(newCommitIDs[count]) - if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total { - masterPushed = true - } - count++ - total++ - - if count >= hookBatchSize { - fmt.Fprintf(out, " Processing %d references\n", count) - hookOptions.OldCommitIDs = oldCommitIDs - hookOptions.NewCommitIDs = newCommitIDs - hookOptions.RefFullNames = refFullNames - resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) - if extra.HasError() { - _ = dWriter.Close() - hookPrintResults(results) - return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) + oldCommitIDs = append(oldCommitIDs, oldCommitID) + newCommitIDs = append(newCommitIDs, newCommitID) + refFullNames = append(refFullNames, refFullName) + if len(refFullNames) >= hookBatchSize { + // process and start a new batch + if err := processBatch(); err != nil { + return err } - wasEmpty = wasEmpty || resp.RepoWasEmpty - results = append(results, resp.Results...) - count = 0 } } if err := scanner.Err(); err != nil { - _ = dWriter.Close() - hookPrintResults(results) return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) } - - if count == 0 { - if wasEmpty && masterPushed { - // We need to tell the repo to reset the default branch to master - extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") - if extra.HasError() { - return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) - } - } - fmt.Fprintf(out, "Processed %d references in total\n", total) - - _ = dWriter.Close() - hookPrintResults(results) - return nil - } - - hookOptions.OldCommitIDs = oldCommitIDs[:count] - hookOptions.NewCommitIDs = newCommitIDs[:count] - hookOptions.RefFullNames = refFullNames[:count] - - fmt.Fprintf(out, " Processing %d references\n", count) - - resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) - if resp == nil { - _ = dWriter.Close() - hookPrintResults(results) - return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) - } - wasEmpty = wasEmpty || resp.RepoWasEmpty - results = append(results, resp.Results...) - - fmt.Fprintf(out, "Processed %d references in total\n", total) - - if wasEmpty && masterPushed { - // We need to tell the repo to reset the default branch to master - extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") - if extra.HasError() { - return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) - } - } - _ = dWriter.Close() - hookPrintResults(results) - - return nil + return processBatch() } func hookPrintResults(results []private.HookPostReceiveBranchResult) { diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 7e6db9abee5..1a93504d97b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -36,25 +36,17 @@ func (repo *Repository) GetCommit(ref string) (*Commit, error) { // GetBranchCommit returns the last commit of given branch. func (repo *Repository) GetBranchCommit(name string) (*Commit, error) { - commitID, err := repo.GetBranchCommitID(name) - if err != nil { - return nil, err - } - return repo.GetCommit(commitID) + return repo.GetCommit(RefNameFromBranch(name).String()) } // GetTagCommit get the commit of the specific tag via name func (repo *Repository) GetTagCommit(name string) (*Commit, error) { - commitID, err := repo.GetTagCommitID(name) - if err != nil { - return nil, err - } - return repo.GetCommit(commitID) + return repo.GetCommit(RefNameFromTag(name).String()) } func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) { // File name starts with ':' must be escaped. - if relpath[0] == ':' { + if strings.HasPrefix(relpath, ":") { relpath = `\` + relpath } diff --git a/modules/private/hook.go b/modules/private/hook.go index cb6cc2f0bdf..843288e0812 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -48,9 +48,7 @@ type SSHLogOption struct { // HookPostReceiveResult represents an individual result from PostReceive type HookPostReceiveResult struct { - Results []HookPostReceiveBranchResult - RepoWasEmpty bool - Err string + Results []HookPostReceiveBranchResult } // HookPostReceiveBranchResult represents an individual branch result from PostReceive diff --git a/modules/repository/push.go b/modules/repository/push.go index 433bbee40ae..5260597229d 100644 --- a/modules/repository/push.go +++ b/modules/repository/push.go @@ -13,9 +13,12 @@ type PushUpdateOptions struct { PusherName string RepoUserName string RepoName string - RefFullName git.RefName // branch, tag or other name to push - OldCommitID string - NewCommitID string + + // FIXME: this struct's design is not right, the changed commits should be in a separate slice + + RefFullName git.RefName // branch, tag or other name to push + OldCommitID string + NewCommitID string } // IsNewRef return true if it's a first-time push to a branch, tag or etc. diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 7e6e06a5f79..e19bee3e7d8 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -5,6 +5,7 @@ package private import ( "context" + "errors" "fmt" "net/http" @@ -29,29 +30,8 @@ import ( repo_service "gitea.dev/services/repository" ) -// HookPostReceive updates services and users -func HookPostReceive(ctx *gitea_context.PrivateContext) { - opts := web.GetForm(ctx).(*private.HookOptions) - - // We don't rely on RepoAssignment here because: - // a) we don't need the git repo in this function - // OUT OF DATE: we do need the git repo to sync the branch to the db now. - // b) our update function will likely change the repository in the db so we will need to refresh it - // c) we don't always need the repo - - ownerName := ctx.PathParam("owner") - repoName := ctx.PathParam("repo") - - // defer getting the repository at this point - as we should only retrieve it if we're going to call update - var ( - repo *repo_model.Repository - gitRepo *git.Repository - ) - defer func() { _ = gitRepo.Close() }() // it's safe to call Close on a nil pointer, but it needs to use the latest value - +func hookPostReceiveCollectPushUpdates(opts *private.HookOptions, repo *repo_model.Repository) []*repo_module.PushUpdateOptions { updates := make([]*repo_module.PushUpdateOptions, 0, len(opts.OldCommitIDs)) - wasEmpty := false - for i := range opts.OldCommitIDs { refFullName := opts.RefFullNames[i] @@ -60,151 +40,124 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // or other less-standard refs spaces are ignored since there // may be a very large number of them). if refFullName.IsBranch() || refFullName.IsTag() { - if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) - if ctx.Written() { - // Error handled in loadRepository - return - } - wasEmpty = repo.IsEmpty - } - option := &repo_module.PushUpdateOptions{ RefFullName: refFullName, OldCommitID: opts.OldCommitIDs[i], NewCommitID: opts.NewCommitIDs[i], PusherID: opts.UserID, PusherName: opts.UserName, - RepoUserName: ownerName, - RepoName: repoName, + RepoUserName: repo.OwnerName, + RepoName: repo.Name, } updates = append(updates, option) - if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") { - // put the master/main branch first - // FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates. - // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once. - // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27 - // If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch. - copy(updates[1:], updates) - updates[0] = option + } + } + return updates +} + +func hookPostReceiveSyncDatabaseBranches(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository, updates []*repo_module.PushUpdateOptions) bool { + branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates)) + for _, update := range updates { + if !update.RefFullName.IsBranch() { + continue + } + if update.IsDelRef() { + if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, fmt.Sprintf("failed to mark branch %s as deleted", update.RefFullName)) + return false } + } else { + branchesToSync = append(branchesToSync, update) + // TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing + pull_service.UpdatePullsRefs(ctx, repo, update) } } - if repo != nil && len(updates) > 0 { - branchesToSync := make([]*repo_module.PushUpdateOptions, 0, len(updates)) - for _, update := range updates { - if !update.RefFullName.IsBranch() { - continue - } - if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) - if ctx.Written() { - return - } - wasEmpty = repo.IsEmpty - } - - if update.IsDelRef() { - if err := git_model.MarkBranchAsDeleted(ctx, repo.ID, update.RefFullName.BranchName(), update.PusherID); err != nil { - log.Error("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to mark branch as deleted: %s/%s Error: %v", ownerName, repoName, err), - }) - return - } - } else { - branchesToSync = append(branchesToSync, update) - - // TODO: should we return the error and return the error when pushing? Currently it will log the error and not prevent the pushing - pull_service.UpdatePullsRefs(ctx, repo, update) - } - } - if len(branchesToSync) > 0 { - var err error - gitRepo, err = gitrepo.OpenRepository(ctx, repo) - if err != nil { - log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), - }) - return - } - - var ( - branchNames = make([]string, 0, len(branchesToSync)) - commitIDs = make([]string, 0, len(branchesToSync)) - ) - for _, update := range branchesToSync { - branchNames = append(branchNames, update.RefFullName.BranchName()) - commitIDs = append(commitIDs, update.NewCommitID) - } - - if err := repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil { - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to sync branch to DB in repository: %s/%s Error: %v", ownerName, repoName, err), - }) - return - } - } - - if err := repo_service.PushUpdates(updates); err != nil { - log.Error("Failed to Update: %s/%s Total Updates: %d", ownerName, repoName, len(updates)) - for i, update := range updates { - log.Error("Failed to Update: %s/%s Update: %d/%d: Branch: %s", ownerName, repoName, i, len(updates), update.RefFullName.BranchName()) - } - log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) - - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), - }) - return - } + if len(branchesToSync) == 0 { + return true } + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) + if err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to open repository") + return false + } + + branchNames := make([]string, 0, len(branchesToSync)) + commitIDs := make([]string, 0, len(branchesToSync)) + for _, update := range branchesToSync { + branchNames = append(branchNames, update.RefFullName.BranchName()) + commitIDs = append(commitIDs, update.NewCommitID) + } + + if err = repo_service.SyncBranchesToDB(ctx, repo.ID, opts.UserID, branchNames, commitIDs, gitRepo.GetCommit); err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to sync branch to DB") + return false + } + return true +} + +// HookPostReceive updates services and users +func HookPostReceive(ctx *gitea_context.PrivateContext) { + opts := web.GetForm(ctx).(*private.HookOptions) + if opts.IsWiki { + setting.PanicInDevOrTesting("wiki hook-post-receive is not supported") + return + } + + ownerName := ctx.PathParam("owner") + repoName := ctx.PathParam("repo") + repo := loadRepository(ctx, ownerName, repoName) + if ctx.Written() { + return + } + // now, repo can't be nil + + // first, collect updates and sync branches + updates := hookPostReceiveCollectPushUpdates(opts, repo) + if !hookPostReceiveSyncDatabaseBranches(ctx, opts, repo, updates) { + return + } + hookPostReceiveSyncRepoDefaultBranch(ctx, opts, repo) + // handle pull request merging, a pull request action should push at least 1 commit if opts.PushTrigger == repo_module.PushTriggerPRMergeToBase { - handlePullRequestMerging(ctx, opts, ownerName, repoName, updates) - if ctx.Written() { + if !hookPostReceiveHandlePullRequestMerging(ctx, opts, updates) { return } } + if !hookPostReceiveUpdateRepoByOptions(ctx, opts, repo) { + return + } + + // push async updates + if err := repo_service.PushUpdates(updates...); err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to push updates") + return + } + + hookPostReceiveRespondWithTrailer(ctx, opts, repo) +} + +func hookPostReceiveUpdateRepoByOptions(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) bool { isPrivate := opts.GitPushOptions.Bool(private.GitPushOptionRepoPrivate) isTemplate := opts.GitPushOptions.Bool(private.GitPushOptionRepoTemplate) // Handle Push Options if isPrivate.Has() || isTemplate.Has() { - // load the repository - if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) - if ctx.Written() { - // Error handled in loadRepository - return - } - wasEmpty = repo.IsEmpty - } - pusher, err := loadContextCacheUser(ctx, opts.UserID) if err != nil { - log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), - }) - return + ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user") + return false } perm, err := access_model.GetDoerRepoPermission(ctx, repo, pusher) if err != nil { - log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), - }) - return + ctx.PrivateError(http.StatusInternalServerError, err, "failed to load doer repo permission") + return false } if !perm.IsOwner() && !perm.IsAdmin() { - ctx.JSON(http.StatusNotFound, private.HookPostReceiveResult{ - Err: "Permissions denied", - }) - return + ctx.PrivateError(http.StatusNotFound, nil, "permission denied") + return false } // FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag @@ -213,22 +166,37 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // TODO: it needs to do more work repo.IsPrivate = isPrivate.Value() if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"}) + log.Error("failed to update repo is_private: %v", err) } } if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() { repo.IsTemplate = isTemplate.Value() if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil { - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"}) + log.Error("failed to update repo is_template: %v", err) } } } + return true +} +func hookPostReceiveRespondWithTrailer(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) { results := make([]private.HookPostReceiveBranchResult, 0, len(opts.OldCommitIDs)) + baseRepo := repo + if repo.IsFork { + if err := repo.GetBaseRepo(ctx); err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to load base repo") + return + } + if repo.BaseRepo.AllowsPulls(ctx) { + baseRepo = repo.BaseRepo + } + } - // We have to reload the repo in case its state is changed above - repo = nil - var baseRepo *repo_model.Repository + if !baseRepo.AllowsPulls(ctx) { + // We can stop there's no need to go any further + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{}) + return + } // Now handle the pull request notification trailers for i := range opts.OldCommitIDs { @@ -237,66 +205,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { // If we've pushed a branch (and not deleted it) if !git.IsEmptyCommitID(newCommitID) && refFullName.IsBranch() { - // First ensure we have the repository loaded, we're allowed pulls requests and we can get the base repo - if repo == nil { - repo = loadRepository(ctx, ownerName, repoName) - if ctx.Written() { - return - } - - baseRepo = repo - - if repo.IsFork { - if err := repo.GetBaseRepo(ctx); err != nil { - log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err), - RepoWasEmpty: wasEmpty, - }) - return - } - if repo.BaseRepo.AllowsPulls(ctx) { - baseRepo = repo.BaseRepo - } - } - - if !baseRepo.AllowsPulls(ctx) { - // We can stop there's no need to go any further - ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ - RepoWasEmpty: wasEmpty, - }) - return - } - } - branch := refFullName.BranchName() - if branch == baseRepo.DefaultBranch { - if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{ - RepoID: repo.ID, - }); err != nil { - ctx.JSON(http.StatusInternalServerError, private.Response{Err: err.Error()}) - return - } - + if branch == baseRepo.DefaultBranch && !repo.IsFork { // If our branch is the default branch of an unforked repo - there's no PR to create or refer to - if !repo.IsFork { - results = append(results, private.HookPostReceiveBranchResult{}) - continue - } + results = append(results, private.HookPostReceiveBranchResult{}) + continue } pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, issues_model.PullRequestFlowGithub) - if err != nil && !issues_model.IsErrPullRequestNotExist(err) { - log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf( - "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err), - RepoWasEmpty: wasEmpty, - }) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to get active PR for branch "+branch) return } - if pr == nil { results = append(results, private.HookPostReceiveBranchResult{ Message: setting.Git.PullRequestPushMessage && baseRepo.AllowsPulls(ctx), @@ -314,43 +235,79 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { } } } - ctx.JSON(http.StatusOK, private.HookPostReceiveResult{ - Results: results, - RepoWasEmpty: wasEmpty, - }) + ctx.JSON(http.StatusOK, private.HookPostReceiveResult{Results: results}) } func loadContextCacheUser(ctx context.Context, id int64) (*user_model.User, error) { return cache.GetWithContextCache(ctx, cachegroup.User, id, user_model.GetUserByID) } -// handlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit -func handlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, ownerName, repoName string, updates []*repo_module.PushUpdateOptions) { +// hookPostReceiveHandlePullRequestMerging handle pull request merging, a pull request action should push at least 1 commit +func hookPostReceiveHandlePullRequestMerging(ctx *gitea_context.PrivateContext, opts *private.HookOptions, updates []*repo_module.PushUpdateOptions) bool { if len(updates) == 0 { - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID), - }) - return + err := fmt.Errorf("Pushing a merged PR (pr:%d) no commits pushed ", opts.PullRequestID) + ctx.PrivateError(http.StatusInternalServerError, err, "no push update") + return false } pr, err := issues_model.GetPullRequestByID(ctx, opts.PullRequestID) if err != nil { - log.Error("GetPullRequestByID[%d]: %v", opts.PullRequestID, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "GetPullRequestByID failed"}) - return + ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pull request") + return false } pusher, err := loadContextCacheUser(ctx, opts.UserID) if err != nil { - log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Load pusher user failed"}) - return + ctx.PrivateError(http.StatusInternalServerError, err, "failed to load pusher user") + return false } // FIXME: Maybe we need a `PullRequestStatusMerged` status for PRs that are merged, currently we use the previous status // here to keep it as before, that maybe PullRequestStatusMergeable - if _, err := pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status); err != nil { - log.Error("Failed to update PR to merged: %v", err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to update PR to merged"}) + _, err = pull_service.SetMerged(ctx, pr, updates[len(updates)-1].NewCommitID, timeutil.TimeStampNow(), pusher, pr.Status) + if err != nil { + ctx.PrivateError(http.StatusInternalServerError, err, "failed to set pr to merged") + return false + } + return true +} + +func hookPostReceiveSyncRepoDefaultBranch(ctx *gitea_context.PrivateContext, opts *private.HookOptions, repo *repo_model.Repository) { + hasBranch := false + for _, refFullName := range opts.RefFullNames { + if hasBranch = refFullName.IsBranch(); hasBranch { + break + } + } + if !hasBranch { + return + } + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) + if err != nil { + log.Error("failed to open git repo: %v", err) + return + } + + // if default branch doesn't exist, try to guess one from existing git repo + _, err = gitRepo.GetBranchCommitID(repo.DefaultBranch) + if errors.Is(err, util.ErrNotExist) { + for _, guessBranchName := range []string{"main", "master"} { + if _, err = gitRepo.GetBranchCommitID(guessBranchName); err == nil { + repo.DefaultBranch = guessBranchName + err = repo_model.UpdateDefaultBranch(ctx, repo) + if err != nil { + log.Error("failed to update default branch: %v", err) + return + } + break + } + } + } + + // if default branch was pushed, always keep the HEAD ref in sync + for _, refFullName := range opts.RefFullNames { + if refFullName.IsBranch() && refFullName.BranchName() == repo.DefaultBranch { + _ = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch) + } } } diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go index b18d9842e89..b465c7f6e8c 100644 --- a/routers/private/hook_post_receive_test.go +++ b/routers/private/hook_post_receive_test.go @@ -32,10 +32,10 @@ func TestHandlePullRequestMerging(t *testing.T) { autoMerge := unittest.AssertExistsAndLoadBean(t, &pull_model.AutoMerge{PullID: pr.ID}) ctx, resp := contexttest.MockPrivateContext(t, "/") - handlePullRequestMerging(ctx, &private.HookOptions{ + hookPostReceiveHandlePullRequestMerging(ctx, &private.HookOptions{ PullRequestID: pr.ID, UserID: 2, - }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ + }, []*repo_module.PushUpdateOptions{ {NewCommitID: "01234567"}, }) assert.Empty(t, resp.Body.String()) diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index aa3ad614c65..f5972c8db05 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -52,13 +52,16 @@ func Branches(ctx *context.Context) { kw := ctx.FormString("q") - defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize) + defaultBranchOptional, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize) if err != nil { ctx.ServerError("LoadBranches", err) return } - commitIDs := []string{defaultBranch.DBBranch.CommitID} + commitIDs := make([]string, 0, len(branches)+1) + if defaultBranchOptional != nil { + commitIDs = append(commitIDs, defaultBranchOptional.DBBranch.CommitID) + } for _, branch := range branches { commitIDs = append(commitIDs, branch.DBBranch.CommitID) } @@ -83,7 +86,7 @@ func Branches(ctx *context.Context) { ctx.Data["Branches"] = branches ctx.Data["CommitStatus"] = commitStatus ctx.Data["CommitStatuses"] = commitStatuses - ctx.Data["DefaultBranchBranch"] = defaultBranch + ctx.Data["DefaultBranchBranch"] = defaultBranchOptional pager := context.NewPagination(branchesCount, pageSize, page, 5) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager @@ -152,7 +155,7 @@ func RestoreBranchPost(ctx *context.Context) { objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName) // Don't return error below this - if err := repo_service.PushUpdate( + if err := repo_service.PushUpdates( &repo_module.PushUpdateOptions{ RefFullName: git.RefNameFromBranch(deletedBranch.Name), OldCommitID: objectFormat.EmptyObjectID().String(), diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index ba4828faf8b..d23cca7fa56 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -18,7 +18,6 @@ import ( repo_model "gitea.dev/models/repo" "gitea.dev/models/unit" user_model "gitea.dev/models/user" - "gitea.dev/modules/cache" "gitea.dev/modules/git" "gitea.dev/modules/log" "gitea.dev/modules/optional" @@ -63,22 +62,6 @@ func MustBeAbleToUpload(ctx *context.Context) { } } -func CommitInfoCache(ctx *context.Context) { - var err error - ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.ServerError("GetBranchCommit", err) - return - } - ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx) - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } - ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount - ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) -} - func checkContextUser(ctx *context.Context, uid int64) *user_model.User { orgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID) if err != nil { diff --git a/routers/web/web.go b/routers/web/web.go index 49a83c1fae5..ee30b614c85 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1476,15 +1476,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Group("/releases", func() { m.Get("/new", repo.NewRelease) m.Post("/new", web.Bind(forms.NewReleaseForm{}), repo.NewReleasePost) + m.Get("/edit/*", repo.EditRelease) + m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) m.Post("/generate-notes", web.Bind(forms.GenerateReleaseNotesForm{}), repo.GenerateReleaseNotes) m.Post("/delete", repo.DeleteRelease) m.Post("/attachments", repo.UploadReleaseAttachment) m.Post("/attachments/remove", repo.DeleteAttachment) }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter) - m.Group("/releases", func() { - m.Get("/edit/*", repo.EditRelease) - m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost) - }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, repo.CommitInfoCache) }, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader) // end "/{username}/{reponame}": repo releases diff --git a/services/context/private.go b/services/context/private.go index 2c0d21102ae..e687821b07f 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -9,6 +9,7 @@ import ( "time" "gitea.dev/modules/graceful" + "gitea.dev/modules/private" "gitea.dev/modules/process" "gitea.dev/modules/web" web_types "gitea.dev/modules/web/types" @@ -49,6 +50,14 @@ func (ctx *PrivateContext) Err() error { return ctx.Base.Err() } +func (ctx *PrivateContext) PrivateError(status int, err error, userMsg string) { + errMsg := "" + if err != nil { + errMsg = err.Error() + } + ctx.JSON(status, private.Response{Err: errMsg, UserMsg: userMsg}) +} + type privateContextKeyType struct{} var privateContextKey privateContextKeyType diff --git a/services/context/repo.go b/services/context/repo.go index fa816ba6adf..81df87a67f4 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -972,12 +972,9 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refShortName) if err == nil { ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if strings.Contains(err.Error(), "fatal: not a git repository") || strings.Contains(err.Error(), "object does not exist") { + } else { // if the repository is broken, we can continue to the handler code, to show "Settings -> Delete Repository" for end users log.Error("GetBranchCommit: %v", err) - } else { - ctx.ServerError("GetBranchCommit", err) - return } } else { // there is a path in request guessLegacyPath := refType == "" diff --git a/services/repository/branch.go b/services/repository/branch.go index 9ab485ca717..0869750db43 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -59,9 +59,9 @@ type Branch struct { } // LoadBranches loads branches from the repository limited by page & pageSize. -func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (*Branch, []*Branch, int64, error) { - defaultDBBranch, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) - if err != nil { +func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, isDeletedBranch optional.Option[bool], keyword string, page, pageSize int) (defaultBranchOptional *Branch, _ []*Branch, _ int64, _ error) { + defaultDBBranchOptional, err := git_model.GetBranch(ctx, repo.ID, repo.DefaultBranch) + if err != nil && !errors.Is(err, util.ErrNotExist) { return nil, nil, 0, err } @@ -108,13 +108,14 @@ func LoadBranches(ctx context.Context, repo *repo_model.Repository, gitRepo *git branches = append(branches, branch) } - // Always add the default branch - log.Debug("loadOneBranch: load default: '%s'", defaultDBBranch.Name) - defaultBranch, err := loadOneBranch(ctx, repo, defaultDBBranch, &rules, repoIDToRepo, repoIDToGitRepo) - if err != nil { - return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + if defaultDBBranchOptional != nil { + // Always add the default branch + defaultBranchOptional, err = loadOneBranch(ctx, repo, defaultDBBranchOptional, &rules, repoIDToRepo, repoIDToGitRepo) + if err != nil { + return nil, nil, 0, fmt.Errorf("loadOneBranch: %v", err) + } } - return defaultBranch, branches, totalNumOfBranches, nil + return defaultBranchOptional, branches, totalNumOfBranches, nil } func getDivergenceCacheKey(repoID int64, branchName string) string { @@ -640,7 +641,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R func deleteBranchSuccessPostProcess(doer *user_model.User, repo *repo_model.Repository, branchName string, branchCommit *git.Commit) { objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) - if err := PushUpdate( + if err := PushUpdates( &repo_module.PushUpdateOptions{ RefFullName: git.RefNameFromBranch(branchName), OldCommitID: branchCommit.ID.String(), diff --git a/services/repository/push.go b/services/repository/push.go index 895e3de0200..7666ecf377b 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -32,10 +32,26 @@ import ( // pushQueue represents a queue to handle update pull request tests var pushQueue *queue.WorkerPoolQueue[[]*repo_module.PushUpdateOptions] -// handle passed PR IDs and test the PRs -func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions { +func initPushQueue() error { + pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", pushQueueHandler) + if pushQueue == nil { + return errors.New("unable to create push_update queue") + } + go graceful.GetManager().RunWithCancel(pushQueue) + return nil +} + +// PushUpdates adds a push update to push queue, each call must pass the same repo updates +func PushUpdates(opts ...*repo_module.PushUpdateOptions) error { + if len(opts) == 0 { + return nil + } + return pushQueue.Push(opts) +} + +func pushQueueHandler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpdateOptions { for _, opts := range items { - if err := pushUpdates(opts); err != nil { + if err := pushQueueHandleUpdates(opts); err != nil { // Username and repository stays the same between items in opts. pushUpdate := opts[0] log.Error("pushUpdate[%s/%s] failed: %v", pushUpdate.RepoUserName, pushUpdate.RepoName, err) @@ -44,37 +60,8 @@ func handler(items ...[]*repo_module.PushUpdateOptions) [][]*repo_module.PushUpd return nil } -func initPushQueue() error { - pushQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "push_update", handler) - if pushQueue == nil { - return errors.New("unable to create push_update queue") - } - go graceful.GetManager().RunWithCancel(pushQueue) - return nil -} - -// PushUpdate is an alias of PushUpdates for single push update options -func PushUpdate(opts *repo_module.PushUpdateOptions) error { - return PushUpdates([]*repo_module.PushUpdateOptions{opts}) -} - -// PushUpdates adds a push update to push queue -func PushUpdates(opts []*repo_module.PushUpdateOptions) error { - if len(opts) == 0 { - return nil - } - - for _, opt := range opts { - if opt.IsNewRef() && opt.IsDelRef() { - return errors.New("Old and new revisions are both NULL") - } - } - - return pushQueue.Push(opts) -} - -// pushUpdates generates push action history feeds for push updating multiple refs -func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { +// pushQueueHandleUpdates generates push action history feeds for push updating multiple refs +func pushQueueHandleUpdates(optsList []*repo_module.PushUpdateOptions) error { if len(optsList) == 0 { return nil } @@ -94,7 +81,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { defer gitRepo.Close() if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { - return fmt.Errorf("Failed to update size for repository: %v", err) + return fmt.Errorf("failed to update size for repository: %v", err) } addTags := make([]string, 0, len(optsList)) @@ -104,10 +91,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { for _, opts := range optsList { log.Trace("pushUpdates: %-v %s %s %s", repo, opts.OldCommitID, opts.NewCommitID, opts.RefFullName) - if opts.IsNewRef() && opts.IsDelRef() { - return fmt.Errorf("old and new revisions are both %s", objectFormat.EmptyObjectID()) + setting.PanicInDevOrTesting("invalid push update (add+del): %+v", opts) + continue } + if opts.RefFullName.IsTag() { if pusher == nil || pusher.ID != opts.PusherID { if opts.PusherID == user_model.ActionsUserID { @@ -188,11 +176,14 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { return err } - // delete cache for divergence + // sync branch related database data if branch == repo.DefaultBranch { if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil { log.Error("DelRepoDivergenceFromCache: %v", err) } + if err := AddRepoToLicenseUpdaterQueue(&LicenseUpdaterOptions{RepoID: repo.ID}); err != nil { + log.Error("AddRepoToLicenseUpdaterQueue: %v", err) + } } else { if err := DelDivergenceFromCache(repo.ID, branch); err != nil { log.Error("DelDivergenceFromCache: %v", err) diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 7d8f2bfc5b8..6d1fa21151e 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -4,7 +4,6 @@
    {{template "base/alert" .}} {{template "repo/sub_menu" .}} - {{if .DefaultBranchBranch}}

    {{ctx.Locale.Tr "repo.default_branch"}} {{if and $.IsWriter $.Repository.CanContentChange (not .IsDeleted)}} @@ -14,7 +13,8 @@ {{end}}

    -
    + {{if .DefaultBranchBranch}} +
    @@ -69,6 +69,10 @@
    + {{else}} +
    + {{ctx.Locale.Tr "repo.branch.default_branch_not_exist" $.Repository.DefaultBranch}} +
    {{end}}

    diff --git a/tests/integration/branches_test.go b/tests/integration/branches_test.go index 22b1549252d..0763aef68fb 100644 --- a/tests/integration/branches_test.go +++ b/tests/integration/branches_test.go @@ -8,6 +8,9 @@ import ( "net/url" "testing" + "gitea.dev/models/db" + repo_model "gitea.dev/models/repo" + "gitea.dev/models/unittest" "gitea.dev/modules/translation" "gitea.dev/tests" @@ -18,55 +21,52 @@ import ( func TestViewBranches(t *testing.T) { defer tests.PrepareTestEnv(t)() + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) req := NewRequest(t, "GET", "/user2/repo1/branches") resp := MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - _, exists := htmlDoc.doc.Find(".delete-branch-button").Attr("data-url") - assert.False(t, exists, "The template has changed") -} + AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-list]", 1) + AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-not-exist]", 0) -func TestDeleteBranch(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - deleteBranch(t) + repo1.DefaultBranch = "non-existent-branch" + _, _ = db.GetEngine(t.Context()).ID(repo1.ID).Cols("default_branch").Update(repo1) + req = NewRequest(t, "GET", "/user2/repo1/branches") + resp = MakeRequest(t, req, http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-list]", 0) + AssertHTMLElement(t, htmlDoc, "[data-testid=branches-default-branch-not-exist]", 1) } func TestUndoDeleteBranch(t *testing.T) { + branchAction := func(t *testing.T, button string) (*HTMLDoc, string) { + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/repo1/branches") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find(button).Attr("data-url") + require.True(t, exists, "The template has changed") + linkURL, err := url.Parse(link) + require.NoError(t, err) + + req = NewRequest(t, "POST", link) + session.MakeRequest(t, req, http.StatusOK) + req = NewRequest(t, "GET", "/user2/repo1/branches") + resp = session.MakeRequest(t, req, http.StatusOK) + + return NewHTMLParser(t, resp.Body), linkURL.Query().Get("name") + } + onGiteaRun(t, func(t *testing.T, u *url.URL) { - deleteBranch(t) - htmlDoc, name := branchAction(t, ".restore-branch-button") + htmlDoc, name := branchAction(t, ".delete-branch-button") + assert.Contains(t, + htmlDoc.doc.Find(".ui.positive.message").Text(), + translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name), + ) + htmlDoc, name = branchAction(t, ".restore-branch-button") assert.Contains(t, htmlDoc.doc.Find(".ui.positive.message").Text(), translation.NewLocale("en-US").TrString("repo.branch.restore_success", name), ) }) } - -func deleteBranch(t *testing.T) { - htmlDoc, name := branchAction(t, ".delete-branch-button") - assert.Contains(t, - htmlDoc.doc.Find(".ui.positive.message").Text(), - translation.NewLocale("en-US").TrString("repo.branch.deletion_success", name), - ) -} - -func branchAction(t *testing.T, button string) (*HTMLDoc, string) { - session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user2/repo1/branches") - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find(button).Attr("data-url") - require.True(t, exists, "The template has changed") - - req = NewRequest(t, "POST", link) - session.MakeRequest(t, req, http.StatusOK) - - url, err := url.Parse(link) - assert.NoError(t, err) - req = NewRequest(t, "GET", "/user2/repo1/branches") - resp = session.MakeRequest(t, req, http.StatusOK) - - return NewHTMLParser(t, resp.Body), url.Query().Get("name") -} From a68ee6a4057ff3768a3b2e11b1766be1ca0771f8 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sat, 13 Jun 2026 02:56:17 -0700 Subject: [PATCH 74/88] fix(deps): update dependency esbuild to v0.28.1 [security] (#38097) --- package.json | 2 +- pnpm-lock.yaml | 254 ++++++++++++++++++++++++------------------------- 2 files changed, 128 insertions(+), 128 deletions(-) diff --git a/package.json b/package.json index c35457e167c..8b313bdfed1 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "cropperjs": "1.6.2", "dayjs": "1.11.21", "easymde": "2.21.0", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee94cec7f24..c30124ac035 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,7 +94,7 @@ importers: version: 2.6.2 '@vitejs/plugin-vue': specifier: 6.0.7 - version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) + version: 6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -132,8 +132,8 @@ importers: specifier: 2.21.0 version: 2.21.0 esbuild: - specifier: 0.28.0 - version: 0.28.0 + specifier: 0.28.1 + version: 0.28.1 idiomorph: specifier: 0.7.4 version: 0.7.4 @@ -193,10 +193,10 @@ importers: version: 0.7.2 vite: specifier: 8.0.16 - version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0) vite-string-plugin: specifier: 2.0.4 - version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) vue: specifier: 3.5.35 version: 3.5.35(typescript@6.0.3) @@ -257,7 +257,7 @@ importers: version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': specifier: 1.6.19 - version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))) + version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))) eslint: specifier: 10.4.1 version: 10.4.1(jiti@2.7.0) @@ -347,7 +347,7 @@ importers: version: 17.17.3 vitest: specifier: 4.1.8 - version: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + version: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) vue-tsc: specifier: 3.3.3 version: 3.3.3(typescript@6.0.3) @@ -592,158 +592,158 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.28.0': - resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.28.0': - resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.28.0': - resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.28.0': - resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.28.0': - resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.28.0': - resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.28.0': - resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.0': - resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.28.0': - resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.28.0': - resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.28.0': - resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.28.0': - resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.28.0': - resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.28.0': - resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.28.0': - resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.28.0': - resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.28.0': - resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.28.0': - resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.0': - resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.28.0': - resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.0': - resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.28.0': - resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.28.0': - resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.28.0': - resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.28.0': - resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.28.0': - resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2498,8 +2498,8 @@ packages: es-toolkit@1.47.0: resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} - esbuild@0.28.0: - resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -5352,82 +5352,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.28.0': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.28.0': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.28.0': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.28.0': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.28.0': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.28.0': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.28.0': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.28.0': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.28.0': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.28.0': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.28.0': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.28.0': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.28.0': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.28.0': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.28.0': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.28.0': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.28.0': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.28.0': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.28.0': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.28.0': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.28.0': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.28.0': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.28.0': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.28.0': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.28.0': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.28.0': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-plugin-eslint-comments@4.7.2(eslint@10.4.1(jiti@2.7.0))': @@ -6358,13 +6358,13 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))(vue@3.5.35(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0) vue: 3.5.35(typescript@6.0.3) - '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)))': + '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)))': dependencies: '@typescript-eslint/scope-manager': 8.60.1 '@typescript-eslint/utils': 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) @@ -6372,7 +6372,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.4.1(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 - vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + vitest: 4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -6385,13 +6385,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -7371,34 +7371,34 @@ snapshots: es-toolkit@1.47.0: {} - esbuild@0.28.0: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.0 - '@esbuild/android-arm': 0.28.0 - '@esbuild/android-arm64': 0.28.0 - '@esbuild/android-x64': 0.28.0 - '@esbuild/darwin-arm64': 0.28.0 - '@esbuild/darwin-x64': 0.28.0 - '@esbuild/freebsd-arm64': 0.28.0 - '@esbuild/freebsd-x64': 0.28.0 - '@esbuild/linux-arm': 0.28.0 - '@esbuild/linux-arm64': 0.28.0 - '@esbuild/linux-ia32': 0.28.0 - '@esbuild/linux-loong64': 0.28.0 - '@esbuild/linux-mips64el': 0.28.0 - '@esbuild/linux-ppc64': 0.28.0 - '@esbuild/linux-riscv64': 0.28.0 - '@esbuild/linux-s390x': 0.28.0 - '@esbuild/linux-x64': 0.28.0 - '@esbuild/netbsd-arm64': 0.28.0 - '@esbuild/netbsd-x64': 0.28.0 - '@esbuild/openbsd-arm64': 0.28.0 - '@esbuild/openbsd-x64': 0.28.0 - '@esbuild/openharmony-arm64': 0.28.0 - '@esbuild/sunos-x64': 0.28.0 - '@esbuild/win32-arm64': 0.28.0 - '@esbuild/win32-ia32': 0.28.0 - '@esbuild/win32-x64': 0.28.0 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -10019,11 +10019,11 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vite-string-plugin@2.0.4(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)): dependencies: - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0) - vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0): + vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -10032,14 +10032,14 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.1 - esbuild: 0.28.0 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): + vitest@4.1.8(@types/node@25.9.1)(happy-dom@20.10.1)(jsdom@20.0.3)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -10056,7 +10056,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0) + vite: 8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.9.1 From aab9737651ce3d2ab61bf8559133dbe154f86cea Mon Sep 17 00:00:00 2001 From: bircni Date: Sat, 13 Jun 2026 12:14:02 +0200 Subject: [PATCH 75/88] fix(ui): prevent commit status popup overflowing its row (#38081) Fixes #38079 ## Regression path The layout previously had `.commit-status-item .status-context { flex: 1 }`, which let the context fill remaining space and ellipsize. That rule was dropped in #37517 ("Refactor pull request view (5)") when the row markup moved to nested `.flex-text-block` wrappers, so nothing constrained the left block anymore. After: image --------- Co-authored-by: wxiaoguang --- templates/repo/pulls/status_items.tmpl | 8 ++++---- web_src/css/base.css | 5 ++++- web_src/css/repo.css | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/templates/repo/pulls/status_items.tmpl b/templates/repo/pulls/status_items.tmpl index fa2a5f80bf7..f3dc1bb23f9 100644 --- a/templates/repo/pulls/status_items.tmpl +++ b/templates/repo/pulls/status_items.tmpl @@ -6,14 +6,14 @@ {{$commitActionsStatuses := ctx.ActionsUtils.CommitStatusesToActionsStatuses $.CommitStatuses}} {{range $cs := $.CommitStatuses}}
    -
    +
    {{$actionStatus := $commitActionsStatuses.IconStatus $cs}} {{if $actionStatus}} {{template "repo/icons/action_status" (dict "Status" $actionStatus "Size" 18 "ClassName" "commit-status icon")}} {{else}} {{template "repo/icons/commit_status" $cs}} {{end}} -
    +
    {{$cs.Context}} {{$cs.Description}}
    @@ -29,9 +29,9 @@ {{end}} {{range $missingCheck := $statusCheckData.MissingRequiredChecks}}
    -
    +
    {{svg "octicon-dot-fill" 16 "commit-status icon tw-text-yellow"}} -
    {{$missingCheck}}
    +
    {{$missingCheck}}
    {{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}
    diff --git a/web_src/css/base.css b/web_src/css/base.css index 4b489362afb..92b82e9d89a 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -840,7 +840,8 @@ table th[data-sortt-desc] .svg { align-items: center; gap: var(--gap-inline); vertical-align: middle; - min-width: 0; /* make ellipsis work */ + max-width: 100%; /* the inner part might have "gt-ellipsis" */ + min-width: 0; /* if it is the top flex container, "max-width" works; but if it is inside another flex container, the parent needs to handle the x-axis and here also needs "min-width" */ } .ui.ui.labeled.button { @@ -856,6 +857,7 @@ table th[data-sortt-desc] .svg { justify-content: space-between; align-items: center; gap: var(--gap-block); + max-width: 100%; min-width: 0; } @@ -868,6 +870,7 @@ table th[data-sortt-desc] .svg { align-items: center; gap: var(--gap-block); max-width: 100%; + min-width: 0; } .flex-left-right > .ui.button, diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 16337a9eadb..e5c293a4d69 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1863,11 +1863,11 @@ tbody.commit-list { } .username-display { - max-width: 100%; /* the inner part might have "gt-ellipsis" */ - min-width: 0; /* if it is the top flex container, "max-width" works; but if it is inside another flex container, the parent needs to handle the x-axis and here also needs "min-width" */ display: inline-flex; gap: var(--gap-inline); align-items: center; + max-width: 100%; /* min/max widths are for "gt-ellipsis", see the comment of other "flex-xxx" family classes */ + min-width: 0; } .username-display > .username-fullname { From e82352f156de2cb29d2f9ced59f0a649a81926ea Mon Sep 17 00:00:00 2001 From: Karthik Bhandary <34509856+karthikbhandary2@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:22:37 +0530 Subject: [PATCH 76/88] feat(web): Add Jupyter Notebook (.ipynb) Rendering Support (#37433) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Closes #37308 Adds native rendering support for Jupyter notebook files (`.ipynb`) in Gitea using backend rendering, allowing users to view formatted notebooks with code cells, markdown, outputs, and visualizations directly in the repository browser. ### Motivation Jupyter notebooks are widely used in data science, machine learning, and scientific computing. Currently, Gitea displays `.ipynb` files as raw JSON, making them difficult to read. This feature enables users to view notebooks in a formatted, readable way similar to GitHub and GitLab. ### Implementation Approach **Evolution:** Initially implemented frontend rendering using `marked` and `Shiki` libraries. After review feedback, migrated to backend rendering for better performance, security, and consistency with Gitea architecture. #### Backend Rendering Advantages - Server-side HTML generation eliminates client-side parsing overhead - Integrates with Gitea existing markup sanitizer for security - Uses Chroma for syntax highlighting (consistent with code files) - Uses Goldmark for markdown rendering (consistent with `.md` files) - No additional frontend dependencies required - Better performance for large notebooks ### Features #### Supported Cell Types - **Markdown cells:** Rendered with Goldmark (tables, lists, links, code blocks, etc.) - **Code cells:** Syntax-highlighted with Chroma, execution counts, language detection from notebook metadata - **Output cells:** Multiple output types in a single cell #### Supported Output Types - ✅ Text/plain outputs - ✅ Images (PNG, JPEG, SVG) with base64 data URIs - ✅ HTML outputs (tables, DataFrames, formatted text) - ✅ LaTeX/math equations (rendered as code blocks) - ✅ Error outputs with traceback (styled in red) - ✅ Stream outputs (`stdout`/`stderr`) - ⚠️ Interactive widgets (Plotly, ipywidgets) show informative messages - ⚠️ JavaScript outputs show security warning (disabled for safety) #### Edge Cases Handled - Empty notebooks or notebooks with no outputs - Corrupted JSON with graceful error display - Mixed output types in single cell - Large base64-encoded images - Execution count of `null` or `0` - `nbformat` version compatibility (only renders `nbformat 4+`, shows message for older versions) ### Changes #### Backend (Go) - `modules/markup/jupyter/jupyter.go` (**NEW**) - Jupyter notebook renderer implementation - Parses `.ipynb` JSON structure and generates HTML - Integrates Chroma for code syntax highlighting - Integrates Goldmark for markdown cell rendering - Dynamic language detection from notebook metadata - Handles all standard Jupyter output types - Comprehensive error handling with user-friendly messages - `modules/markup/renderer.go` (**MODIFIED**) - Registered Jupyter renderer in markup system - `main.go` (**MODIFIED**) - Import Jupyter renderer package for initialization #### Styling (CSS) - `web_src/css/markup/jupyter.css` (**NEW**) - Comprehensive styling for notebook cells, code, outputs - Uses Gitea CSS variables for consistent theming - Responsive layout with proper spacing - Table styling for DataFrame outputs - Removed parent container padding for consistency with other renderers #### Sanitizer Rules - `modules/markup/jupyter/jupyter.go` → `SanitizerRules()` - Configured HTML sanitization rules for safe rendering: - Cell structure (markdown, code, input/output wrappers) - Code highlighting (Chroma classes) - Images (base64 data URIs only) - Tables (DataFrames) - Markdown elements (headers, lists, links, etc.) ### Security Considerations - Server-side rendering: No client-side JavaScript execution - HTML sanitization: Strict allowlist for HTML elements and attributes - Image security: Only base64 data URIs allowed (no external URLs) - JavaScript disabled: `application/javascript` outputs show warning - XSS protection: Gitea markup sanitizer handles all HTML output ### Testing Manual testing performed with various notebooks: - Markdown rendering (headers, lists, tables, links, code blocks) - Code cells with execution counts and syntax highlighting - Multiple output types (text, images, HTML, LaTeX, errors, streams) - Error handling for edge cases - Theme compatibility (light/dark mode) ### Screenshots image image image image image image image ### Dependencies No new dependencies required: - Chroma (existing) - Syntax highlighting - Goldmark (existing) - Markdown rendering - Standard library - JSON parsing ### Key Design Decisions - Backend rendering for performance and security - Reuses existing Gitea infrastructure (Chroma, Goldmark, sanitizer) - Consistent styling with other markup renderers - Graceful degradation for unsupported features --- **Development Note:** This PR was developed with assistance from Amazon Q Developer and Claude AI for implementation, debugging, and testing. --------- Signed-off-by: Karthik Bhandary <34509856+karthikbhandary2@users.noreply.github.com> Co-authored-by: karthik.bhandary Co-authored-by: wxiaoguang Co-authored-by: bircni --- main.go | 1 + modules/htmlutil/html.go | 47 +++ modules/htmlutil/html_test.go | 9 + modules/markup/jupyter/jupyter-test.ipynb | 74 ++++ modules/markup/jupyter/jupyter.go | 393 ++++++++++++++++++++++ modules/markup/jupyter/jupyter_test.go | 314 +++++++++++++++++ modules/test/utils.go | 49 +++ tests/integration/html_helper.go | 39 +-- web_src/css/index.css | 1 + web_src/css/markup/jupyter.css | 93 +++++ 10 files changed, 987 insertions(+), 33 deletions(-) create mode 100644 modules/markup/jupyter/jupyter-test.ipynb create mode 100644 modules/markup/jupyter/jupyter.go create mode 100644 modules/markup/jupyter/jupyter_test.go create mode 100644 web_src/css/markup/jupyter.css diff --git a/main.go b/main.go index 80b8a51d4a4..0a3d0164fa2 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( // register supported doc types _ "gitea.dev/modules/markup/console" _ "gitea.dev/modules/markup/csv" + _ "gitea.dev/modules/markup/jupyter" _ "gitea.dev/modules/markup/markdown" _ "gitea.dev/modules/markup/orgmode" diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index 5db1d6404ad..a480826bc61 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -4,6 +4,7 @@ package htmlutil import ( + "errors" "fmt" "html/template" "io" @@ -88,6 +89,52 @@ func EscapeString(s string) template.HTML { return template.HTML(template.HTMLEscapeString(s)) } +type HTMLWriter interface { + OriginWriter() io.Writer + WriteString(s string) HTMLWriter + WriteHTML(s template.HTML) HTMLWriter + WriteFormat(fmt template.HTML, args ...any) HTMLWriter + Err() error +} + +type htmlWriter struct { + w io.Writer + errs []error +} + +func (h *htmlWriter) OriginWriter() io.Writer { + return h.w +} + +func (h *htmlWriter) WriteString(s string) HTMLWriter { + if _, err := io.WriteString(h.w, template.HTMLEscapeString(s)); err != nil { + h.errs = append(h.errs, err) + } + return h +} + +func (h *htmlWriter) WriteHTML(s template.HTML) HTMLWriter { + if _, err := io.WriteString(h.w, string(s)); err != nil { + h.errs = append(h.errs, err) + } + return h +} + +func (h *htmlWriter) WriteFormat(fmt template.HTML, args ...any) HTMLWriter { + if _, err := HTMLPrintf(h.w, fmt, args...); err != nil { + h.errs = append(h.errs, err) + } + return h +} + +func (h *htmlWriter) Err() error { + return errors.Join(h.errs...) +} + +func NewHTMLWriter(w io.Writer) HTMLWriter { + return &htmlWriter{w: w} +} + type HTMLBuilder struct { sb strings.Builder } diff --git a/modules/htmlutil/html_test.go b/modules/htmlutil/html_test.go index a1ab0a6a49b..88e4935a1df 100644 --- a/modules/htmlutil/html_test.go +++ b/modules/htmlutil/html_test.go @@ -5,6 +5,7 @@ package htmlutil import ( "html/template" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -29,3 +30,11 @@ func TestHTMLBuilder(t *testing.T) { assert.Equal(t, "<
    >>", b.String()) assert.Equal(t, template.HTML("<
    >>"), b.HTMLString()) } + +func TestHTMLWriter(t *testing.T) { + sb := new(strings.Builder) + w := NewHTMLWriter(sb) + w.WriteString("<").WriteHTML("
    ").WriteFormat("%s%s", ">", EscapeString(">")) + assert.Equal(t, "<
    >>", sb.String()) + assert.NoError(t, w.Err()) +} diff --git a/modules/markup/jupyter/jupyter-test.ipynb b/modules/markup/jupyter/jupyter-test.ipynb new file mode 100644 index 00000000000..1bb45f99584 --- /dev/null +++ b/modules/markup/jupyter/jupyter-test.ipynb @@ -0,0 +1,74 @@ +{ + "metadata": {}, + "nbformat": 4, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "source": ["print('very-looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong')"], + "outputs": [ + { + "output_type": "execute_result", + "text": ["very-looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong ...\n"] + }, + { + "output_type": "stream", + "name": "stdout", + "text": ["stdout 1 ...\n", "stdout 2 ...\n"] + }, + { + "output_type": "stream", + "name": "stderr", + "text": ["stderr ...\n"] + }, + { + "data": { + "text/plain": ["data text 1\n", "data text 2\n"] + } + }, + { + "data": { + "text/plain": true + } + }, + { + "data": { + "image/svg+xml": [""] + } + }, + { + "data": { + "text/html": "HTML Link" + } + }, + { + "data": { + "text/latex": "$$a=1$$" + } + }, + { + "data": { + "text/plain": "plain text" + } + }, + { + "output_type": "error", + "ename": "Error Name", + "traceback": ["stacktrace 1", "stacktrace 2"] + } + ] + }, + { + "cell_type": "unknown-cell" + }, + { + "cell_type": "markdown", + "source": [ + "# h1\n", "## h2\n", "### h3\n", "\n", "paragraph 1\n", "\n", + "very-looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong\n", + "- list item 1\n", "- list item 2\n", "\n", "```python\n", "print('code block')\n", "```\n", + "
    th1th2
    td1td2
    \n" + ] + } + ] +} diff --git a/modules/markup/jupyter/jupyter.go b/modules/markup/jupyter/jupyter.go new file mode 100644 index 00000000000..059046c1365 --- /dev/null +++ b/modules/markup/jupyter/jupyter.go @@ -0,0 +1,393 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jupyter + +import ( + "encoding/base64" + "fmt" + "io" + "strings" + "sync" + + "gitea.dev/modules/highlight" + "gitea.dev/modules/htmlutil" + "gitea.dev/modules/json" + "gitea.dev/modules/log" + "gitea.dev/modules/markup" + "gitea.dev/modules/markup/markdown" + "gitea.dev/modules/setting" + "gitea.dev/modules/util" +) + +func init() { + markup.RegisterRenderer(renderer{}) +} + +// Renderer implements markup.Renderer for Jupyter notebooks +type renderer struct{} + +var ( + _ markup.Renderer = (*renderer)(nil) + _ markup.PostProcessRenderer = (*renderer)(nil) + _ markup.ExternalRenderer = (*renderer)(nil) // FIXME: this is not an external render, need to refactor the framework in the future +) + +type mimeHandler struct { + Mime string + Fn func(w htmlutil.HTMLWriter, data string) error +} + +func renderCellCodeOutputTextPlain(w htmlutil.HTMLWriter, text string) error { + w.WriteFormat(`
    %s
    `, text) + return w.Err() +} + +func renderCellCodeOutputUnsupported(w htmlutil.HTMLWriter, message string) error { + w.WriteFormat(`
    %s
    `, message) + return w.Err() +} + +var dataMimeHandlers = sync.OnceValue(func() []mimeHandler { + renderImage := func(w htmlutil.HTMLWriter, subtype, payload string) error { + w.WriteFormat(`
    `, subtype, payload) + return w.Err() + } + renderUnsupportedOutput := func(message string) func(htmlutil.HTMLWriter, string) error { + return func(w htmlutil.HTMLWriter, _ string) error { + return renderCellCodeOutputUnsupported(w, message) + } + } + return []mimeHandler{ + // Images (PNG, JPEG, SVG) + {"image/png", func(w htmlutil.HTMLWriter, d string) error { + return renderImage(w, "png", d) + }}, + {"image/jpeg", func(w htmlutil.HTMLWriter, d string) error { + return renderImage(w, "jpeg", d) + }}, + {"image/svg+xml", func(w htmlutil.HTMLWriter, d string) error { + return renderImage(w, "svg+xml", base64.StdEncoding.EncodeToString(util.UnsafeStringToBytes(d))) + }}, + + // Rich & Math Layouts + {"text/html", func(w htmlutil.HTMLWriter, d string) error { + // To future developers: don't allow custom CSS classes or attributes, + // because ".link-action" or "data-fetch-xxx" can send POST requests and lead to XSS. + // If you'd really like to support more, do remember to correctly sanitize the values. + w.WriteFormat(`
    %s
    `, markup.Sanitize(d)) + return w.Err() + }}, + {"text/latex", func(w htmlutil.HTMLWriter, d string) error { + w.WriteFormat(`
    %s
    `, trimMathDelimiters(d)) + return w.Err() + }}, + {"text/plain", renderCellCodeOutputTextPlain}, + + // Security Placeholders + {"application/javascript", renderUnsupportedOutput("[JavaScript output - execution disabled for security]")}, + {"application/vnd.plotly.v1+json", renderUnsupportedOutput("[Plotly output - interactive plots not supported]")}, + {"application/vnd.jupyter.widget-view+json", renderUnsupportedOutput("[Jupyter widget - interactive widgets not supported]")}, + } +}) + +func (renderer) Name() string { + return "jupyter-render" +} + +func (renderer) NeedPostProcess() bool { return true } + +func (renderer) GetExternalRendererOptions() markup.ExternalRendererOptions { + return markup.ExternalRendererOptions{ + // HINT: no need to let markup render sanitize the output because there are many special CSS class names, inline attributes. + // This render must guarantee that the output is safe and no XSS + SanitizerDisabled: true, + } +} + +func (renderer) FileNamePatterns() []string { + return []string{"*.ipynb"} +} + +func (renderer) SanitizerRules() []setting.MarkupSanitizerRule { + return nil +} + +// Notebook structures +type Notebook struct { + Cells []Cell `json:"cells"` + Metadata map[string]any `json:"metadata"` + Nbformat int `json:"nbformat"` +} + +type Cell struct { + CellType string `json:"cell_type"` + Source any `json:"source"` // string or []string + Outputs []Output `json:"outputs,omitempty"` + ExecutionCount any `json:"execution_count,omitempty"` // int or null + Metadata map[string]any `json:"metadata,omitempty"` +} + +type Output struct { + OutputType string `json:"output_type"` + Data map[string]any `json:"data,omitempty"` + Text any `json:"text,omitempty"` // string or []string + Name string `json:"name,omitempty"` + Traceback any `json:"traceback,omitempty"` // []string + Ename string `json:"ename,omitempty"` + Evalue string `json:"evalue,omitempty"` +} + +// Render renders Jupyter notebook to HTML +func (renderer) Render(ctx *markup.RenderContext, input io.Reader, outputWriter io.Writer) error { + htmlWriter := htmlutil.NewHTMLWriter(outputWriter) + // the size is (should be) checked and/or limited by the caller to avoid OOM + var notebook Notebook + if err := json.NewDecoder(input).Decode(¬ebook); err != nil { + htmlWriter.WriteFormat(`
    Failed to parse notebook JSON: %v
    `, err) + return htmlWriter.Err() + } + + // Check nbformat version + if notebook.Nbformat < 4 { + htmlWriter.WriteFormat( + `
    This notebook uses an older format (nbformat %d). Only nbformat 4+ is supported for rendering. Please upgrade the notebook in Jupyter or view the raw JSON.
    `, + notebook.Nbformat, + ) + return htmlWriter.Err() + } + + // Detect language + language := "python" // default + if metadata, ok := notebook.Metadata["language_info"].(map[string]any); ok { + if name, ok := metadata["name"].(string); ok { + language = name + } + } else if kernelSpec, ok := notebook.Metadata["kernelspec"].(map[string]any); ok { + if lang, ok := kernelSpec["language"].(string); ok { + language = lang + } + } + + // Start rendering + htmlWriter.WriteHTML(`
    `) + + // limiting the cell rendering to 100 cells + cells := notebook.Cells + truncated := false + const maxRenderedCells = 100 + + if len(cells) > maxRenderedCells { + cells = cells[:maxRenderedCells] // Slice down to exactly 100 elements instantly at the pointer layer + truncated = true + } + + for _, cell := range cells { + if err := renderCell(ctx, htmlWriter, cell, language); err != nil { + log.Warn("Failed to render cell: %v", err) // TODO: RENDER-LOG-HANDLING: see other comments + continue + } + } + + if truncated { + htmlWriter.WriteHTML(`
    `) + htmlWriter.WriteHTML(`Output truncated. This notebook contains too many cells to display efficiently.`) + htmlWriter.WriteHTML(`
    `) + } + + htmlWriter.WriteHTML(`
    `) + return htmlWriter.Err() +} + +func renderCellCode(output htmlutil.HTMLWriter, cell Cell, language string) error { + source := joinSource(cell.Source) + var executionCount *int64 + if cell.ExecutionCount != nil { + if count, err := util.ToInt64(cell.ExecutionCount); err == nil { + executionCount = &count + } + } + + output.WriteHTML(`
    `) + { + if executionCount != nil { + output.WriteFormat(`
    In [%d]:
    `, *executionCount) + } else { + output.WriteHTML(`
    In [ ]:
    `) + } + + // Highlight code + lexer := highlight.DetectChromaLexerByFileName("", language) + output.WriteFormat(`
    `, strings.ToLower(language))
    +		output.WriteHTML(highlight.RenderCodeByLexer(lexer, source))
    +		output.WriteHTML("
    ") + } + output.WriteHTML(`
    `) + + // Render outputs + if len(cell.Outputs) > 0 { + hasExecutionResult := false + for _, out := range cell.Outputs { + if out.OutputType == "execute_result" { + hasExecutionResult = true + break + } + } + + output.WriteHTML(`
    `) + { + if hasExecutionResult && executionCount != nil { + output.WriteFormat(`
    Out [%d]:
    `, *executionCount) + } else { + output.WriteHTML(`
    `) + } + + output.WriteHTML(`
    `) + for _, out := range cell.Outputs { + renderCellCodeOutput(output, out) + } + output.WriteHTML(`
    `) + } + output.WriteHTML(`
    `) + } + + return output.Err() +} + +func renderCell(ctx *markup.RenderContext, output htmlutil.HTMLWriter, cell Cell, language string) error { + switch cell.CellType { + case "markdown": + output.WriteHTML(` +
    +
    +
    +
    `) + if err := renderCellMarkdown(ctx, output, joinSource(cell.Source)); err != nil { + return err + } + output.WriteHTML(`
    `) + case "code": + output.WriteHTML(`
    `) + if err := renderCellCode(output, cell, language); err != nil { + return err + } + output.WriteHTML(`
    `) + default: + output.WriteFormat(` +
    +
    +
    Cell:
    +
    [Cell type %s - unsupported, skipped]
    +
    +
    `, cell.CellType) + } + return output.Err() +} + +func renderCellMarkdown(rctx *markup.RenderContext, output htmlutil.HTMLWriter, source string) error { + markdownCtx := markup.NewRenderContext(rctx) + // make sure the markdown render use the same options and helper to generate correct contents (e.g.: links) + markdownCtx.RenderOptions = rctx.RenderOptions + markdownCtx.RenderHelper = rctx.RenderHelper + output.WriteHTML(`
    `) + if err := markdown.Render(markdownCtx, strings.NewReader(source), output.OriginWriter()); err != nil { + return err + } + output.WriteHTML(`
    `) + return output.Err() +} + +func renderCellCodeOutput(output htmlutil.HTMLWriter, out Output) { + if out.Data != nil { + // Iterate through our priority list to find the best matching MIME handler available + for _, h := range dataMimeHandlers() { + if rawPayload, exists := out.Data[h.Mime]; exists { + var stringPayload string + + // Flatten the polymorphic JSON input (string or []any) into a single clean string + switch v := rawPayload.(type) { + case string: + stringPayload = v + case []any: + stringPayload = joinSource(v) + default: + _ = renderCellCodeOutputUnsupported(output, fmt.Sprintf("[Data output - unsupported data type %T for mime type %s]", rawPayload, h.Mime)) + continue + } + + if err := h.Fn(output, stringPayload); err != nil { + // TODO: RENDER-LOG-HANDLING: outputting render's error to sever's log is not a proper approach + // The errors can be: + // * unsupported element (cell, data, etc): it should render the message on the UI to tell users that the content is not supported, or ignore them if they are ignore-able + // * logic error: it should report to server logs + // * network error: io.Writer tries to write to the HTTP connection, so the error can also be a network error, such error should be ignored + log.Error("Jupyter rendering engine failed for MIME type %s: %v", h.Mime, err) + } + + // Return immediately after rendering the top matching priority format + return + } + } + } + + // Stream output + if out.OutputType == "stream" && out.Text != nil { + streamName := util.Iif(out.Name == "stderr", "stderr", "stdout") + output.WriteFormat(`
    %s
    `, streamName, joinSource(out.Text)) + return + } + + // Error output + if out.OutputType == "error" { + traceback := "" + if tb, ok := out.Traceback.([]any); ok { + lines := make([]string, len(tb)) + for i, line := range tb { + lines[i] = fmt.Sprint(line) + } + traceback = strings.Join(lines, "\n") + } + if traceback == "" && out.Ename != "" { + traceback = fmt.Sprintf("%s: %s", out.Ename, out.Evalue) + } + output.WriteFormat(`
    %s
    `, traceback) + return + } + + // Generic text output + if out.Text != nil { + _ = renderCellCodeOutputTextPlain(output, joinSource(out.Text)) + } +} + +func joinSource(source any) string { + switch v := source.(type) { + case nil: + return "" + case string: + return v + case []any: + // the "source slice item" has EOL ("\n"), so just join them together + parts := make([]string, len(v)) + for i, part := range v { + parts[i] = fmt.Sprint(part) + } + return strings.Join(parts, "") + default: + return fmt.Sprint(v) + } +} + +// trimMathDelimiters strips a single pair of surrounding math delimiters ("$$...$$" or "$...$"), +// so the inner expression is handled by the math post-processor. Unlike strings.Trim, it does not +// eat unrelated "$" characters elsewhere in multi-expression content. +func trimMathDelimiters(s string) string { + s = strings.TrimSpace(s) + if t, ok := strings.CutPrefix(s, "$$"); ok { + return strings.TrimSuffix(t, "$$") + } + if t, ok := strings.CutPrefix(s, "$"); ok { + return strings.TrimSuffix(t, "$") + } + return s +} diff --git a/modules/markup/jupyter/jupyter_test.go b/modules/markup/jupyter/jupyter_test.go new file mode 100644 index 00000000000..fd1464d1727 --- /dev/null +++ b/modules/markup/jupyter/jupyter_test.go @@ -0,0 +1,314 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jupyter + +import ( + "fmt" + "strings" + "testing" + + "gitea.dev/modules/markup" + "gitea.dev/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRender(t *testing.T) { + r := renderer{} + + t.Run("Basic notebook", func(t *testing.T) { + input := `{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "source": ["print('hello')"], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": ["hello\n"] + } + ] + } + ], + "metadata": {}, + "nbformat": 4 + }` + + var output strings.Builder + ctx := &markup.RenderContext{} + err := r.Render(ctx, strings.NewReader(input), &output) + + assert.NoError(t, err) + result := output.String() + assert.Contains(t, result, `
    `) + assert.Contains(t, result, `
    `) + assert.Contains(t, result, `In [1]:`) + assert.Contains(t, result, `print`) + assert.Contains(t, result, `hello`) + assert.Contains(t, result, `stream-stdout`) + }) + + t.Run("Markdown cell with XSS Protection", func(t *testing.T) { + input := `{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Title\n", + "Some text\n", + "[click me](javascript:alert(1))\n", + "" + ] + } + ], + "metadata": {}, + "nbformat": 4 + }` + + var output strings.Builder + ctx := markup.NewRenderContext(t.Context()) + err := r.Render(ctx, strings.NewReader(input), &output) + + assert.NoError(t, err) + result := output.String() + + // Assert normal markup still renders correctly + assert.Contains(t, result, `
    `) + assert.Contains(t, result, `Title`) + assert.Contains(t, result, `Some text`) + assert.Contains(t, result, `click me`) + + // CRITICAL SECURITY ASSERTIONS: Ensure XSS vectors are completely stripped + assert.NotContains(t, result, `javascript:alert`) + assert.NotContains(t, result, `
    Safe Content
    " + ] + }, + "metadata": {} + } + ] + } + ] + }` + + var output strings.Builder + ctx := markup.NewRenderContext(t.Context()) + ctx.RenderOptions.MarkupType = "jupyter-render" + err := markup.Render(ctx, strings.NewReader(maliciousNotebook), &output) + assert.NoError(t, err) + const expected = ` +
    +
    +
    +
    In [1]:
    +
    +
    
    +					a=1
    +				
    +
    +
    +
    +
    Out [1]:
    +
    +
    +
    Safe Content
    +
    +
    +
    +
    +
    ` + assert.Equal(t, test.NormalizeHTMLSpaces(expected), test.NormalizeHTMLSpaces(output.String())) +} diff --git a/modules/test/utils.go b/modules/test/utils.go index 5a4e13b4232..12b35f42f7a 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -12,12 +12,16 @@ import ( "net/http" "net/http/httptest" "os" + "regexp" + "slices" "strconv" "strings" "sync" "gitea.dev/modules/json" "gitea.dev/modules/util" + + "golang.org/x/net/html" ) // RedirectURL returns the redirect URL of a http response. @@ -182,3 +186,48 @@ func ExternalServiceHTTP(t TestingT, envVarName, def string) string { } return val } + +var normalizeHTMLSpacesRegexp = sync.OnceValue(func() (ret struct { + afterRt, beforeLt *regexp.Regexp +}, +) { + ret.afterRt = regexp.MustCompile(`>\s*`) + ret.beforeLt = regexp.MustCompile(`\s*<`) + return ret +}) + +func NormalizeHTMLSpaces(s string) string { + vars := normalizeHTMLSpacesRegexp() + s = vars.afterRt.ReplaceAllString(s, ">\n") + s = vars.beforeLt.ReplaceAllString(s, "\n<") + return strings.TrimSpace(s) +} + +func NormalizeHTMLAttributes(t TestingT, s string) string { + nodes, err := html.Parse(strings.NewReader(s)) + if err != nil { + t.Errorf("failed to parse expected HTML: %v", err) + return "" + } + + var normalize func(n *html.Node) + normalize = func(n *html.Node) { + slices.SortFunc(n.Attr, func(a, b html.Attribute) int { + if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 { + return cmp + } + if cmp := strings.Compare(a.Key, b.Key); cmp != 0 { + return cmp + } + return strings.Compare(a.Val, b.Val) + }) + for c := n.FirstChild; c != nil; c = c.NextSibling { + normalize(c) + } + } + var sb strings.Builder + if err = html.Render(&sb, nodes); err != nil { + t.Errorf("failed to render HTML: %v", err) + } + return sb.String() +} diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index cefe5592c4a..fc900e5a3de 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -5,13 +5,12 @@ package integration import ( "io" - "slices" - "strings" "testing" + "gitea.dev/modules/test" + "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" - "golang.org/x/net/html" ) // HTMLDoc struct @@ -53,36 +52,10 @@ func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string func assertHTMLEq(t testing.TB, expected, actual string) { t.Helper() - if expected == actual { + if expected == actual { // fast path return } - exp, err := html.Parse(strings.NewReader(expected)) - if !assert.NoError(t, err) { - return - } - act, err := html.Parse(strings.NewReader(actual)) - if !assert.NoError(t, err) { - return - } - var normalize func(n *html.Node) - normalize = func(n *html.Node) { - slices.SortFunc(n.Attr, func(a, b html.Attribute) int { - if cmp := strings.Compare(a.Namespace, b.Namespace); cmp != 0 { - return cmp - } - if cmp := strings.Compare(a.Key, b.Key); cmp != 0 { - return cmp - } - return strings.Compare(a.Val, b.Val) - }) - for c := n.FirstChild; c != nil; c = c.NextSibling { - normalize(c) - } - } - normalize(exp) - normalize(act) - var expNormalized, actNormalized strings.Builder - assert.NoError(t, html.Render(&expNormalized, exp)) - assert.NoError(t, html.Render(&actNormalized, act)) - assert.Equal(t, expNormalized.String(), actNormalized.String()) + exp := test.NormalizeHTMLAttributes(t, expected) + act := test.NormalizeHTMLAttributes(t, actual) + assert.Equal(t, exp, act) } diff --git a/web_src/css/index.css b/web_src/css/index.css index 2d3e118825d..71e58e7c8ec 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -52,6 +52,7 @@ @import "./markup/content.css"; @import "./markup/codeblock.css"; @import "./markup/codepreview.css"; +@import "./markup/jupyter.css"; @import "./font_i18n.css"; @import "./base.css"; diff --git a/web_src/css/markup/jupyter.css b/web_src/css/markup/jupyter.css new file mode 100644 index 00000000000..ab128e64a77 --- /dev/null +++ b/web_src/css/markup/jupyter.css @@ -0,0 +1,93 @@ +.markup.jupyter-render { + padding: 0; +} + +.markup .jupyter-notebook { + padding: 20px; + background: var(--color-body); + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + font-family: var(--fonts-monospace); + display: flex; + flex-direction: column; + gap: 2em; +} + +/* cell code */ +.markup .jupyter-notebook .cell-line { + display: flex; + width: 100%; + gap: 0.5em; +} + +.markup .jupyter-notebook .cell-left { + width: 100px; + flex-shrink: 0; +} + +.markup .jupyter-notebook .cell-right { + flex: 1; +} + +.markup .jupyter-notebook .cell-prompt { + padding: 10px 0; + color: var(--color-text-light-2); + font-size: 13px; +} + +.markup .jupyter-notebook .cell-left.cell-prompt { + padding-left: 10px; + text-align: right; + white-space: nowrap; + user-select: none; +} + +.markup .jupyter-notebook .cell-right.cell-prompt { + padding-right: 10px; +} + +.markup .jupyter-notebook .cell-input, +.markup .jupyter-notebook .cell-output { + overflow-x: auto; +} + +.markup .jupyter-notebook .cell-input pre, +.markup .jupyter-notebook .cell-output pre { + padding: 10px 16px; + font-size: 13px; + min-height: 40px; + margin: 0; +} + +.markup .jupyter-notebook .cell-input pre { + background-color: var(--color-code-bg); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.markup .jupyter-notebook .cell-output { + display: flex; + flex-direction: column; + gap: 1em; +} + +.markup .jupyter-notebook .cell-type-code { + display: flex; + flex-direction: column; + gap: 1em; +} + +.markup .jupyter-notebook .cell-output-unsupported { + color: var(--color-text-light-2); + font-style: italic; + font-size: 13px; +} + +.markup .jupyter-notebook .cell-output-error { + color: var(--color-red); +} + +/* cell markdown */ +.markup .jupyter-notebook .cell-right .embedded-markdown { + padding: 0 16px; /* match cell code right padding */ +} From c7af379672666bb6d50024e92961ae863a2d3201 Mon Sep 17 00:00:00 2001 From: Pycub Date: Sun, 14 Jun 2026 18:53:48 +0330 Subject: [PATCH 77/88] fix(api): nil pointer panic when filtering tracked times by a non-existent user (#38112) ## Problem `GET /repos/{owner}/{repo}/times` and `GET /repos/{owner}/{repo}/issues/{index}/times` crash with a nil pointer dereference when the `user` query filter names a user that does not exist. ## Root cause In `ListTrackedTimes` and `ListTrackedTimesByRepository`, the `IsErrUserNotExist` branch sends the 404 but is missing a `return`, so execution falls through to `opts.UserID = user.ID` with a nil `user`. --------- Co-authored-by: bircni --- routers/api/v1/repo/issue_tracked_time.go | 2 + .../api_issue_tracked_time_test.go | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 33af841fbd1..ff723e679ff 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -91,6 +91,7 @@ func ListTrackedTimes(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { ctx.APIError(http.StatusNotFound, err.Error()) + return } else if err != nil { ctx.APIErrorInternal(err) return @@ -499,6 +500,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { ctx.APIError(http.StatusNotFound, err.Error()) + return } else if err != nil { ctx.APIErrorInternal(err) return diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go index fcbe4dfa519..0ae8a858ac1 100644 --- a/tests/integration/api_issue_tracked_time_test.go +++ b/tests/integration/api_issue_tracked_time_test.go @@ -13,6 +13,7 @@ import ( issues_model "gitea.dev/models/issues" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/json" api "gitea.dev/modules/structs" "gitea.dev/tests" @@ -61,6 +62,44 @@ func TestAPIGetTrackedTimes(t *testing.T) { assert.Equal(t, int64(6), filterAPITimes[1].ID) } +// TestAPIGetTrackedTimesNonExistentUserFilter ensures filtering by a user that +// does not exist returns a clean 404 instead of panicking (nil pointer dereference). +func TestAPIGetTrackedTimesNonExistentUserFilter(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + assert.NoError(t, issue2.LoadRepo(t.Context())) + + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeReadRepository) + + for _, tc := range []struct { + name string + url string + }{ + {"repository level", fmt.Sprintf("/api/v1/repos/%s/%s/times?user=nonexistentuser", user2.Name, issue2.Repo.Name)}, + {"issue level", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times?user=nonexistentuser", user2.Name, issue2.Repo.Name, issue2.Index)}, + } { + t.Run(tc.name, func(t *testing.T) { + req := NewRequest(t, "GET", tc.url).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusNotFound) + + assert.True(t, json.Valid(resp.Body.Bytes()), "response body must be a single JSON value, got: %s", resp.Body.Bytes()) + + var apiError api.APIError + DecodeJSON(t, resp, &apiError) + assert.Contains(t, apiError.Message, "user does not exist") + }) + } + + t.Run("existing user", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/times?user=%s", user2.Name, issue2.Repo.Name, user2.Name).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, api.TrackedTimeList{}) + }) +} + func TestAPIDeleteTrackedTime(t *testing.T) { defer tests.PrepareTestEnv(t)() From b8ef6a91e64f50589f419816376942981d7e36da Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 14 Jun 2026 18:42:01 +0200 Subject: [PATCH 78/88] docs: Publish TOC Election Result 2026 (#38111) - Adjusted the wording for what happens on a draw (somehow we managed to get a draw) The new members are: - @delvh - @bircni - @TheFox0x7 Closes #37551 --- docs/community-governance.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/community-governance.md b/docs/community-governance.md index 0d9a9835a78..c1d30f7bd18 100644 --- a/docs/community-governance.md +++ b/docs/community-governance.md @@ -185,17 +185,30 @@ As long as seats are empty in the TOC, members of the previous TOC can fill them If an elected member that accepts the seat does not have 2FA configured yet, they will be temporarily counted as `answer pending` until they manage to configure 2FA, thus leaving their seat empty for this duration. +If multiple persons have the same amount of votes, a random draw will be used to determine the order of the candidates with the same amount of votes, and thus who gets the seat first. +The candidates will be placed in the list in an alphabetical insensitive order by their username. +We use this script to determine the order of candidates with the same amount of votes: + +```python +import random +random.seed("Gitea TOC Election") +random.choice([, , ...]) +``` + +The result of this script needs then to be published in the TOC election issue to ensure transparency of the process. + ### Current TOC members -- 2025-01-01 ~ 2026-06-14 +- 2026-06-14 ~ 2026-12-31 - Company - [Jason Song](https://gitea.com/wolfogre) - [Lunny Xiao](https://gitea.com/lunny) - [Matti Ranta](https://gitea.com/techknowlogick) - Community - - [6543](https://gitea.com/6543) <6543@obermui.de> + - [bircni](https://gitea.com/bircni) - [delvh](https://gitea.com/delvh) - - [lafriks](https://gitea.com/lafriks) + - [TheFox0x7](https://gitea.com/TheFox0x7) + ### Previous TOC/owners members @@ -207,9 +220,10 @@ Here's the history of the owners and the time they served: - [Lauris Bukšis-Haberkorns](https://gitea.com/lafriks) - [2018](https://github.com/go-gitea/gitea/issues/3255), [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801) - [Matti Ranta](https://gitea.com/techknowlogick) - [2019](https://github.com/go-gitea/gitea/issues/5572), [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 - [Andrew Thornton](https://gitea.com/zeripath) - [2020](https://github.com/go-gitea/gitea/issues/9230), [2021](https://github.com/go-gitea/gitea/issues/13801), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [6543](https://gitea.com/6543) - 2023 +- [6543](https://gitea.com/6543) - 2023, 2025 - [John Olheiser](https://gitea.com/jolheiser) - 2023, 2024 - [Jason Song](https://gitea.com/wolfogre) - 2023 +- [lafriks](https://gitea.com/lafriks) - 2025 ## Governance Compensation From c6167d1ff58874d823f2ad6b28b338196f9c4eda Mon Sep 17 00:00:00 2001 From: TheFox0x7 Date: Sun, 14 Jun 2026 20:05:18 +0200 Subject: [PATCH 79/88] feat(api): add token introspection and self-deletion endpoint (#37995) Adds a /api/v1/token endpoint that allows tokens to introspect and delete themselves. partially fixes: https://github.com/go-gitea/gitea/issues/33583 Assisted-by: Mistral Vibe:mistral-medium-3.5 --------- Signed-off-by: wxiaoguang Co-authored-by: wxiaoguang --- models/auth/access_token.go | 60 ++------------- models/auth/access_token_test.go | 7 +- modules/structs/token.go | 31 ++++++++ routers/api/v1/api.go | 6 ++ routers/api/v1/swagger/app.go | 7 ++ routers/api/v1/token/token.go | 88 +++++++++++++++++++++ routers/api/v1/user/app.go | 10 +-- services/auth/basic.go | 5 +- services/auth/oauth2.go | 2 +- templates/swagger/v1_json.tmpl | 97 +++++++++++++++++++++++ templates/swagger/v1_openapi3_json.tmpl | 95 +++++++++++++++++++++++ tests/integration/api_token_self_test.go | 98 ++++++++++++++++++++++++ 12 files changed, 437 insertions(+), 69 deletions(-) create mode 100644 modules/structs/token.go create mode 100644 routers/api/v1/token/token.go create mode 100644 tests/integration/api_token_self_test.go diff --git a/models/auth/access_token.go b/models/auth/access_token.go index da451fb044d..63a345dfcdc 100644 --- a/models/auth/access_token.go +++ b/models/auth/access_token.go @@ -20,42 +20,6 @@ import ( "xorm.io/builder" ) -// ErrAccessTokenNotExist represents a "AccessTokenNotExist" kind of error. -type ErrAccessTokenNotExist struct { - Token string -} - -// IsErrAccessTokenNotExist checks if an error is a ErrAccessTokenNotExist. -func IsErrAccessTokenNotExist(err error) bool { - _, ok := err.(ErrAccessTokenNotExist) - return ok -} - -func (err ErrAccessTokenNotExist) Error() string { - return fmt.Sprintf("access token does not exist [sha: %s]", err.Token) -} - -func (err ErrAccessTokenNotExist) Unwrap() error { - return util.ErrNotExist -} - -// ErrAccessTokenEmpty represents a "AccessTokenEmpty" kind of error. -type ErrAccessTokenEmpty struct{} - -// IsErrAccessTokenEmpty checks if an error is a ErrAccessTokenEmpty. -func IsErrAccessTokenEmpty(err error) bool { - _, ok := err.(ErrAccessTokenEmpty) - return ok -} - -func (err ErrAccessTokenEmpty) Error() string { - return "access token is empty" -} - -func (err ErrAccessTokenEmpty) Unwrap() error { - return util.ErrInvalidArgument -} - var successfulAccessTokenCache *lru.Cache[string, any] // AccessToken represents a personal access token. @@ -134,21 +98,11 @@ func getAccessTokenIDFromCache(token string) int64 { // GetAccessTokenBySHA returns access token by given token value func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error) { - if token == "" { - return nil, ErrAccessTokenEmpty{} - } - // A token is defined as being SHA1 sum these are 40 hexadecimal bytes long - if len(token) != 40 { - return nil, ErrAccessTokenNotExist{token} - } - for _, x := range []byte(token) { - if x < '0' || (x > '9' && x < 'a') || x > 'f' { - return nil, ErrAccessTokenNotExist{token} - } + if len(token) < 8 { + return nil, util.NewNotExistErrorf("access token not found") } lastEight := token[len(token)-8:] - if id := getAccessTokenIDFromCache(token); id > 0 { accessToken := &AccessToken{ TokenLastEight: lastEight, @@ -169,7 +123,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error if err != nil { return nil, err } else if len(tokens) == 0 { - return nil, ErrAccessTokenNotExist{token} + return nil, util.NewNotExistErrorf("access token not found") } for _, t := range tokens { @@ -181,7 +135,7 @@ func GetAccessTokenBySHA(ctx context.Context, token string) (*AccessToken, error return &t, nil } } - return nil, ErrAccessTokenNotExist{token} + return nil, util.NewNotExistErrorf("access token not found") } // AccessTokenByNameExists checks if a token name has been used already by a user. @@ -218,13 +172,11 @@ func UpdateAccessToken(ctx context.Context, t *AccessToken) error { // DeleteAccessTokenByID deletes access token by given ID. func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error { - cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{ - UID: userID, - }) + cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{UID: userID}) if err != nil { return err } else if cnt != 1 { - return ErrAccessTokenNotExist{} + return util.NewNotExistErrorf("access token not found") } return nil } diff --git a/models/auth/access_token_test.go b/models/auth/access_token_test.go index 504600cd087..acab8b3ab50 100644 --- a/models/auth/access_token_test.go +++ b/models/auth/access_token_test.go @@ -9,6 +9,7 @@ import ( auth_model "gitea.dev/models/auth" "gitea.dev/models/db" "gitea.dev/models/unittest" + "gitea.dev/modules/util" "github.com/stretchr/testify/assert" ) @@ -76,11 +77,11 @@ func TestGetAccessTokenBySHA(t *testing.T) { _, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash") assert.Error(t, err) - assert.True(t, auth_model.IsErrAccessTokenNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) _, err = auth_model.GetAccessTokenBySHA(t.Context(), "") assert.Error(t, err) - assert.True(t, auth_model.IsErrAccessTokenEmpty(err)) + assert.ErrorIs(t, err, util.ErrNotExist) } func TestListAccessTokens(t *testing.T) { @@ -128,5 +129,5 @@ func TestDeleteAccessTokenByID(t *testing.T) { err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100) assert.Error(t, err) - assert.True(t, auth_model.IsErrAccessTokenNotExist(err)) + assert.ErrorIs(t, err, util.ErrNotExist) } diff --git a/modules/structs/token.go b/modules/structs/token.go new file mode 100644 index 00000000000..af72aca487c --- /dev/null +++ b/modules/structs/token.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import "time" + +// CurrentAccessToken represents the metadata of the currently authenticated token. +// swagger:model CurrentAccessToken +type CurrentAccessToken struct { + // The unique identifier of the access token + ID int64 `json:"id"` + // The name of the access token + Name string `json:"name"` + // The scopes granted to this access token + Scopes []string `json:"scopes"` + // The timestamp when the token was created + CreatedAt time.Time `json:"created_at"` + // The timestamp when the token was last used + LastUsedAt time.Time `json:"last_used_at"` + // The owner of the access token + User *UserMeta `json:"user"` +} + +// UserMeta represents minimal user information for the token owner. +type UserMeta struct { + // The unique identifier of the user + ID int64 `json:"id"` + // The username of the user + Login string `json:"login"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 3bac1eac919..4715ca1d672 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -88,6 +88,7 @@ import ( "gitea.dev/routers/api/v1/packages" "gitea.dev/routers/api/v1/repo" "gitea.dev/routers/api/v1/settings" + "gitea.dev/routers/api/v1/token" "gitea.dev/routers/api/v1/user" "gitea.dev/routers/common" "gitea.dev/services/actions" @@ -976,6 +977,11 @@ func Routes() *web.Router { }) }) + // Token introspection and deletion endpoint + m.Combo("/token"). + Get(reqToken(), token.GetCurrentToken). + Delete(reqToken(), token.DeleteCurrentToken) + // Notifications (requires 'notifications' scope) // The notifications API is not available for public-only tokens because a user's notifications mix // public and private repository events in the same mailbox. diff --git a/routers/api/v1/swagger/app.go b/routers/api/v1/swagger/app.go index dc30cda6996..3097035e456 100644 --- a/routers/api/v1/swagger/app.go +++ b/routers/api/v1/swagger/app.go @@ -20,3 +20,10 @@ type swaggerResponseAccessToken struct { // in:body Body api.AccessToken `json:"body"` } + +// CurrentAccessToken represents the currently authenticated access token. +// swagger:response CurrentAccessToken +type swaggerResponseCurrentAccessToken struct { + // in:body + Body api.CurrentAccessToken `json:"body"` +} diff --git a/routers/api/v1/token/token.go b/routers/api/v1/token/token.go new file mode 100644 index 00000000000..7712a7c8c2e --- /dev/null +++ b/routers/api/v1/token/token.go @@ -0,0 +1,88 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package token + +import ( + "errors" + "net/http" + + auth_model "gitea.dev/models/auth" + user_model "gitea.dev/models/user" + "gitea.dev/modules/auth/httpauth" + api "gitea.dev/modules/structs" + "gitea.dev/modules/util" + "gitea.dev/services/context" +) + +// GetCurrentToken returns metadata about the currently authenticated token. +func GetCurrentToken(ctx *context.APIContext) { + // swagger:operation GET /token miscellaneous getCurrentToken + // --- + // summary: Get the currently authenticated token + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/CurrentAccessToken" + accessToken, err := getToken(ctx) + if err != nil { + ctx.APIErrorAuto(err) + return + } + + // Get user info + user, err := user_model.GetUserByID(ctx, accessToken.UID) + if err != nil { + ctx.APIErrorAuto(err) + return + } + + ctx.JSON(http.StatusOK, &api.CurrentAccessToken{ + ID: accessToken.ID, + Name: accessToken.Name, + Scopes: accessToken.Scope.StringSlice(), + CreatedAt: accessToken.CreatedUnix.AsTime(), + LastUsedAt: accessToken.UpdatedUnix.AsTime(), + User: &api.UserMeta{ + ID: user.ID, + Login: user.Name, + }, + }) +} + +// DeleteCurrentToken deletes the currently authenticated token. +func DeleteCurrentToken(ctx *context.APIContext) { + // swagger:operation DELETE /token miscellaneous deleteCurrentToken + // --- + // summary: Delete the currently authenticated token + // produces: + // - application/json + // responses: + // "204": + // description: token deleted + accessToken, err := getToken(ctx) + if err != nil { + ctx.APIErrorAuto(err) + return + } + + // Delete the token + err = auth_model.DeleteAccessTokenByID(ctx, accessToken.ID, accessToken.UID) + if err != nil && !errors.Is(err, util.ErrNotExist) { + ctx.APIErrorAuto(err) + return + } + ctx.Status(http.StatusNoContent) +} + +// getToken retrieves an access token from the API context's Authorization header and validates it against the database. +// Returns nil if the token is invalid and handles the response +func getToken(ctx *context.APIContext) (*auth_model.AccessToken, error) { + authHeader := ctx.Req.Header.Get("Authorization") + parsed, ok := httpauth.ParseAuthorizationHeader(authHeader) + if !ok || parsed.BearerToken == nil { + return nil, util.NewNotExistErrorf("invalid access token") + } + return auth_model.GetAccessTokenBySHA(ctx, parsed.BearerToken.Token) +} diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index a410909e0e5..87aef1d10de 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -191,17 +191,9 @@ func DeleteAccessToken(ctx *context.APIContext) { return } } - if tokenID == 0 { - ctx.APIErrorInternal(nil) - return - } if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil { - if auth_model.IsErrAccessTokenNotExist(err) { - ctx.APIErrorNotFound() - } else { - ctx.APIErrorInternal(err) - } + ctx.APIErrorAuto(err) return } diff --git a/services/auth/basic.go b/services/auth/basic.go index c7db14e6e7e..ed2a2e1945c 100644 --- a/services/auth/basic.go +++ b/services/auth/basic.go @@ -5,6 +5,7 @@ package auth import ( + "errors" "net/http" actions_model "gitea.dev/models/actions" @@ -104,8 +105,8 @@ func (b *Basic) VerifyAuthToken(req *http.Request, w http.ResponseWriter, store store.GetData()["IsApiToken"] = true store.GetData()["ApiTokenScope"] = token.Scope return u, nil - } else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) { - log.Error("GetAccessTokenBySha: %v", err) + } else if !errors.Is(err, util.ErrNotExist) { + log.Error("GetAccessTokenBySHA: %v", err) } // check task token diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index a2f7d5d1e7f..cb622c22581 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -128,7 +128,7 @@ func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataS } t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA) if err != nil { - if auth_model.IsErrAccessTokenNotExist(err) { + if errors.Is(err, util.ErrNotExist) { // check task token if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil { log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 286bec3a50f..861cb88c2a4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -19202,6 +19202,38 @@ } } }, + "/token": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Get the currently authenticated token", + "operationId": "getCurrentToken", + "responses": { + "200": { + "$ref": "#/responses/CurrentAccessToken" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Delete the currently authenticated token", + "operationId": "deleteCurrentToken", + "responses": { + "204": { + "description": "token deleted" + } + } + } + }, "/topics/search": { "get": { "produces": [ @@ -25116,6 +25148,47 @@ }, "x-go-package": "gitea.dev/modules/structs" }, + "CurrentAccessToken": { + "type": "object", + "title": "CurrentAccessToken represents the metadata of the currently authenticated token.", + "properties": { + "created_at": { + "description": "The timestamp when the token was created", + "type": "string", + "format": "date-time", + "x-go-name": "CreatedAt" + }, + "id": { + "description": "The unique identifier of the access token", + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "last_used_at": { + "description": "The timestamp when the token was last used", + "type": "string", + "format": "date-time", + "x-go-name": "LastUsedAt" + }, + "name": { + "description": "The name of the access token", + "type": "string", + "x-go-name": "Name" + }, + "scopes": { + "description": "The scopes granted to this access token", + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Scopes" + }, + "user": { + "$ref": "#/definitions/UserMeta" + } + }, + "x-go-package": "gitea.dev/modules/structs" + }, "DeleteEmailOption": { "description": "DeleteEmailOption options when deleting email addresses", "type": "object", @@ -30585,6 +30658,24 @@ }, "x-go-package": "gitea.dev/models/activities" }, + "UserMeta": { + "type": "object", + "title": "UserMeta represents minimal user information for the token owner.", + "properties": { + "id": { + "description": "The unique identifier of the user", + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "login": { + "description": "The username of the user", + "type": "string", + "x-go-name": "Login" + } + }, + "x-go-package": "gitea.dev/modules/structs" + }, "UserSettings": { "description": "UserSettings represents user settings", "type": "object", @@ -31089,6 +31180,12 @@ } } }, + "CurrentAccessToken": { + "description": "CurrentAccessToken represents the currently authenticated access token.", + "schema": { + "$ref": "#/definitions/CurrentAccessToken" + } + }, "DeployKey": { "description": "DeployKey", "schema": { diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl index 6fd253528e9..782e9bce426 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1_openapi3_json.tmpl @@ -399,6 +399,16 @@ }, "description": "CronList" }, + "CurrentAccessToken": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentAccessToken" + } + } + }, + "description": "CurrentAccessToken represents the currently authenticated access token." + }, "DeployKey": { "content": { "application/json": { @@ -4952,6 +4962,47 @@ "type": "object", "x-go-package": "gitea.dev/modules/structs" }, + "CurrentAccessToken": { + "properties": { + "created_at": { + "description": "The timestamp when the token was created", + "format": "date-time", + "type": "string", + "x-go-name": "CreatedAt" + }, + "id": { + "description": "The unique identifier of the access token", + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "last_used_at": { + "description": "The timestamp when the token was last used", + "format": "date-time", + "type": "string", + "x-go-name": "LastUsedAt" + }, + "name": { + "description": "The name of the access token", + "type": "string", + "x-go-name": "Name" + }, + "scopes": { + "description": "The scopes granted to this access token", + "items": { + "type": "string" + }, + "type": "array", + "x-go-name": "Scopes" + }, + "user": { + "$ref": "#/components/schemas/UserMeta" + } + }, + "title": "CurrentAccessToken represents the metadata of the currently authenticated token.", + "type": "object", + "x-go-package": "gitea.dev/modules/structs" + }, "DeleteEmailOption": { "description": "DeleteEmailOption options when deleting email addresses", "properties": { @@ -10454,6 +10505,24 @@ "type": "object", "x-go-package": "gitea.dev/models/activities" }, + "UserMeta": { + "properties": { + "id": { + "description": "The unique identifier of the user", + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "login": { + "description": "The username of the user", + "type": "string", + "x-go-name": "Login" + } + }, + "title": "UserMeta represents minimal user information for the token owner.", + "type": "object", + "x-go-package": "gitea.dev/modules/structs" + }, "UserSettings": { "description": "UserSettings represents user settings", "properties": { @@ -31385,6 +31454,32 @@ ] } }, + "/token": { + "delete": { + "operationId": "deleteCurrentToken", + "responses": { + "204": { + "description": "token deleted" + } + }, + "summary": "Delete the currently authenticated token", + "tags": [ + "miscellaneous" + ] + }, + "get": { + "operationId": "getCurrentToken", + "responses": { + "200": { + "$ref": "#/components/responses/CurrentAccessToken" + } + }, + "summary": "Get the currently authenticated token", + "tags": [ + "miscellaneous" + ] + } + }, "/topics/search": { "get": { "operationId": "topicSearch", diff --git a/tests/integration/api_token_self_test.go b/tests/integration/api_token_self_test.go new file mode 100644 index 00000000000..2720c59d2c5 --- /dev/null +++ b/tests/integration/api_token_self_test.go @@ -0,0 +1,98 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + auth_model "gitea.dev/models/auth" + "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + api "gitea.dev/modules/structs" + "gitea.dev/tests" + + "github.com/stretchr/testify/assert" +) + +// TestAPIGetCurrentToken tests getting metadata of the currently authenticated token +func TestAPIGetCurrentToken(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Success with all scopes", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-all", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) + + req := NewRequest(t, "GET", "/api/v1/token"). + AddTokenAuth(accessToken.Token) + resp := MakeRequest(t, req, http.StatusOK) + + currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{}) + assert.Equal(t, accessToken.ID, currentToken.ID) + assert.Equal(t, accessToken.Name, currentToken.Name) + assert.Equal(t, user.ID, currentToken.User.ID) + assert.Equal(t, user.Name, currentToken.User.Login) + }) + + t.Run("Success with limited scopes", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-get-current-token-limited", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeReadRepository}) + + req := NewRequest(t, "GET", "/api/v1/token"). + AddTokenAuth(accessToken.Token) + resp := MakeRequest(t, req, http.StatusOK) + + currentToken := DecodeJSON(t, resp, &api.CurrentAccessToken{}) + assert.Equal(t, accessToken.ID, currentToken.ID) + assert.Equal(t, accessToken.Name, currentToken.Name) + assert.Equal(t, user.ID, currentToken.User.ID) + assert.Equal(t, user.Name, currentToken.User.Login) + }) + + t.Run("Bad token", func(t *testing.T) { + req := NewRequest(t, "GET", "/api/v1/token"). + AddTokenAuth("this does not exist") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "GET", "/api/v1/token") + MakeRequest(t, req, http.StatusUnauthorized) + }) +} + +// TestAPITokenSelfService tests delete operations on token +func TestAPITokenSelfService(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("Success then verify deleted", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-delete-current-token", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll}) + + // Delete the token via the endpoint + req := NewRequest(t, "DELETE", "/api/v1/token"). + AddTokenAuth(accessToken.Token) + MakeRequest(t, req, http.StatusNoContent) + + // Verify the token is deleted + unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: accessToken.ID}) + + // Verify the token can no longer be used for GET + req = NewRequest(t, "GET", "/api/v1/token"). + AddTokenAuth(accessToken.Token) + MakeRequest(t, req, http.StatusUnauthorized) + + // Verify the token can no longer be used for DELETE + req = NewRequest(t, "DELETE", "/api/v1/token"). + AddTokenAuth(accessToken.Token) + MakeRequest(t, req, http.StatusUnauthorized) + }) + + t.Run("Bad token", func(t *testing.T) { + req := NewRequest(t, "DELETE", "/api/v1/token"). + AddTokenAuth("this does not exist") + MakeRequest(t, req, http.StatusUnauthorized) + + req = NewRequest(t, "DELETE", "/api/v1/token") + MakeRequest(t, req, http.StatusUnauthorized) + }) +} From 3417bc89794fdd19b7531d9ed38a68b93539b663 Mon Sep 17 00:00:00 2001 From: delvh Date: Sun, 14 Jun 2026 20:06:41 +0200 Subject: [PATCH 80/88] docs: Clarify criteria for becoming a merger (#38113) --- docs/community-governance.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/community-governance.md b/docs/community-governance.md index c1d30f7bd18..90ba435628c 100644 --- a/docs/community-governance.md +++ b/docs/community-governance.md @@ -164,7 +164,12 @@ Mergers are the maintainers who carry out the final merge of approved PRs. Their #### Becoming a merger -A merger should already be a Gitea maintainer. To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel. Mergers teams may also invite contributors. +A merger must already be a Gitea maintainer. +To apply, use the [Discord](https://discord.gg/Gitea) `#maintainers` channel. +The minimum requirement for applications to become a merger is to have participated actively in the community for at least four months before applying. +Ultimately, regardless of previous participation, you can only become a merger if the TOC votes in your favor. + +You may also be invited by the TOC to become a merger. ### Technical Oversight Committee (TOC) From 47d48eb208ad7573f570a0d12d18f1468ae051a4 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 15 Jun 2026 02:26:22 +0800 Subject: [PATCH 81/88] chore: fix form string abuse (#38106) --- modules/sitemap/sitemap.go | 12 ++++++++---- modules/sitemap/sitemap_test.go | 8 ++++---- routers/web/admin/orgs.go | 6 ++---- routers/web/admin/users.go | 7 ++----- routers/web/explore/org.go | 9 +++------ routers/web/explore/user.go | 15 ++++----------- routers/web/user/setting/security/openid_test.go | 10 ++-------- services/context/base_form.go | 5 ----- 8 files changed, 25 insertions(+), 47 deletions(-) diff --git a/modules/sitemap/sitemap.go b/modules/sitemap/sitemap.go index 280ca1d7100..5ac37033d92 100644 --- a/modules/sitemap/sitemap.go +++ b/modules/sitemap/sitemap.go @@ -63,20 +63,24 @@ func (s *Sitemap) Add(u URL) { // WriteTo writes the sitemap to a response func (s *Sitemap) WriteTo(w io.Writer) (int64, error) { if l := len(s.URLs); l > urlsLimit { - return 0, fmt.Errorf("The sitemap contains %d URLs, but only %d are allowed", l, urlsLimit) + return 0, fmt.Errorf("sitemap contains %d URLs, but only %d are allowed", l, urlsLimit) } if l := len(s.Sitemaps); l > urlsLimit { - return 0, fmt.Errorf("The sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit) + return 0, fmt.Errorf("sitemap contains %d sub-sitemaps, but only %d are allowed", l, urlsLimit) } buf := bytes.NewBufferString(xml.Header) - if err := xml.NewEncoder(buf).Encode(s); err != nil { + encoder := xml.NewEncoder(buf) + defer encoder.Close() + if err := encoder.Encode(s); err != nil { return 0, err } + _ = encoder.Flush() if err := buf.WriteByte('\n'); err != nil { return 0, err } + // FIXME: such limit is not right, the content has been written, it would have already caused OOM if buf.Len() > sitemapFileLimit { - return 0, fmt.Errorf("The sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit) + return 0, fmt.Errorf("sitemap has %d bytes, but only %d are allowed", buf.Len(), sitemapFileLimit) } return buf.WriteTo(w) } diff --git a/modules/sitemap/sitemap_test.go b/modules/sitemap/sitemap_test.go index 1180463cd79..9ff97939014 100644 --- a/modules/sitemap/sitemap_test.go +++ b/modules/sitemap/sitemap_test.go @@ -61,14 +61,14 @@ func TestNewSitemap(t *testing.T) { { name: "too many urls", urls: make([]URL, 50001), - wantErr: "The sitemap contains 50001 URLs, but only 50000 are allowed", + wantErr: "sitemap contains 50001 URLs, but only 50000 are allowed", }, { name: "too big file", urls: []URL{ {URL: strings.Repeat("b", 50*1024*1024+1)}, }, - wantErr: "The sitemap has 52428932 bytes, but only 52428800 are allowed", + wantErr: "sitemap has 52428932 bytes, but only 52428800 are allowed", }, } for _, tt := range tests { @@ -137,14 +137,14 @@ func TestNewSitemapIndex(t *testing.T) { { name: "too many sitemaps", urls: make([]URL, 50001), - wantErr: "The sitemap contains 50001 sub-sitemaps, but only 50000 are allowed", + wantErr: "sitemap contains 50001 sub-sitemaps, but only 50000 are allowed", }, { name: "too big file", urls: []URL{ {URL: strings.Repeat("b", 50*1024*1024+1)}, }, - wantErr: "The sitemap has 52428952 bytes, but only 52428800 are allowed", + wantErr: "sitemap has 52428952 bytes, but only 52428800 are allowed", }, } for _, tt := range tests { diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index 1474038c915..02037af89fa 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -23,10 +23,7 @@ func Organizations(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.organizations") ctx.Data["PageIsAdminOrganizations"] = true - if ctx.FormString("sort") == "" { - ctx.SetFormString("sort", UserSearchDefaultAdminSort) - } - + sortOrder := ctx.FormString("sort", UserSearchDefaultAdminSort) explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Types: []user_model.UserType{user_model.UserTypeOrganization}, @@ -35,5 +32,6 @@ func Organizations(ctx *context.Context) { PageSize: setting.UI.Admin.OrgPagingNum, }, Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}, + OrderBy: db.SearchOrderBy(sortOrder), }, tplOrgs) } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 4b345945089..f918c8b5d31 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -55,11 +55,7 @@ func Users(ctx *context.Context) { statusFilterMap[filterKey] = paramVal } - sortType := ctx.FormString("sort") - if sortType == "" { - sortType = UserSearchDefaultAdminSort - ctx.SetFormString("sort", sortType) - } + sortType := ctx.FormString("sort", UserSearchDefaultAdminSort) ctx.PageData["adminUserListSearchForm"] = map[string]any{ "StatusFilterMap": statusFilterMap, "SortType": sortType, @@ -78,6 +74,7 @@ func Users(ctx *context.Context) { IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]), IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]), IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones + OrderBy: db.SearchOrderBy(sortType), }, tplUsers) } diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index 621f6bd97a5..687d83ff36c 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -38,17 +38,14 @@ func Organizations(ctx *context.Context) { "alphabetically", "reversealphabetically", ) - sortOrder := ctx.FormString("sort") - if sortOrder == "" { - sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") - ctx.SetFormString("sort", sortOrder) - } - + sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") + sortOrder := ctx.FormString("sort", sortOrderDefault) RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Types: []user_model.UserType{user_model.UserTypeOrganization}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, Visible: visibleTypes, + OrderBy: db.SearchOrderBy(sortOrder), SupportedSortOrders: supportedSortOrders, }, tplExploreUsers) diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 00217d662c1..64e2a92d685 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -55,11 +55,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t ) // we can not set orderBy to `models.SearchOrderByXxx`, because there may be a JOIN in the statement, different tables may have the same name columns - - sortOrder := ctx.FormString("sort") - if sortOrder == "" { - sortOrder = setting.UI.ExploreDefaultSort - } + sortOrder := util.IfZero(string(opts.OrderBy), ctx.FormString("sort", setting.UI.ExploreDefaultSort)) ctx.Data["SortType"] = sortOrder switch sortOrder { @@ -145,18 +141,15 @@ func Users(ctx *context.Context) { "alphabetically", "reversealphabetically", ) - sortOrder := ctx.FormString("sort") - if sortOrder == "" { - sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") - ctx.SetFormString("sort", sortOrder) - } - + sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest") + sortOrder := ctx.FormString("sort", sortOrderDefault) RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Types: []user_model.UserType{user_model.UserTypeIndividual}, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, IsActive: optional.Some(true), Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}, + OrderBy: db.SearchOrderBy(sortOrder), SupportedSortOrders: supportedSortOrders, }, tplExploreUsers) diff --git a/routers/web/user/setting/security/openid_test.go b/routers/web/user/setting/security/openid_test.go index 046e7357e1b..f00506effaf 100644 --- a/routers/web/user/setting/security/openid_test.go +++ b/routers/web/user/setting/security/openid_test.go @@ -15,22 +15,16 @@ import ( func TestDeleteOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "POST /user/settings/security") + ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1") contexttest.LoadUser(t, ctx, 2) - ctx.SetFormString("id", "1") - DeleteOpenID(ctx) - assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus()) } func TestToggleOpenIDVisibilityReturnsNotFoundForOtherUsersAddress(t *testing.T) { unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "POST /user/settings/security") + ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1") contexttest.LoadUser(t, ctx, 2) - ctx.SetFormString("id", "1") - ToggleOpenIDVisibility(ctx) - assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus()) } diff --git a/services/context/base_form.go b/services/context/base_form.go index 088888b461e..c6a70991297 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -78,8 +78,3 @@ func (b *Base) FormOptionalBool(key string) optional.Option[bool] { v = v || strings.EqualFold(s, "on") return optional.Some(v) } - -func (b *Base) SetFormString(key, value string) { - _ = b.Req.FormValue(key) // force parse form - b.Req.Form.Set(key, value) -} From 80ca22a9efcd5a28d9ee046b02c92d1e7f22d577 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 14 Jun 2026 14:48:14 -0400 Subject: [PATCH 82/88] chore(deps): bump dockerfile to use Alpine 3.24 (#38077) --- Dockerfile | 6 +++--- Dockerfile.rootless | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 383e761330e..57cc12dfe97 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem -FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ @@ -9,7 +9,7 @@ COPY --exclude=.git/ . . RUN make frontend # Build backend for each target platform -FROM docker.io/library/golang:1.26-alpine3.23 AS build-env +FROM docker.io/library/golang:1.26-alpine3.24 AS build-env ARG GITEA_VERSION ARG TAGS="" @@ -44,7 +44,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /tmp/local/etc/s6/.s6-svscan/* \ /go/src/gitea.dev/gitea -FROM docker.io/library/alpine:3.23 AS gitea +FROM docker.io/library/alpine:3.24 AS gitea EXPOSE 22 3000 diff --git a/Dockerfile.rootless b/Dockerfile.rootless index af1ef336ae4..4be5a4f8b35 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem -FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.24 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ @@ -9,7 +9,7 @@ COPY --exclude=.git/ . . RUN make frontend # Build backend for each target platform -FROM docker.io/library/golang:1.26-alpine3.23 AS build-env +FROM docker.io/library/golang:1.26-alpine3.24 AS build-env ARG GITEA_VERSION ARG TAGS="" @@ -39,7 +39,7 @@ COPY docker/rootless /tmp/local RUN chmod 755 /tmp/local/usr/local/bin/* \ /go/src/gitea.dev/gitea -FROM docker.io/library/alpine:3.23 AS gitea-rootless +FROM docker.io/library/alpine:3.24 AS gitea-rootless EXPOSE 2222 3000 From 55250407dd0b6bb03e6111aefb1bb2ab08273bd1 Mon Sep 17 00:00:00 2001 From: bircni Date: Sun, 14 Jun 2026 21:07:25 +0200 Subject: [PATCH 83/88] feat(org): add team visibility so org members can discover teams (#37680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37670. Today, org members in Gitea only see teams they're a member of. In larger orgs that hurts onboarding and discoverability — there's no way to look up which team owns what without asking around. GitHub solves this with a per-team visibility setting; this PR brings the same model to Gitea. ## What changes - Every team gets a `visibility` setting: - `private` *(default)* — only team members and org owners can see the team. Same as today's behavior. - `limited` — listable by any member of the organization. Members and the repos the team has access to are visible too. Non-org-members still see nothing. - `public` — listable by any signed-in user. - The Owners team visibility is fixed and cannot be changed via settings. - Existing teams default to `private`, so this is a no-op for anyone who doesn't change anything. ## API - `Team`, `CreateTeamOption`, `EditTeamOption` all gain a `visibility` field (string enum: `private` | `limited` | `public`). - `GET /orgs/{org}/teams` and `/orgs/{org}/teams/search` now apply the same visibility rules as the web UI: - site admins and org owners still see every team - other org members see their own teams plus any `limited` or `public` team - `private` teams are no longer leaked through these endpoints - Swagger/OpenAPI specs regenerated. ## UI View from admin2 (not an owner): View from admin (owner): --------- Signed-off-by: bircni Co-authored-by: TheFox0x7 Co-authored-by: wxiaoguang --- build/generate-openapi.go | 4 +- build/openapi3gen/convert.go | 141 +++++++++++++++++++++--- build/openapi3gen/convert_test.go | 58 +++++++++- build/openapi3gen/enumscan.go | 26 +++-- build/openapi3gen/enumscan_test.go | 32 ++++-- models/db/engine.go | 19 ++-- models/db/list.go | 5 +- models/issues/pull_list.go | 2 +- models/migrations/migrations.go | 1 + models/migrations/v1_27/v337.go | 36 ++++++ models/organization/org.go | 1 + models/organization/team.go | 34 +++++- models/organization/team_list.go | 61 ++++++++-- models/organization/team_test.go | 85 ++++++++++++++ models/repo/repo_list.go | 3 +- modules/structs/org_team.go | 24 ++++ options/locale/locale_en-US.json | 8 ++ routers/api/v1/api.go | 119 ++++++++++++++------ routers/api/v1/org/team.go | 47 +++++--- routers/web/org/home.go | 21 +++- routers/web/org/teams.go | 18 ++- services/context/org.go | 48 +++++--- services/convert/convert.go | 1 + services/forms/org.go | 1 + services/org/team.go | 2 +- templates/org/team/new.tmpl | 48 +++++++- templates/org/team/sidebar.tmpl | 13 ++- templates/org/team/teams.tmpl | 11 +- templates/swagger/v1_json.tmpl | 33 ++++++ templates/swagger/v1_openapi3_json.tmpl | 32 ++++++ tests/integration/api_team_test.go | 60 ++++++++++ 31 files changed, 850 insertions(+), 144 deletions(-) create mode 100644 models/migrations/v1_27/v337.go diff --git a/build/generate-openapi.go b/build/generate-openapi.go index 3c65781481d..f0cdd866345 100644 --- a/build/generate-openapi.go +++ b/build/generate-openapi.go @@ -57,8 +57,8 @@ func main() { log.Fatalf("scanning swagger:enum annotations: %v", err) } names := make([]string, 0, len(astEnumMap)) - for _, n := range astEnumMap { - names = append(names, n) + for _, ns := range astEnumMap { + names = append(names, ns...) } sort.Strings(names) fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", ")) diff --git a/build/openapi3gen/convert.go b/build/openapi3gen/convert.go index 9d446ff45b8..d1ee1a3a68f 100644 --- a/build/openapi3gen/convert.go +++ b/build/openapi3gen/convert.go @@ -6,6 +6,7 @@ package openapi3gen import ( "fmt" "regexp" + "sort" "strings" "gitea.dev/modules/json" @@ -25,10 +26,12 @@ var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`) // Gitea-specific post-processing: file-schema fixups, URI formats, // deprecated flags, and shared-enum extraction. // -// astEnumMap is a value-set-key → Go-type-name map (built by -// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the -// map, Convert returns an error — no fallback naming. -func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) { +// astEnumMap is a value-set-key → Go-type-name(s) map (built by +// ScanSwaggerEnumTypes). When a value set is shared by multiple Go types, +// per-property disambiguation uses the x-go-enum-desc extension. If a shared +// enum in the spec has no matching entry, Convert returns an error — no +// fallback naming. +func Convert(swaggerJSON []byte, astEnumMap map[string][]string) (*openapi3.T, error) { var swagger2 openapi2.T if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil { return nil, fmt.Errorf("parsing swagger 2.0: %w", err) @@ -176,12 +179,24 @@ type enumUsage struct { // If the derived enum name collides with an existing component schema, or // no // swagger:enum annotation matches the value set, generation aborts // with an actionable error — there are no silent fallbacks. -func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error { +func extractSharedEnums(doc *openapi3.T, astEnumMap map[string][]string) error { if doc.Components == nil { return nil } - enumGroups := map[string][]enumUsage{} + type groupKey struct { + valueSet string + typeName string + } + enumGroups := map[groupKey][]enumUsage{} + groupOrder := []groupKey{} // deterministic iteration + + addUsage := func(key groupKey, u enumUsage) { + if _, seen := enumGroups[key]; !seen { + groupOrder = append(groupOrder, key) + } + enumGroups[key] = append(enumGroups[key], u) + } for schemaName, schemaRef := range doc.Components.Schemas { if schemaRef.Value == nil { @@ -192,24 +207,31 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error { continue } if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") { - key := EnumKey(propRef.Value.Enum) - enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false}) + key := groupKey{ + valueSet: EnumKey(propRef.Value.Enum), + typeName: extractEnumTypeName(propRef.Value, astEnumMap), + } + addUsage(key, enumUsage{schemaName, propName, propRef, false}) } if propRef.Value.Type.Is("array") && propRef.Value.Items != nil && propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" && len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") { - key := EnumKey(propRef.Value.Items.Value.Enum) - enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true}) + key := groupKey{ + valueSet: EnumKey(propRef.Value.Items.Value.Enum), + typeName: extractEnumTypeName(propRef.Value.Items.Value, astEnumMap), + } + addUsage(key, enumUsage{schemaName, propName, propRef, true}) } } } - for key, usages := range enumGroups { + for _, key := range groupOrder { + usages := enumGroups[key] if len(usages) < 2 { continue } - enumName, err := deriveEnumName(key, usages, astEnumMap) + enumName, err := deriveEnumName(key.valueSet, key.typeName, usages, astEnumMap) if err != nil { return err } @@ -257,12 +279,17 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error { return nil } -// deriveEnumName looks up a shared enum's Go type name from astEnumMap by -// value-set key. If no annotation matches, returns an error identifying the -// offending properties and the fix. -func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) { - if name, ok := astEnumMap[key]; ok { - return name, nil +// deriveEnumName looks up a shared enum's Go type name. If typeName is +// non-empty (because we recovered it from x-go-enum-desc), it is used +// directly. Otherwise the value-set must map to exactly one known type. On +// failure, returns an error identifying the offending properties. +func deriveEnumName(key, typeName string, usages []enumUsage, astEnumMap map[string][]string) (string, error) { + if typeName != "" { + return typeName, nil + } + names := astEnumMap[key] + if len(names) == 1 { + return names[0], nil } props := map[string]bool{} @@ -273,9 +300,87 @@ func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string for p := range props { propList = append(propList, p) } + if len(names) > 1 { + return "", fmt.Errorf( + "value-set %q is shared by multiple swagger:enum types %v and could not be disambiguated for properties: %v; "+ + "ensure go-swagger emits x-go-enum-desc for those properties", + key, names, propList, + ) + } return "", fmt.Errorf( "no swagger:enum annotation matches value-set %q used by %d properties: %v; "+ "fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus", key, len(usages), propList, ) } + +// extractEnumTypeName recovers the Go type name a schema's enum came from by +// parsing the property's x-go-enum-desc extension. go-swagger emits one line +// per value as " [ ]"; the type is the longest +// common prefix of the const names, narrowed to the candidate set in +// astEnumMap. Returns "" if extraction is inconclusive. +func extractEnumTypeName(s *openapi3.Schema, astEnumMap map[string][]string) string { + if s == nil || s.Extensions == nil { + return "" + } + raw, ok := s.Extensions["x-go-enum-desc"] + if !ok { + return "" + } + desc, ok := raw.(string) + if !ok { + return "" + } + candidates := astEnumMap[EnumKey(s.Enum)] + if len(candidates) == 0 { + return "" + } + // Collect the const names (second whitespace-separated field per line). + var consts []string + for line := range strings.SplitSeq(desc, "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 { + consts = append(consts, fields[1]) + } + } + if len(consts) == 0 { + return "" + } + // A candidate matches when it is a prefix of every const name AND the + // first character after the prefix is an uppercase ASCII letter — this + // rejects e.g. "Alpha" matching "Alphabet" (suffix "bet" starts lower) + // while still accepting both "Alpha" and "AlphaPlus" against "AlphaPlusX" + // (both prefixes valid). The most specific (longest) wins; ties return + // "" so deriveEnumName surfaces the ambiguity rather than silently + // picking a winner. + ordered := append([]string(nil), candidates...) + sort.Slice(ordered, func(i, j int) bool { return len(ordered[i]) > len(ordered[j]) }) + var matches []string + for _, name := range ordered { + ok := true + for _, c := range consts { + if !strings.HasPrefix(c, name) { + ok = false + break + } + suffix := c[len(name):] + // Empty suffix means the const name exactly equals the type name — valid exact match. + // A non-empty suffix must begin with an uppercase letter to reject incidental + // prefix matches (e.g. "Alpha" should not match "Alphabet"). + if len(suffix) > 0 && (suffix[0] < 'A' || suffix[0] > 'Z') { + ok = false + break + } + } + if ok { + matches = append(matches, name) + } + } + if len(matches) == 0 { + return "" + } + if len(matches) > 1 && len(matches[0]) == len(matches[1]) { + return "" + } + return matches[0] +} diff --git a/build/openapi3gen/convert_test.go b/build/openapi3gen/convert_test.go index a9a715e6c2a..1cf73bd1f0e 100644 --- a/build/openapi3gen/convert_test.go +++ b/build/openapi3gen/convert_test.go @@ -12,9 +12,9 @@ import ( func TestDeriveEnumName_hit(t *testing.T) { key := EnumKey([]any{"red", "green", "blue"}) - astMap := map[string]string{key: "Color"} + astMap := map[string][]string{key: {"Color"}} usages := []enumUsage{{schemaName: "Paint", propName: "color"}} - got, err := deriveEnumName(key, usages, astMap) + got, err := deriveEnumName(key, "", usages, astMap) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -26,7 +26,7 @@ func TestDeriveEnumName_hit(t *testing.T) { func TestDeriveEnumName_miss(t *testing.T) { key := EnumKey([]any{"x", "y"}) usages := []enumUsage{{schemaName: "Thing", propName: "kind"}} - _, err := deriveEnumName(key, usages, map[string]string{}) + _, err := deriveEnumName(key, "", usages, map[string][]string{}) if err == nil { t.Fatal("expected miss error, got nil") } @@ -64,7 +64,7 @@ func TestExtractSharedEnums_usesASTMap(t *testing.T) { }, }, } - astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"} + astMap := map[string][]string{EnumKey([]any{"red", "green", "blue"}): {"Color"}} if err := extractSharedEnums(doc, astMap); err != nil { t.Fatalf("extractSharedEnums: %v", err) } @@ -139,6 +139,54 @@ func TestFixFileSchemas_recursesIntoNested(t *testing.T) { } } +func TestExtractEnumTypeName_TeamVisibility(t *testing.T) { + enum := []any{"public", "limited", "private"} + key := EnumKey(enum) + astMap := map[string][]string{key: {"UserVisibility", "TeamVisibility"}} + schema := &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: enum, + Extensions: map[string]any{ + "x-go-enum-desc": "public TeamVisibilityPublic\nlimited TeamVisibilityLimited\nprivate TeamVisibilityPrivate", + }, + } + if got := extractEnumTypeName(schema, astMap); got != "TeamVisibility" { + t.Fatalf("got %q, want %q", got, "TeamVisibility") + } +} + +func TestExtractEnumTypeName_ambiguousPrefixTie(t *testing.T) { + enum := []any{"one", "two"} + key := EnumKey(enum) + astMap := map[string][]string{key: {"AB", "AC"}} + schema := &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: enum, + Extensions: map[string]any{ + "x-go-enum-desc": "one ABOne\ntwo ACTwo", + }, + } + if got := extractEnumTypeName(schema, astMap); got != "" { + t.Fatalf("got %q, want empty string for ambiguous tie", got) + } +} + +func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) { + enum := []any{"a", "b"} + key := EnumKey(enum) + astMap := map[string][]string{key: {"Alpha", "Alphabet"}} + schema := &openapi3.Schema{ + Type: &openapi3.Types{"string"}, + Enum: enum, + Extensions: map[string]any{ + "x-go-enum-desc": "a AlphabetA\nb AlphabetB", + }, + } + if got := extractEnumTypeName(schema, astMap); got != "Alphabet" { + t.Fatalf("got %q, want %q", got, "Alphabet") + } +} + func TestExtractSharedEnums_missReturnsError(t *testing.T) { doc := &openapi3.T{ Components: &openapi3.Components{ @@ -164,7 +212,7 @@ func TestExtractSharedEnums_missReturnsError(t *testing.T) { }, }, } - if err := extractSharedEnums(doc, map[string]string{}); err == nil { + if err := extractSharedEnums(doc, map[string][]string{}); err == nil { t.Fatal("expected miss error") } } diff --git a/build/openapi3gen/enumscan.go b/build/openapi3gen/enumscan.go index dd116205493..73bbcb5e150 100644 --- a/build/openapi3gen/enumscan.go +++ b/build/openapi3gen/enumscan.go @@ -34,13 +34,16 @@ func EnumKey(values []any) string { var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`) // ScanSwaggerEnumTypes walks .go files under each dir and returns a map from -// a canonical value-set key (see EnumKey) to the Go type name declared with -// // swagger:enum TypeName. +// a canonical value-set key (see EnumKey) to the Go type names declared with +// // swagger:enum TypeName. Multiple type names per key are allowed (e.g. +// distinct enum types that happen to share a value set such as +// {public, limited, private}); callers must disambiguate per-usage (typically +// by parsing the property's x-go-enum-desc extension to recover the const +// type prefix). // -// Returns an error on parse failure, on an annotation for a type whose -// constants can't be extracted, or on value-set collisions between two -// different enum types. -func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) { +// Returns an error on parse failure or on an annotation for a type whose +// constants can't be extracted. +func ScanSwaggerEnumTypes(dirs []string) (map[string][]string, error) { fset := token.NewFileSet() parsed := []*ast.File{} @@ -92,17 +95,18 @@ func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) { } } - result := map[string]string{} + result := map[string][]string{} for typeName := range enumTypes { values, ok := enumValues[typeName] if !ok || len(values) == 0 { return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName) } key := EnumKey(values) - if existing, ok := result[key]; ok && existing != typeName { - return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key) - } - result[key] = typeName + result[key] = append(result[key], typeName) + } + for key, names := range result { + sort.Strings(names) + result[key] = names } return result, nil } diff --git a/build/openapi3gen/enumscan_test.go b/build/openapi3gen/enumscan_test.go index 2e5fe99db0f..8cb16b721d1 100644 --- a/build/openapi3gen/enumscan_test.go +++ b/build/openapi3gen/enumscan_test.go @@ -6,10 +6,19 @@ package openapi3gen import ( "os" "path/filepath" + "slices" "strings" "testing" ) +func single(got map[string][]string, key string) string { + v := got[key] + if len(v) != 1 { + return "" + } + return v[0] +} + func TestEnumKey_sortsAndJoins(t *testing.T) { key := EnumKey([]any{"b", "a", "c"}) if key != "a|b|c" { @@ -47,7 +56,7 @@ const ( t.Fatalf("ScanSwaggerEnumTypes: %v", err) } wantKey := EnumKey([]any{"red", "green", "blue"}) - if got[wantKey] != "Color" { + if single(got, wantKey) != "Color" { t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color") } } @@ -98,13 +107,14 @@ const ( t.Fatal(err) } - _, err := ScanSwaggerEnumTypes([]string{dir}) - if err == nil { - t.Fatal("expected collision error, got nil") + got, err := ScanSwaggerEnumTypes([]string{dir}) + if err != nil { + t.Fatalf("ScanSwaggerEnumTypes: %v", err) } - msg := err.Error() - if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") { - t.Fatalf("error %q should mention both Alpha and Beta", msg) + key := EnumKey([]any{"x", "y"}) + names := got[key] + if !slices.Equal(names, []string{"Alpha", "Beta"}) { + t.Fatalf("map[%q] = %v, want [Alpha Beta]", key, names) } } @@ -168,7 +178,7 @@ type Hue string t.Fatalf("ScanSwaggerEnumTypes: %v", err) } wantKey := EnumKey([]any{"a", "b"}) - if got[wantKey] != "Hue" { + if single(got, wantKey) != "Hue" { t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue") } } @@ -194,7 +204,7 @@ type Shade string t.Fatalf("ScanSwaggerEnumTypes: %v", err) } wantKey := EnumKey([]any{"dark", "light"}) - if got[wantKey] != "Shade" { + if single(got, wantKey) != "Shade" { t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade") } } @@ -230,10 +240,10 @@ const ( } colorKey := EnumKey([]any{"red", "blue"}) shadeKey := EnumKey([]any{"dark", "light"}) - if got[colorKey] != "Color" { + if single(got, colorKey) != "Color" { t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color") } - if got[shadeKey] != "Shade" { + if single(got, shadeKey) != "Shade" { t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade") } } diff --git a/models/db/engine.go b/models/db/engine.go index d4ac0b4aca0..ef0636ca607 100755 --- a/models/db/engine.go +++ b/models/db/engine.go @@ -28,9 +28,8 @@ var ( registeredInitFuncs []func() error ) -// Engine represents a xorm engine or session. -type Engine interface { - Table(tableNameOrBean any) *xorm.Session +// SQLSession represents a common interface for engine and session to execute SQLs +type SQLSession interface { Count(...any) (int64, error) Decr(column string, arg ...any) *xorm.Session Delete(...any) (int64, error) @@ -52,7 +51,6 @@ type Engine interface { Limit(limit int, start ...int) *xorm.Session NoAutoTime() *xorm.Session SumInt(bean any, columnName string) (res int64, err error) - Sync(...any) error Select(string) *xorm.Session SetExpr(string, any) *xorm.Session NotIn(string, ...any) *xorm.Session @@ -61,12 +59,20 @@ type Engine interface { Distinct(...string) *xorm.Session Query(...any) ([]map[string][]byte, error) Cols(...string) *xorm.Session + Table(tableNameOrBean any) *xorm.Session Context(ctx context.Context) *xorm.Session - Ping() error + QueryInterface(sqlOrArgs ...any) ([]map[string]any, error) IsTableExist(tableNameOrBean any) (bool, error) } -// Session represents a xorm session interface, used as an abstraction over *xorm.Session. +// Engine represents a xorm engine +type Engine interface { + SQLSession + Sync(...any) error + Ping() error +} + +// Session represents a xorm session interface type Session interface { Engine And(query any, args ...any) *xorm.Session @@ -89,7 +95,6 @@ type EngineMigration interface { Dialect() dialects.Dialect DropTables(beans ...any) error NewSession() *xorm.Session - QueryInterface(sqlOrArgs ...any) ([]map[string]any, error) SetMapper(mapper names.Mapper) SyncWithOptions(opts xorm.SyncOptions, beans ...any) (*xorm.SyncResult, error) TableInfo(bean any) (*schemas.Table, error) diff --git a/models/db/list.go b/models/db/list.go index 47c163c1d72..e91a7054112 100644 --- a/models/db/list.go +++ b/models/db/list.go @@ -24,10 +24,9 @@ type Paginator interface { } // SetSessionPagination sets pagination for a database session -func SetSessionPagination(sess Engine, p Paginator) Session { +func SetSessionPagination(sess Engine, p Paginator) { skip, take := p.GetSkipTake() - - return sess.Limit(take, skip) + sess.Limit(take, skip) } // ListOptions options to paginate results diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index f271850a147..b734c81af9c 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -181,7 +181,7 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio findSession := listPullRequestStatement(ctx, baseRepoID, opts) applySorts(findSession, opts.SortType, 0) - findSession = db.SetSessionPagination(findSession, opts) + db.SetSessionPagination(findSession, opts) prs := make([]*PullRequest, 0, opts.PageSize) found := findSession.Find(&prs) return prs, maxResults, found diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index aedd679c57f..81a618a2076 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -414,6 +414,7 @@ func prepareMigrationTasks() []*migration { newMigration(334, "Add cancelling support to action runners", v1_27.AddCancellingSupportToActionRunner), newMigration(335, "Add reusable workflow fields and action_run_attempt_job_id_index table for ActionRunJob", v1_27.AddReusableWorkflowFieldsToActionRunJob), newMigration(336, "Add ActionRunJobSummary table", v1_27.AddActionRunJobSummaryTable), + newMigration(337, "Add visibility to team", v1_27.AddVisibilityToTeam), } return preparedMigrations } diff --git a/models/migrations/v1_27/v337.go b/models/migrations/v1_27/v337.go new file mode 100644 index 00000000000..61fd2445ccf --- /dev/null +++ b/models/migrations/v1_27/v337.go @@ -0,0 +1,36 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_27 + +import ( + "gitea.dev/models/db" + + "xorm.io/xorm" +) + +type VisibleType int + +type teamWithVisibility struct { + Visibility VisibleType `xorm:"NOT NULL DEFAULT 2"` +} + +func (teamWithVisibility) TableName() string { + return "team" +} + +func AddVisibilityToTeam(x db.EngineMigration) error { + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + IgnoreConstrains: true, + }, new(teamWithVisibility)); err != nil { + return err + } + + // Owner teams must remain listable to all org members; new orgs create + // them as "limited", so make existing owner teams limited too. + // Filter on authorize=4 (AccessModeOwner) so a user-created team that + // happens to share the name "owners" is not accidentally affected. + _, err := x.Exec("UPDATE `team` SET visibility = ? WHERE lower_name = ? AND authorize = ?", 1, "owners", 4) + return err +} diff --git a/models/organization/org.go b/models/organization/org.go index c2f77a83dde..9a28aa39181 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -370,6 +370,7 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode NumMembers: 1, IncludesAllRepositories: true, CanCreateOrgRepo: true, + Visibility: structs.VisibleTypeLimited, } if err = db.Insert(ctx, t); err != nil { return fmt.Errorf("insert owner team: %w", err) diff --git a/models/organization/team.go b/models/organization/team.go index fd6365e7319..ea2f52a1edb 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -14,6 +14,7 @@ import ( "gitea.dev/models/unit" user_model "gitea.dev/models/user" "gitea.dev/modules/log" + "gitea.dev/modules/structs" "gitea.dev/modules/util" "xorm.io/builder" @@ -81,9 +82,36 @@ type Team struct { Members []*user_model.User `xorm:"-"` NumRepos int NumMembers int - Units []*TeamUnit `xorm:"-"` - IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` - CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"` + Units []*TeamUnit `xorm:"-"` + IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` + CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"` + Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 2"` +} + +func (t *Team) IsPublic() bool { return t.Visibility.IsPublic() } +func (t *Team) IsLimited() bool { return t.Visibility.IsLimited() } +func (t *Team) IsPrivate() bool { return t.Visibility.IsPrivate() } + +// CanNonMemberReadMeta reports whether a non-member, non-owner doer may read +// the team's metadata, based on the team's visibility tier and the parent org's +// visibility. Privileged callers (site admins, org owners, team members) are +// decided by the caller before reaching here. +func (t *Team) CanNonMemberReadMeta(ctx context.Context, org, doer *user_model.User) (bool, error) { + switch t.Visibility { + case structs.VisibleTypePublic: + return HasOrgOrUserVisible(ctx, org, doer), nil + case structs.VisibleTypeLimited: + return IsOrganizationMember(ctx, t.OrgID, doer.ID) + default: + return false, nil + } +} + +func NormalizeTeamVisibility(s string) structs.VisibleType { + if vt, ok := structs.VisibilityModes[s]; ok { + return vt + } + return structs.VisibleTypePrivate } func init() { diff --git a/models/organization/team_list.go b/models/organization/team_list.go index 64b084eaaaf..bb2f9f224d1 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -10,6 +10,8 @@ import ( "gitea.dev/models/db" "gitea.dev/models/perm" "gitea.dev/models/unit" + user_model "gitea.dev/models/user" + "gitea.dev/modules/structs" "xorm.io/builder" ) @@ -50,9 +52,15 @@ type SearchTeamOptions struct { Keyword string OrgID int64 IncludeDesc bool + // IncludeVisibilities, when combined with UserID, also returns teams whose + // visibility is in this list, even if UserID is not a member. Typical values: + // - {limited,public} for org members + // - {public} for signed-in users who are not org members + // Leave empty to return only teams the user is a member of. + IncludeVisibilities []structs.VisibleType } -func (opts *SearchTeamOptions) toCond() builder.Cond { +func (opts *SearchTeamOptions) applyToSession(sess db.SQLSession) { cond := builder.NewCond() if len(opts.Keyword) > 0 { @@ -68,11 +76,51 @@ func (opts *SearchTeamOptions) toCond() builder.Cond { cond = cond.And(builder.Eq{"`team`.org_id": opts.OrgID}) } - if opts.UserID > 0 { + switch { + case opts.UserID > 0 && len(opts.IncludeVisibilities) > 0: + sess = sess.Join("LEFT", "team_user", "team_user.team_id = team.id AND team_user.uid = ?", opts.UserID) + cond = cond.And(builder.Or( + builder.Eq{"team_user.uid": opts.UserID}, + builder.In("`team`.visibility", opts.IncludeVisibilities), + )) + case opts.UserID > 0: + sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id") cond = cond.And(builder.Eq{"team_user.uid": opts.UserID}) + case len(opts.IncludeVisibilities) > 0: + cond = cond.And(builder.In("`team`.visibility", opts.IncludeVisibilities)) } + sess.Where(cond) +} - return cond +func VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn bool) []structs.VisibleType { + switch { + case isOrgMember: + return []structs.VisibleType{structs.VisibleTypeLimited, structs.VisibleTypePublic} + case isSignedIn: + return []structs.VisibleType{structs.VisibleTypePublic} + default: + return nil + } +} + +func ApplyTeamListFilter(ctx context.Context, orgID int64, viewer *user_model.User, isSignedIn bool, opts *SearchTeamOptions) error { + if viewer.IsAdmin { + return nil + } + isOwner, err := IsOrganizationOwner(ctx, orgID, viewer.ID) + if err != nil { + return err + } + if isOwner { + return nil + } + isOrgMember, err := IsOrganizationMember(ctx, orgID, viewer.ID) + if err != nil { + return err + } + opts.UserID = viewer.ID + opts.IncludeVisibilities = VisibleTeamVisibilitiesFor(isOrgMember, isSignedIn) + return nil } // SearchTeam search for teams. Caller is responsible to check permissions. @@ -80,15 +128,12 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64, sess := db.GetEngine(ctx) opts.SetDefaultValues() - cond := opts.toCond() + opts.applyToSession(sess) - if opts.UserID > 0 { - sess = sess.Join("INNER", "team_user", "team_user.team_id = team.id") - } db.SetSessionPagination(sess, opts) teams := make([]*Team, 0, opts.PageSize) - count, err := sess.Where(cond).OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams) + count, err := sess.OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams) if err != nil { return nil, 0, err } diff --git a/models/organization/team_test.go b/models/organization/team_test.go index aa1cc90caa3..30fe6e82290 100644 --- a/models/organization/team_test.go +++ b/models/organization/team_test.go @@ -10,6 +10,8 @@ import ( "gitea.dev/models/organization" repo_model "gitea.dev/models/repo" "gitea.dev/models/unittest" + user_model "gitea.dev/models/user" + "gitea.dev/modules/structs" "github.com/stretchr/testify/assert" ) @@ -38,6 +40,43 @@ func TestTeam_IsMember(t *testing.T) { assert.False(t, team.IsMember(t.Context(), unittest.NonexistentID)) } +func TestTeam_CanNonMemberReadMeta(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // public org + org35 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 35}) // private org + member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // member of org 3 and org 35 + outsider := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // member of neither org + + test := func(name string, team *organization.Team, org, doer *user_model.User, expected bool) { + t.Run(name, func(t *testing.T) { + ok, err := team.CanNonMemberReadMeta(t.Context(), org, doer) + assert.NoError(t, err) + assert.Equal(t, expected, ok) + }) + } + + // Public team is gated only by the parent org's visibility. + publicTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePublic} + test("public team, public org, member", publicTeam, org3, member, true) + test("public team, public org, outsider", publicTeam, org3, outsider, true) + + // Public team inside a private org: only org members may see it. + publicTeamPrivOrg := &organization.Team{OrgID: 35, Visibility: structs.VisibleTypePublic} + test("public team, private org, org member", publicTeamPrivOrg, org35, member, true) + test("public team, private org, outsider", publicTeamPrivOrg, org35, outsider, false) + + // Limited team: any org member, but never outsiders. + limitedTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypeLimited} + test("limited team, org member", limitedTeam, org3, member, true) + test("limited team, outsider", limitedTeam, org3, outsider, false) + + // Private team is never visible to non-members; members/owners are admitted by the caller. + privateTeam := &organization.Team{OrgID: 3, Visibility: structs.VisibleTypePrivate} + test("private team, org member", privateTeam, org3, member, false) + test("private team, outsider", privateTeam, org3, outsider, false) +} + func TestTeam_GetRepositories(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -172,6 +211,52 @@ func TestGetUserOrgTeams(t *testing.T) { test(3, unittest.NonexistentID) } +func TestSearchTeamIncludeVisible(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + const orgID int64 = 3 + // User 5 is an org member but only belongs to team 1 (Owners) — make sure + // they don't see team 2 (default private) but do see a freshly added + // limited team they are not a member of. + visible := &organization.Team{ + OrgID: orgID, + LowerName: "visible-team", + Name: "visible-team", + AccessMode: 1, // read + Visibility: structs.VisibleTypeLimited, + } + assert.NoError(t, db.Insert(t.Context(), visible)) + teams, _, err := organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{ + OrgID: orgID, + UserID: 2, + IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true), + }) + assert.NoError(t, err) + ids := make(map[int64]bool, len(teams)) + for _, team := range teams { + assert.Equal(t, orgID, team.OrgID) + ids[team.ID] = true + } + // user 2 is in team 1 and team 2 in org 3, plus should see the new visible team. + assert.True(t, ids[1], "expected to see team 1 (member)") + assert.True(t, ids[2], "expected to see team 2 (member)") + assert.True(t, ids[visible.ID], "expected to see visible team") + + // user 5 is only an org member in team 1, must not see secret team 2 but must see the visible one. + teams, _, err = organization.SearchTeam(t.Context(), &organization.SearchTeamOptions{ + OrgID: orgID, + UserID: 5, + IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(true, true), + }) + assert.NoError(t, err) + ids = make(map[int64]bool, len(teams)) + for _, team := range teams { + ids[team.ID] = true + } + assert.False(t, ids[2], "user 5 must not see private team 2") + assert.True(t, ids[visible.ID], "user 5 must see the limited team") +} + func TestHasTeamRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index d0e5dd8f60f..57f9e788332 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -783,7 +783,8 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) repos := make(RepositoryList, 0, opts.PageSize) - return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos) + db.SetSessionPagination(sess, &opts) + return repos, count, sess.Find(&repos) } func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) { diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index 20959931d33..c9542951a02 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -4,6 +4,20 @@ package structs +// TeamVisibility controls who can list a team within its organization. +// - "public": visible to any signed-in user (still bounded by org visibility) +// - "limited": visible to any member of the parent organization +// - "private": visible only to team members and org owners +// +// swagger:enum TeamVisibility +type TeamVisibility string + +const ( + TeamVisibilityPublic TeamVisibility = "public" + TeamVisibilityLimited TeamVisibility = "limited" + TeamVisibilityPrivate TeamVisibility = "private" +) + // Team represents a team in an organization type Team struct { // The unique identifier of the team @@ -24,6 +38,11 @@ type Team struct { UnitsMap map[string]string `json:"units_map"` // Whether the team can create repositories in the organization CanCreateOrgRepo bool `json:"can_create_org_repo"` + // Team visibility within the organization. "private" teams are only + // listable by members and org owners; "limited" teams are listable by + // any organization member; "public" teams are listable by any signed-in + // user. + Visibility TeamVisibility `json:"visibility"` } // CreateTeamOption options for creating a team @@ -42,6 +61,8 @@ type CreateTeamOption struct { UnitsMap map[string]string `json:"units_map"` // Whether the team can create repositories in the organization CanCreateOrgRepo bool `json:"can_create_org_repo"` + // Team visibility within the organization. Defaults to "private". + Visibility TeamVisibility `json:"visibility" binding:"OmitEmpty;In(public,limited,private)"` } // EditTeamOption options for editing a team @@ -60,4 +81,7 @@ type EditTeamOption struct { UnitsMap map[string]string `json:"units_map"` // Whether the team can create repositories in the organization CanCreateOrgRepo *bool `json:"can_create_org_repo"` + // Team visibility within the organization. When omitted, visibility is + // left unchanged. + Visibility *TeamVisibility `json:"visibility" binding:"OmitEmpty;In(public,limited,private)"` } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 90c7c71fb7d..f6e71cccb3e 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2865,6 +2865,14 @@ "org.teams.all_repositories_read_permission_desc": "This team grants Read access to all repositories: members can view and clone repositories.", "org.teams.all_repositories_write_permission_desc": "This team grants Write access to all repositories: members can read from and push to repositories.", "org.teams.all_repositories_admin_permission_desc": "This team grants Admin access to all repositories: members can read from, push to and add collaborators to repositories.", + "org.teams.visibility": "Visibility", + "org.teams.visibility_private": "Private", + "org.teams.visibility_private_helper": "Visible only to team members and organization owners.", + "org.teams.visibility_limited": "Limited", + "org.teams.visibility_limited_helper": "Visible to all members of this organization.", + "org.teams.visibility_public": "Public", + "org.teams.visibility_public_helper": "Visible to any signed-in user.", + "org.teams.owners_visibility_fixed": "The Owners team visibility cannot be changed.", "org.teams.invite.title": "You have been invited to join team %s in organization %s.", "org.teams.invite.by": "Invited by %s", "org.teams.invite.description": "Please click the button below to join the team.", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 4715ca1d672..02cabc55d16 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -505,41 +505,79 @@ func reqOrgOwnership() func(ctx *context.APIContext) { } } -// reqTeamMembership user should be an team member, or a site admin -func reqTeamMembership() func(ctx *context.APIContext) { +func teamAccessPrivileged(ctx *context.APIContext) (orgID int64, privileged, ok bool) { + if ctx.IsUserSiteAdmin() { + return 0, true, true + } + if ctx.Org.Team == nil { + setting.PanicInDevOrTesting("teamAccess: unprepared context") + ctx.APIErrorInternal(errors.New("teamAccess: unprepared context")) + return 0, false, false + } + + orgID = ctx.Org.Team.OrgID + isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return 0, false, false + } else if isOwner { + return orgID, true, true + } + + isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return 0, false, false + } + return orgID, isTeamMember, true +} + +func denyNonTeamMember(ctx *context.APIContext, orgID int64) { + isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + } else if isOrgMember { + ctx.APIError(http.StatusForbidden, "Must be a team member") + } else { + ctx.APIErrorNotFound() + } +} + +// reqTeamReadAccess allows callers who can list the team to read its metadata. +// Non-members are admitted by the team's visibility tier and parent org visibility. +// Not sufficient for mutations — use reqOrgOwnership() or reqTeamMembership() for those. +func reqTeamReadAccess() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.IsUserSiteAdmin() { + orgID, privileged, ok := teamAccessPrivileged(ctx) + if !ok || privileged { return } - if ctx.Org.Team == nil { - setting.PanicInDevOrTesting("reqTeamMembership: unprepared context") - ctx.APIErrorInternal(errors.New("reqTeamMembership: unprepared context")) + if ctx.Org.Organization == nil { + setting.PanicInDevOrTesting("reqTeamReadAccess: organization not loaded") + ctx.APIErrorInternal(errors.New("reqTeamReadAccess: organization not loaded")) return } - orgID := ctx.Org.Team.OrgID - isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID) + visible, err := ctx.Org.Team.CanNonMemberReadMeta(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return - } else if isOwner { - return } + if !visible { + // Not admitted by visibility: 403 for org members, 404 otherwise. + denyNonTeamMember(ctx, orgID) + } + } +} - if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil { - ctx.APIErrorInternal(err) - return - } else if !isTeamMember { - isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID) - if err != nil { - ctx.APIErrorInternal(err) - } else if isOrgMember { - ctx.APIError(http.StatusForbidden, "Must be a team member") - } else { - ctx.APIErrorNotFound() - } +// reqTeamMembership user should be a team member, or a site admin +func reqTeamMembership() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + orgID, privileged, ok := teamAccessPrivileged(ctx) + if !ok || privileged { return } + denyNonTeamMember(ctx, orgID) } } @@ -649,6 +687,17 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } return } + if ctx.Org.Organization == nil { + ctx.Org.Organization, err = organization.GetOrgByID(ctx, ctx.Org.Team.OrgID) + if err != nil { + if organization.IsErrOrgNotExist(err) { + ctx.APIErrorNotFound() + } else { + ctx.APIErrorInternal(err) + } + return + } + } } } } @@ -1703,25 +1752,31 @@ func Routes() *web.Router { }, reqToken(), reqOrgOwnership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly()) m.Group("/teams/{teamid}", func() { - m.Combo("").Get(reqToken(), org.GetTeam). - Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam). + m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam). Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam) + m.Group("", func() { + m.Get("", org.GetTeam) + m.Group("/members", func() { + m.Get("", reqOrgMembership(), org.GetTeamMembers) + m.Combo("/{username}").Get(reqOrgMembership(), org.GetTeamMember) + }) + m.Group("/repos", func() { + m.Get("", org.GetTeamRepos) + m.Combo("/{org}/{reponame}").Get(org.GetTeamRepo) + }) + m.Get("/activities/feeds", org.ListTeamActivityFeeds) + }, reqTeamReadAccess()) m.Group("/members", func() { - m.Get("", reqToken(), org.GetTeamMembers) m.Combo("/{username}"). - Get(reqToken(), org.GetTeamMember). Put(reqToken(), reqOrgOwnership(), org.AddTeamMember). Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember) }) m.Group("/repos", func() { - m.Get("", reqToken(), org.GetTeamRepos) m.Combo("/{org}/{reponame}"). - Put(reqToken(), org.AddTeamRepository). - Delete(reqToken(), org.RemoveTeamRepository). - Get(reqToken(), org.GetTeamRepo) + Put(reqToken(), reqTeamMembership(), org.AddTeamRepository). + Delete(reqToken(), reqTeamMembership(), org.RemoveTeamRepository) }) - m.Get("/activities/feeds", org.ListTeamActivityFeeds) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), checkTokenPublicOnly()) m.Group("/admin", func() { m.Group("/cron", func() { diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 4373b90f2fa..bc6bb54e90c 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -55,10 +55,15 @@ func ListTeams(ctx *context.APIContext) { // "$ref": "#/responses/notFound" listOptions := utils.GetListOptions(ctx) - teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{ + opts := &organization.SearchTeamOptions{ ListOptions: listOptions, OrgID: ctx.Org.Organization.ID, - }) + } + if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil { + ctx.APIErrorInternal(err) + return + } + teams, count, err := organization.SearchTeam(ctx, opts) if err != nil { ctx.APIErrorInternal(err) return @@ -218,6 +223,7 @@ func CreateTeam(ctx *context.APIContext) { IncludesAllRepositories: form.IncludesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, AccessMode: teamPermission, + Visibility: organization.NormalizeTeamVisibility(string(form.Visibility)), } if team.AccessMode < perm.AccessModeAdmin { @@ -295,6 +301,10 @@ func EditTeam(ctx *context.APIContext) { team.Description = *form.Description } + if form.Visibility != nil && !team.IsOwnerTeam() { + team.Visibility = organization.NormalizeTeamVisibility(string(*form.Visibility)) + } + isAuthChanged := false isIncludeAllChanged := false if !team.IsOwnerTeam() && len(form.Permission) != 0 { @@ -387,15 +397,6 @@ func GetTeamMembers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID) - if err != nil { - ctx.APIErrorInternal(err) - return - } else if !isMember && !ctx.Doer.IsAdmin { - ctx.APIErrorNotFound() - return - } - listOptions := utils.GetListOptions(ctx) teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ ListOptions: listOptions, @@ -574,14 +575,20 @@ func GetTeamRepos(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - repos := make([]*api.Repository, len(teamRepos)) - for i, repo := range teamRepos { + repos := make([]*api.Repository, 0, len(teamRepos)) + for _, repo := range teamRepos { permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return } - repos[i] = convert.ToRepo(ctx, repo, permission) + // A team's repo list is reachable by non-team-members through the team's + // visibility tier, so never expose repos (incl. their names) the doer + // cannot access. + if !permission.HasAnyUnitAccessOrPublicAccess() { + continue + } + repos = append(repos, convert.ToRepo(ctx, repo, permission)) } ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize) ctx.SetTotalCountHeader(int64(team.NumRepos)) @@ -633,6 +640,12 @@ func GetTeamRepo(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } + // The team may be reachable by a non-team-member via its visibility tier; + // don't confirm the existence of a repo the doer cannot access. + if !permission.HasAnyUnitAccessOrPublicAccess() { + ctx.APIErrorNotFound() + return + } ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) } @@ -806,9 +819,9 @@ func SearchTeam(ctx *context.APIContext) { ListOptions: listOptions, } - // Only admin is allowed to search for all teams - if !ctx.Doer.IsAdmin { - opts.UserID = ctx.Doer.ID + if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil { + ctx.APIErrorInternal(err) + return } teams, maxResults, err := organization.SearchTeam(ctx, opts) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index a88c8461767..7c52455d31d 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -101,7 +101,26 @@ func home(ctx *context.Context, viewRepositories bool) { const orgOverviewTeamsLimit = 5 ctx.Data["OrgOverviewMembers"] = members - ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)] + // The overview widget shows only teams the viewer belongs to. ctx.Org.Teams + // may include visible-but-not-joined teams (via IncludeVisibilities for + // signed-in non-members), so re-query the viewer's own membership; owners + // keep the full list they are entitled to manage. + overviewTeams := ctx.Org.Teams + if !ctx.Org.IsOwner { + overviewTeams = nil + if ctx.Org.IsMember { + overviewTeams, _, err = organization.SearchTeam(ctx, &organization.SearchTeamOptions{ + OrgID: org.ID, + UserID: ctx.Doer.ID, + ListOptions: db.ListOptions{Page: 1, PageSize: orgOverviewTeamsLimit}, + }) + if err != nil { + ctx.ServerError("SearchTeam", err) + return + } + } + } + ctx.Data["OrgOverviewTeams"] = overviewTeams[:min(len(overviewTeams), orgOverviewTeamsLimit)] ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index a026f4aa073..11c4a1da3a5 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -21,6 +21,7 @@ import ( user_model "gitea.dev/models/user" "gitea.dev/modules/log" "gitea.dev/modules/setting" + "gitea.dev/modules/structs" "gitea.dev/modules/templates" "gitea.dev/modules/util" "gitea.dev/modules/web" @@ -80,6 +81,8 @@ func Teams(ctx *context.Context) { UserID: util.Iif(shouldSeeAllOrgTeams, 0, ctx.Doer.ID), Keyword: keyword, IncludeDesc: true, + IncludeVisibilities: util.Iif(shouldSeeAllOrgTeams, nil, + org_model.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, ctx.IsSigned)), ListOptions: db.ListOptions{Page: page, PageSize: pagingNum}, } return org_model.SearchTeam(ctx, opts) @@ -377,6 +380,7 @@ func NewTeamPost(ctx *context.Context) { AccessMode: teamPermission, IncludesAllRepositories: includesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, + Visibility: org_model.NormalizeTeamVisibility(form.Visibility), } units := make([]*org_model.TeamUnit, 0, len(unitPerms)) @@ -477,13 +481,22 @@ func SearchTeam(ctx *context.Context) { PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), } + shouldSeeAll, err := context.UserShouldSeeAllOrgTeams(ctx) + if err != nil { + ctx.ServerError("UserShouldSeeAllOrgTeams", err) + return + } + opts := &org_model.SearchTeamOptions{ - // UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in Keyword: ctx.FormTrim("q"), OrgID: ctx.Org.Organization.ID, IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), ListOptions: listOptions, } + if !shouldSeeAll { + opts.UserID = ctx.Doer.ID + opts.IncludeVisibilities = org_model.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, ctx.IsSigned) + } teams, maxResults, err := org_model.SearchTeam(ctx, opts) if err != nil { @@ -556,8 +569,11 @@ func EditTeamPost(ctx *context.Context) { t.IncludesAllRepositories = includesAllRepositories } t.CanCreateOrgRepo = form.CanCreateOrgRepo + t.Visibility = org_model.NormalizeTeamVisibility(form.Visibility) } else { t.CanCreateOrgRepo = true + // The owner team must remain listable to all org members. + t.Visibility = structs.VisibleTypeLimited } t.Description = form.Description diff --git a/services/context/org.go b/services/context/org.go index 79dcfab5f2b..0f6a06e3269 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -179,20 +179,28 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) { ctx.ServerError("UserShouldSeeAllOrgTeams", err) return } - if ctx.Org.IsMember { - if shouldSeeAllTeams { - ctx.Org.Teams, err = org.LoadTeams(ctx) - if err != nil { - ctx.ServerError("LoadTeams", err) - return - } - } else { - ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID) - if err != nil { - ctx.ServerError("GetUserTeams", err) - return - } + switch { + case shouldSeeAllTeams: + ctx.Org.Teams, err = org.LoadTeams(ctx) + if err != nil { + ctx.ServerError("LoadTeams", err) + return } + case ctx.IsSigned: + // Signed-in non-members still see teams whose visibility tier + // includes them (public for any signed-in user, plus limited + // for org members), and any team they directly belong to. + ctx.Org.Teams, _, err = organization.SearchTeam(ctx, &organization.SearchTeamOptions{ + OrgID: org.ID, + UserID: ctx.Doer.ID, + IncludeVisibilities: organization.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, true), + }) + if err != nil { + ctx.ServerError("SearchTeam", err) + return + } + } + if ctx.Org.IsMember { ctx.Data["NumTeams"] = len(ctx.Org.Teams) } @@ -203,7 +211,6 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) { if strings.EqualFold(team.LowerName, teamName) { teamExists = true ctx.Org.Team = team - ctx.Org.IsTeamMember = true ctx.Data["Team"] = ctx.Org.Team break } @@ -214,13 +221,24 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) { return } + // Membership in a visible team is not implied by its presence in + // ctx.Org.Teams; admins/org owners keep the privileged flag set + // earlier in this function. + if !ctx.Org.IsOwner { + ctx.Org.IsTeamMember, err = organization.IsTeamMember(ctx, org.ID, ctx.Org.Team.ID, ctx.Doer.ID) + if err != nil { + ctx.ServerError("IsTeamMember", err) + return + } + } ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember if opts.RequireTeamMember && !ctx.Org.IsTeamMember { ctx.NotFound(err) return } - ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.HasAdminAccess() + isTeamOwnerOrAdmin := ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.HasAdminAccess() + ctx.Org.IsTeamAdmin = ctx.Org.IsOwner || (ctx.Org.IsTeamMember && isTeamOwnerOrAdmin) ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin if opts.RequireTeamAdmin && !ctx.Org.IsTeamAdmin { ctx.NotFound(err) diff --git a/services/convert/convert.go b/services/convert/convert.go index 49d74f7d109..d1437de286f 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -836,6 +836,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([] Permission: api.AccessLevelName(t.AccessMode.ToString()), Units: t.GetUnitNames(), UnitsMap: t.GetUnitsMap(), + Visibility: api.TeamVisibility(t.Visibility.String()), } if loadOrgs { diff --git a/services/forms/org.go b/services/forms/org.go index 2aacf1b645d..be3f64d630e 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -70,6 +70,7 @@ type CreateTeamForm struct { Permission string RepoAccess string CanCreateOrgRepo bool + Visibility string `binding:"OmitEmpty;In(public,limited,private)"` } // Validate validates the fields diff --git a/services/org/team.go b/services/org/team.go index f3313e0edf5..b079313306d 100644 --- a/services/org/team.go +++ b/services/org/team.go @@ -110,7 +110,7 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA sess := db.GetEngine(ctx) if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description", - "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil { + "can_create_org_repo", "authorize", "includes_all_repositories", "visibility").Update(t); err != nil { return fmt.Errorf("update: %w", err) } diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index f8785bb466a..7e0403a2f56 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -12,10 +12,10 @@ {{template "base/alert" .}}
    - {{if eq .Team.LowerName "owners"}} + {{if .Team.IsOwnerTeam}} {{end}} - + {{ctx.Locale.Tr "org.team_name_helper"}}
    @@ -23,7 +23,47 @@ {{ctx.Locale.Tr "org.team_desc_helper"}}
    - {{if not (eq .Team.LowerName "owners")}} + {{if .Team.IsOwnerTeam}} +
    + +
    + {{if .Team.IsPrivate}} + {{ctx.Locale.Tr "org.teams.visibility_private"}} + {{else if .Team.IsLimited}} + {{ctx.Locale.Tr "org.teams.visibility_limited"}} + {{else if .Team.IsPublic}} + {{ctx.Locale.Tr "org.teams.visibility_public"}} + {{end}} +
    + {{ctx.Locale.Tr "org.teams.owners_visibility_fixed"}} +
    + {{end}} + {{if not .Team.IsOwnerTeam}} +
    + +
    +
    +
    + + + {{ctx.Locale.Tr "org.teams.visibility_private_helper"}} +
    +
    +
    +
    + + + {{ctx.Locale.Tr "org.teams.visibility_limited_helper"}} +
    +
    +
    +
    + + + {{ctx.Locale.Tr "org.teams.visibility_public_helper"}} +
    +
    +

    @@ -135,7 +175,7 @@ {{else}} - {{if not (eq .Team.LowerName "owners")}} + {{if not .Team.IsOwnerTeam}} {{end}} {{end}} diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 1036e886da7..a0b936285e6 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -1,6 +1,15 @@

    - {{.Team.Name}} +
    + {{.Team.Name}} + {{if .Team.IsPrivate}} + {{ctx.Locale.Tr "org.teams.visibility_private"}} + {{else if .Team.IsLimited}} + {{ctx.Locale.Tr "org.teams.visibility_limited"}} + {{else if .Team.IsPublic}} + {{ctx.Locale.Tr "org.teams.visibility_public"}} + {{end}} +
    {{if .Team.IsMember ctx $.SignedUser.ID}}

    `) + output.WriteHTML(` +
    +
    +
    `) case "code": output.WriteHTML(`
    `) if err := renderCellCode(output, cell, language); err != nil { @@ -273,13 +283,7 @@ func renderCell(ctx *markup.RenderContext, output htmlutil.HTMLWriter, cell Cell } output.WriteHTML(`
    `) default: - output.WriteFormat(` -
    -
    -
    Cell:
    -
    [Cell type %s - unsupported, skipped]
    -
    -
    `, cell.CellType) + renderCellPrompt(output, "Cell:", htmlutil.HTMLFormat("[Cell type %s - unsupported, skipped]", cell.CellType)) } return output.Err() } diff --git a/modules/markup/jupyter/jupyter_test.go b/modules/markup/jupyter/jupyter_test.go index fd1464d1727..61d362da987 100644 --- a/modules/markup/jupyter/jupyter_test.go +++ b/modules/markup/jupyter/jupyter_test.go @@ -215,7 +215,7 @@ func TestRender(t *testing.T) { err := r.Render(ctx, strings.NewReader(input), &output) assert.NoError(t, err) - assert.Regexp(t, `
    This notebook uses an older format.*
    `, output.String()) + assert.Regexp(t, `
    This notebook uses an older format.*
    `, output.String()) }) } diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index d108ea33797..ef3415d52e5 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -28,7 +28,7 @@

    -
    +
    {{if .IsFileTooLarge}} {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index b04dc16cdfb..96148f57a9f 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -11,7 +11,7 @@ {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}}
    -
    +
    {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
    {{if .IsFileTooLarge}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 9f936afb8e0..7d139efceb2 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -91,7 +91,7 @@
    -
    +
    {{if not .RenderAsMarkup}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{end}} diff --git a/web_src/css/repo/file-view.css b/web_src/css/repo/file-view.css index 3f1c42a4a1f..fec1c7cd8f5 100644 --- a/web_src/css/repo/file-view.css +++ b/web_src/css/repo/file-view.css @@ -1,3 +1,9 @@ +.file-view-container { + padding: 0 !important; /* the file-view itself provides padding */ + width: 100% !important; /* override fomantic's "100% + 2px" */ + max-width: 100% !important; +} + .file-view tr.active .lines-num, .file-view tr.active .lines-escape, .file-view tr.active .lines-code { From bce6df24b799a2fc7f9be7bb8a5ac2a4075d66e0 Mon Sep 17 00:00:00 2001 From: bircni Date: Mon, 15 Jun 2026 06:56:26 +0200 Subject: [PATCH 86/88] feat(actions): show run status on browser tab favicon (#38071) --- web_src/js/components/ActionStatusIcon.vue | 21 ++--- web_src/js/components/RepoActionView.vue | 11 ++- web_src/js/modules/action-status-icon.test.ts | 9 ++ web_src/js/modules/action-status-icon.ts | 37 ++++++++ web_src/js/modules/favicon-status.test.ts | 29 ++++++ web_src/js/modules/favicon-status.ts | 90 +++++++++++++++++++ web_src/js/svg.ts | 2 + 7 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 web_src/js/modules/action-status-icon.test.ts create mode 100644 web_src/js/modules/action-status-icon.ts create mode 100644 web_src/js/modules/favicon-status.test.ts create mode 100644 web_src/js/modules/favicon-status.ts diff --git a/web_src/js/components/ActionStatusIcon.vue b/web_src/js/components/ActionStatusIcon.vue index 12da669ac4d..bebb510467b 100644 --- a/web_src/js/components/ActionStatusIcon.vue +++ b/web_src/js/components/ActionStatusIcon.vue @@ -2,32 +2,33 @@ action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, cancelling, unknown. --> diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 03d62bd7ccf..9ce926673b9 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -1,7 +1,8 @@