From 8bf3c8b79dcb98c4838a5334d2ad70514962583a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 8 Apr 2026 13:43:28 -0700 Subject: [PATCH 001/126] Frontport changelog of v1.26.0-rc0 (#37138) --- CHANGELOG.md | 379 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b662cb4ad5b..0fbdf6d9f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,385 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.26.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0-rc0) - 2026-04-07 + +* BREAKING + * Correct swagger annotations for enums, status codes, and notification state (#37030) + * Remove GET API registration-token (#36801) + * Support Actions `concurrency` syntax (#32751) + * Make PUBLIC_URL_DETECTION default to "auto" (#36955) +* SECURITY + * Bound PageSize in `ListUnadoptedRepositories` (#36884) +* FEATURES + * Support Actions `concurrency` syntax (#32751) + * Add terraform state registry (#36710) + * Instance-wide (global) info banner and maintenance mode (#36571) + * Support rendering OpenAPI spec (#36449) + * Add keyboard shortcuts for repository file and code search (#36416) + * Add support for archive-upload rpc (#36391) + * Add ability to download subpath archive (#36371) + * Add workflow dependencies visualization (#26062) (#36248) & Restyle Workflow Graph (#36912) + * Automatic generation of release notes (#35977) + * Add "Go to file", "Delete Directory" to repo file list page (#35911) + * Introduce "config edit-ini" sub command to help maintaining INI config file (#35735) + * Add button to re-run failed jobs in Actions (#36924) + * Support actions and reusable workflows from private repos (#32562) + * Add summary to action runs view (#36883) + * Add user badges (#36752) + * Add configurable permissions for Actions automatic tokens (#36173) + * Add per-runner “Disable/Pause” (#36776) +* PERFORMANCE + * WorkflowDispatch API optionally return runid (#36706) + * Add render cache for SVG icons (#36863) + * Load `mentionValues` asynchronously (#36739) + * Lazy-load some Vue components, fix heatmap chunk loading on every page (#36719) + * Load heatmap data asynchronously (#36622) + * Use prev/next pagination for user profile activities page to speed up (#36642) + * Refactor cat-file batch operations and support `--batch-command` approach (#35775) + * Use merge tree to detect conflicts when possible (#36400) +* ENHANCEMENTS + * Adds option to force update new branch in contents routes (#35592) + * Add viewer controller for mermaid (zoom, drag) (#36557) + * Add code editor setting dropdowns (#36534) + * Add `elk` layout support to mermaid (#36486) + * Add resolve/unresolve review comment API endpoints (#36441) + * Allow configuring default PR base branch (fixes #36412) (#36425) + * Add support for RPM Errata (updateinfo.xml) (#37125) + * Require additional user confirmation for making repo private (#36959) + * Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786) + * Add `actions.WORKFLOW_DIRS` setting (#36619) + * Avoid opening new tab when downloading actions logs (#36740) + * Implements OIDC RP-Initiated Logout (#36724) + * Show workflow link (#37070) + * Desaturate dark theme background colors (#37056) + * Refactor "org teams" page and help new users to "add member" to an org (#37051) + * Add webhook name field to improve webhook identification (#37025) (#37040) + * Make task list checkboxes clickable in the preview tab (#37010) + * Improve severity labels in Actions logs and tweak colors (#36993) + * Linkify URLs in Actions workflow logs (#36986) + * Allow text selection on checkbox labels (#36970) + * Support dark/light theme images in markdown (#36922) + * Enable native dark mode for swagger-ui (#36899) + * Rework checkbox styling, remove `input` border hover effect (#36870) + * Refactor storage content-type handling of ServeDirectURL (#36804) + * Use "Enable Gravatar" but not "Disable" (#36771) + * Use case-insensitive matching for Git error "Not a valid object name" (#36728) + * Add “Copy Source” to markup comment menu (#36726) + * Change image transparency grid to CSS (#36711) + * Add "Run" prefix for unnamed action steps (#36624) + * Persist actions log time display settings in `localStorage` (#36623) + * Use first commit title for multi-commit PRs and fix auto-focus title field (#36606) + * Improve BuildCaseInsensitiveLike with lowercase (#36598) + * Improve diff highlighting (#36583) + * Exclude cancelled runs from failure-only email notifications (#36569) + * Use full-file highlighting for diff sections (#36561) + * Color command/error logs in Actions log (#36538) + * Add paging headers (#36521) + * Improve timeline entries for WIP prefix changes in pull requests (#36518) + * Add FOLDER_ICON_THEME configuration option (#36496) + * Normalize guessed languages for code highlighting (#36450) + * Add chunked transfer encoding support for LFS uploads (#36380) + * Indicate when only optional checks failed (#36367) + * Add 'allow_maintainer_edit' API option for creating a pull request (#36283) + * Support closing keywords with URL references (#36221) + * Improve diff file headers (#36215) + * Fix and enhance comment editor monospace toggle (#36181) + * Add git.DIFF_RENAME_SIMILARITY_THRESHOLD option (#36164) + * Add matching pair insertion to markdown textarea (#36121) + * Add sorting/filtering to admin user search API endpoint (#36112) + * Allow action user have read permission in public repo like other user (#36095) + * Disable matchBrackets in monaco (#36089) + * Use GitHub-style commit message for squash merge (#35987) + * Make composer registry support tar.gz and tar.bz2 and fix bugs (#35958) + * Add GITEA_PR_INDEX env variable to githooks (#35938) + * Add proper error message if session provider can not be created (#35520) + * Add button to copy file name in PR files (#35509) + * Move `X_FRAME_OPTIONS` setting from `cors` to `security` section (#30256) + * Add placeholder content for empty content page (#37114) + * Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917) + * Redirect to the only OAuth2 provider when no other login methods and fix various problems (#36901) + * Add admin badge to navbar avatar (#36790) + * Add `never` option to `PUBLIC_URL_DETECTION` configuration (#36785) + * Add background and run count to actions list page (#36707) + * Add icon to buttons "Close with Comment", "Close Pull Request", "Close Issue" (#36654) + * Add support for in_progress event in workflow_run webhook (#36979) + * Report commit status for pull_request_review events (#36589) + * Render merged pull request title as such in dashboard feed (#36479) + * Feature to be able to filter project boards by milestones (#36321) + * Use user id in noreply emails (#36550) + * Enable pagination on GiteaDownloader.getIssueReactions() (#36549) + * Remove striped tables in UI (#36509) + * Improve control char rendering and escape button styling (#37094) + * Support legacy run/job index-based URLs and refactor migration 326 (#37008) + * Add date to "No Contributions" tooltip (#36190) + * Show edit page confirmation dialog on tree view file change (#36130) + * Mention proc-receive in text for dashboard.resync_all_hooks func (#35991) + * Reuse selectable style for wiki (#35990) + * Support blue yellow colorblind theme (#35910) + * Support selecting theme on the footer (#35741) + * Improve online runner check (#35722) + * Add quick approve button on PR page (#35678) + * Enable commenting on expanded lines in PR diffs (#35662) + * Print PR-Title into tooltip for actions (#35579) + * Use explicit, stronger defaults for newly generated repo signing keys for Debian (#36236) + * Improve the compare page (#36261) + * Unify repo names in system notices (#36491) + * Move package settings to package instead of being tied to version (#37026) + * Add Actions API rerun endpoints for runs and jobs (#36768) + * Add branch_count to repository API (#35351) (#36743) + * Add created_by filter to SearchIssues (#36670) + * Allow admins to rename non-local users (#35970) + * Support updating branch via API (#35951) + * Add an option to automatically verify SSH keys from LDAP (#35927) + * Make "update file" API can create a new file when SHA is not set (#35738) + * Update issue.go with labels documentation (labels content, not ids) (#35522) + * Expose content_version for optimistic locking on issue and PR edits (#37035) + * Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) +* BUGFIXES + * Fix API not persisting pull request unit config when has_pull_requests is not set (#36718) + * Rename CSS variables and improve colorblind themes (#36353) + * Hide `add-matcher` and `remove-matcher` from actions job logs (#36520) + * Prevent navigation keys from triggering actions during IME composition (#36540) + * Fix vertical alignment of `.commit-sign-badge` children (#36570) + * Fix duplicate startup warnings in admin panel (#36641) + * Fix CODEOWNERS review request attribution using comment metadata (#36348) + * Fix HTML tags appearing in wiki table of contents (#36284) + * Fix various bugs (#37096) + * Fix various legacy problems (#37092) + * Fix RPM Registry 404 when package name contains 'package' (#37087) + * Merge some standalone Vite entries into index.js (#37085) + * Fix various problems (#37077) + * Fix issue label deletion with Actions tokens (#37013) + * Hide delete branch or tag buttons in mirror or archived repositories. (#37006) + * Fix org contact email not clearable once set (#36975) + * Fix a bug when forking a repository in an organization (#36950) + * Preserve sort order of exclusive labels from template repo (#36931) + * Make container registry support Apple Container (basic auth) (#36920) + * Fix the wrong push commits in the pull request when force push (#36914) + * Add class "list-header-filters" to the div for projects (#36889) + * Fix dbfs error handling (#36844) + * Fix incorrect viewed files counter if reverted change was viewed (#36819) + * Refactor avatar package, support default avatar fallback (#36788) + * Fix README symlink resolution in subdirectories like .github (#36775) + * Fix CSS stacking context issue in actions log (#36749) + * Add gpg signing for merge rebase and update by rebase (#36701) + * Delete non-exist branch should return 404 (#36694) + * Fix `TestActionsCollaborativeOwner` (#36657) + * Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646) + * Fix linguist-detectable attribute being ignored for configuration files (#36640) + * Fix state desync in ComboMarkdownEditor (#36625) + * Unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597) + * Pull Request Pusher should be the author of the merge (#36581) + * Fix various version parsing problems (#36553) + * Fix highlight diff result (#36539) + * Fix mirror sync parser and fix mirror messages (#36504) + * Fix bug when list pull request commits (#36485) + * Fix various bugs (#36446) + * Fix issue filter menu layout (#36426) + * Restrict branch naming when new change matches with protection rules (#36405) + * Fix link/origin referrer and login redirect (#36279) + * Generate IDs for HTML headings without id attribute (#36233) + * Use a migration test instead of a wrong test which populated the meta test repositories and fix a migration bug (#36160) + * Fix issue close timeline icon (#36138) + * Fix diff blob excerpt expansion (#35922) + * Fix external render (#35727) + * Fix review request webhook bug (#35339) (#35723) + * Fix shutdown waitgroup panic (#35676) + * Cleanup ActionRun creation (#35624) + * Fix possible bug when migrating issues/pull requests (#33487) + * Various fixes (#36697) + * Apply notify/register mail flags during install load (#37120) + * Repair duration display for bad stopped timestamps (#37121) + * Fix(upgrade.sh): use HTTPS for GPG key import and restore SELinux context after upgrade (#36930) + * Fix various trivial problems (#36921) + * Fix various trivial problems (#36953) + * Fix NuGet package upload error handling (#37074) + * Fix CodeQL code scanning alerts (#36858) + * Refactor issue sidebar and fix various problems (#37045) + * Fix various problems (#37029) + * Fix relative-time RangeError (#37021) + * Fix chroma lexer mapping (#36629) + * Fix typos and grammar in English locale (#36751) + * Fix milestone/project text overflow in issue sidebar (#36741) + * Fix `no-content` message not rendering after comment edit (#36733) + * Fix theme loading in development (#36605) + * Fix workflow run jobs API returning null steps (#36603) + * Fix timeline event layout overflow with long content (#36595) + * Fix minor UI issues in runner edit page (#36590) + * Fix incorrect vendored detections (#36508) + * Fix editorconfig not respected in PR Conversation view (#36492) + * Don't create self-references in merged PRs (#36490) + * Fix potential incorrect runID in run status update (#36437) + * Fix file-tree ui error when adding files to repo without commits (#36312) + * Improve image captcha contrast for dark mode (#36265) + * Fix panic in blame view when a file has only a single commit (#36230) + * Fix spelling error in migrate-storage cmd utility (#36226) + * Fix code highlighting on blame page (#36157) + * Fix nilnil in onedev downloader (#36154) + * Fix actions lint (#36029) + * Fix oauth2 session gob register (#36017) + * Fix Arch repo pacman.conf snippet (#35825) + * Fix a number of `strictNullChecks`-related issues (#35795) + * Fix URLJoin, markup render link reoslving, sign-in/up/linkaccount page common data (#36861) + * Hide delete directory button for mirror or archive repository and disable the menu item if user have no permission (#36384) + * Update message severity colors, fix navbar double border (#37019) + * Inline and lazy-load EasyMDE CSS, fix border colors (#36714) + * Closed milestones with no issues now show as 100% completed (#36220) + * Add test for ExtendCommentTreePathLength migration and fix bugs (#35791) + * Only turn links to current instance into hash links (#36237) + * Fix typos in code comments: doesnt, dont, wont (#36890) +* REFACTOR + * Replace Monaco with CodeMirror (#36764) + * Replace CSRF cookie with `CrossOriginProtection` (#36183) + * Replace index with id in actions routes (#36842) + * Remove unnecessary function parameter (#35765) + * Move jobparser from act repository to Gitea (#36699) + * Refactor compare router param parse (#36105) + * Optimize 'refreshAccesses' to perform update without removing then adding (#35702) + * Clean up checkbox cursor styles (#37016) + * Remove undocumented support of signing key in the repository git configuration file (#36143) + * Switch `cmd/` to use constructor functions. (#36962) + * Use `relative-time` to render absolute dates (#36238) + * Some refactors about GetMergeBase (#36186) + * Some small refactors (#36163) + * Use gitRepo as parameter instead of repopath when invoking sign functions (#36162) + * Move blame to gitrepo (#36161) + * Move some functions to gitrepo package to reduce RepoPath reference directly (#36126) + * Use gitrepo's clone and push when possible (#36093) + * Remove mermaid margin workaround (#35732) + * Move some functions to gitrepo package (#35543) + * Move GetDiverging functions to gitrepo (#35524) + * Use global lock instead of status pool for cron lock (#35507) + * Use explicit mux instead of DefaultServeMux (#36276) + * Use gitrepo's push function (#36245) + * Pass request context to generateAdditionalHeadersForIssue (#36274) + * Move assign project when creating pull request to the same database transaction (#36244) + * Move catfile batch to a sub package of git module (#36232) + * Use gitrepo.Repository instead of wikipath (#35398) + * Use experimental go json v2 library (#35392) + * Refactor template render (#36438) + * Refactor GetRepoRawDiffForFile to avoid unnecessary pipe or goroutine (#36434) + * Refactor text utility classes to Tailwind CSS (#36703) + * Refactor git command stdio pipe (#36422) + * Refactor git command context & pipeline (#36406) + * Refactor git command stdio pipe (#36393) + * Remove unused functions (#36672) + * Refactor Actions Token Access (#35688) + * Move commit related functions to gitrepo package (#35600) + * Move archive function to repo_model and gitrepo (#35514) + * Move some functions to gitrepo package (#35503) + * Use git model to detect whether branch exist instead of gitrepo method (#35459) + * Some refactor for repo path (#36251) + * Extract helper functions from SearchIssues (#36158) + * Refactor merge conan and container auth preserve actions taskID (#36560) + * Refactor Nuget Auth to reuse Basic Auth Token Validation (#36558) + * Refactor ActionsTaskID (#36503) + * Refactor auth middleware (#36848) + * Refactor code render and render control chars (#37078) + * Clean up AppURL, remove legacy origin-url webcomponent (#37090) + * Remove `util.URLJoin` and replace all callers with direct path concatenation (#36867) + * Replace legacy tw-flex utility classes with flex-text-block/inline (#36778) + * Mark unused&immature activitypub as "not implemented" (#36789) +* TESTING + * Add e2e tests for server push events (#36879) + * Rework e2e tests (#36634) + * Add e2e reaction test, improve accessibility, enable parallel testing (#37081) + * Increase e2e test timeouts on CI to fix flaky tests (#37053) +* BUILD + * Convert locale files from ini to json format (#35489) + * Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180) + * Port away from `flake-utils` (#35675) + * Remove nolint (#36252) + * Update the Unlicense copy to latest version (#36636) + * Update to go 1.26.0 and golangci-lint 2.9.0 (#36588) + * Replace `google/go-licenses` with custom generation (#36575) + * Update go dependencies (#36548) + * Bump appleboy/git-push-action from 1.0.0 to 1.2.0 (#36306) + * Remove fomantic form module (#36222) + * Bump setup-node to v6, re-enable cache (#36207) + * Bump crowdin/github-action from 1 to 2 (#36204) + * Revert "Bump alpine to 3.23 (#36185)" (#36202) + * Update chroma to v2.21.1 (#36201) + * Bump astral-sh/setup-uv from 6 to 7 (#36198) + * Bump docker/build-push-action from 5 to 6 (#36197) + * Bump aws-actions/configure-aws-credentials from 4 to 5 (#36196) + * Bump dev-hanz-ops/install-gh-cli-action from 0.1.0 to 0.2.1 (#36195) + * Add JSON linting (#36192) + * Enable dependabot for actions (#36191) + * Bump alpine to 3.23 (#36185) + * Update chroma to v2.21.0 (#36171) + * Update JS deps and eslint enhancements (#36147) + * Update JS deps (#36091) + * update golangci-lint to v2.7.0 (#36079) + * Update JS deps, fix deprecations (#36040) + * Update JS deps (#35978) + * Add toolchain directive to go.mod (#35901) + * Move `gitea-vet` to use `go tool` (#35878) + * Update to go 1.25.4 (#35877) + * Enable TypeScript `strictNullChecks` (#35843) + * Enable `vue/require-typed-ref` eslint rule (#35764) + * Update JS dependencies (#35759) + * Move `codeformat` folder to tools (#35758) + * Update dependencies (#35733) + * Bump happy-dom from 20.0.0 to 20.0.2 (#35677) + * Bump setup-go to v6 (#35660) + * Update JS deps, misc tweaks (#35643) + * Bump happy-dom from 19.0.2 to 20.0.0 (#35625) + * Use bundled version of spectral (#35573) + * Update JS and PY deps (#35565) + * Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557) + * Migrate from webpack to vite (#37002) + * Update JS dependencies and misc tweaks (#37064) + * Update to eslint 10 (#36925) + * Optimize Docker build with dependency layer caching (#36864) + * Update JS deps (#36850) + * Update tool dependencies and fix new lint issues (#36702) + * Remove redundant linter rules (#36658) + * Move Fomantic dropdown CSS to custom module (#36530) + * Remove and forbid `@ts-expect-error` (#36513) + * Refactor git command stderr handling (#36402) + * Enable gocheckcompilerdirectives linter (#36156) + * Replace `lint-go-gopls` with additional `govet` linters (#36028) + * Update golangci-lint to v2.6.0 (#35801) + * Misc tool tweaks (#35734) + * Add cache to container build (#35697) + * Upgrade vite (#37126) + * Update `setup-uv` to v8.0.0 (#37101) + * Upgrade `go-git` to v5.17.2 and related dependencies (#37060) + * Raise minimum Node.js version to 22.18.0 (#37058) + * Upgrade `golang.org/x/image` to v0.38.0 (#37054) + * Update minimum go version to 1.26.1, golangci-lint to 2.11.2, fix test style (#36876) + * Enable eslint concurrency (#36878) + * Vendor relative-time-element as local web component (#36853) + * Update material-icon-theme v5.32.0 (#36832) + * Update Go dependencies (#36781) + * Upgrade minimatch (#36760) + * Remove i18n backport tool at the moment because of translation format changed (#36643) + * Update emoji data for Unicode 16 (#36596) + * Update JS dependencies, adjust webpack config, misc fixes (#36431) + * Update material-icon-theme to v5.31.0 (#36427) + * Update JS and PY deps (#36383) + * Bump alpine to 3.23, add platforms to `docker-dryrun` (#36379) + * Update JS deps (#36354) + * Update goldmark to v1.7.16 (#36343) + * Update chroma to v2.22.0 (#36342) +* DOCS + * Update AI Contribution Policy (#37022) + * Update AGENTS.md with additional guidelines (#37018) + * Add missing cron tasks to example ini (#37012) + * Add AI Contribution Policy to CONTRIBUTING.md (#36651) + * Minor punctuation improvement in CONTRIBUTING.md (#36291) + * Add documentation for markdown anchor post-processing (#36443) +* MISC + * Correct spelling (#36783) + * Update Nix flake (#37110) + * Update Nix flake (#37024) + * Add valid github scopes (#36977) + * Update Nix flake (#36943) + * Update Nix flake (#36902) + * Update Nix flake (#36857) + * Update Nix flake (#36787) + ## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/v1.25.5) - 2026-03-10 * SECURITY From 6f9fa5578537684795b6ee2046594d97527cb18f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:26:21 +0800 Subject: [PATCH 002/126] models/fixtures: add "DO NOT add more test data" comment to all yml fixture files (#37150) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com> --- models/fixtures/access.yml | 2 ++ models/fixtures/access_token.yml | 2 ++ models/fixtures/action.yml | 2 ++ models/fixtures/action_artifact.yml | 2 ++ models/fixtures/action_run.yml | 2 ++ models/fixtures/action_run_job.yml | 2 ++ models/fixtures/action_runner.yml | 2 ++ models/fixtures/action_runner_token.yml | 2 ++ models/fixtures/action_task.yml | 2 ++ models/fixtures/action_task_output.yml | 2 ++ models/fixtures/attachment.yml | 2 ++ models/fixtures/badge.yml | 2 ++ models/fixtures/branch.yml | 2 ++ models/fixtures/collaboration.yml | 2 ++ models/fixtures/comment.yml | 2 ++ models/fixtures/commit_status.yml | 2 ++ models/fixtures/commit_status_index.yml | 2 ++ models/fixtures/deploy_key.yml | 2 ++ models/fixtures/email_address.yml | 2 ++ models/fixtures/external_login_user.yml | 2 ++ models/fixtures/follow.yml | 2 ++ models/fixtures/gpg_key.yml | 2 ++ models/fixtures/gpg_key_import.yml | 2 ++ models/fixtures/hook_task.yml | 2 ++ models/fixtures/issue.yml | 2 ++ models/fixtures/issue_assignees.yml | 2 ++ models/fixtures/issue_index.yml | 2 ++ models/fixtures/issue_label.yml | 2 ++ models/fixtures/issue_pin.yml | 2 ++ models/fixtures/issue_user.yml | 2 ++ models/fixtures/issue_watch.yml | 2 ++ models/fixtures/label.yml | 2 ++ models/fixtures/lfs_meta_object.yml | 2 ++ models/fixtures/login_source.yml | 2 ++ models/fixtures/milestone.yml | 2 ++ models/fixtures/mirror.yml | 2 ++ models/fixtures/notice.yml | 2 ++ models/fixtures/notification.yml | 2 ++ models/fixtures/oauth2_application.yml | 2 ++ models/fixtures/oauth2_authorization_code.yml | 2 ++ models/fixtures/oauth2_grant.yml | 2 ++ models/fixtures/org_user.yml | 2 ++ models/fixtures/project.yml | 2 ++ models/fixtures/project_board.yml | 2 ++ models/fixtures/project_issue.yml | 2 ++ models/fixtures/protected_branch.yml | 2 ++ models/fixtures/protected_tag.yml | 2 ++ models/fixtures/public_key.yml | 2 ++ models/fixtures/pull_request.yml | 2 ++ models/fixtures/reaction.yml | 2 ++ models/fixtures/release.yml | 2 ++ models/fixtures/renamed_branch.yml | 2 ++ models/fixtures/repo_archiver.yml | 2 ++ models/fixtures/repo_indexer_status.yml | 2 ++ models/fixtures/repo_license.yml | 2 ++ models/fixtures/repo_redirect.yml | 2 ++ models/fixtures/repo_topic.yml | 2 ++ models/fixtures/repo_transfer.yml | 2 ++ models/fixtures/repo_unit.yml | 2 ++ models/fixtures/repository.yml | 2 ++ models/fixtures/review.yml | 2 ++ models/fixtures/star.yml | 2 ++ models/fixtures/stopwatch.yml | 2 ++ models/fixtures/system_setting.yml | 2 ++ models/fixtures/team.yml | 2 ++ models/fixtures/team_repo.yml | 2 ++ models/fixtures/team_unit.yml | 2 ++ models/fixtures/team_user.yml | 2 ++ models/fixtures/topic.yml | 2 ++ models/fixtures/tracked_time.yml | 2 ++ models/fixtures/two_factor.yml | 2 ++ models/fixtures/user.yml | 2 ++ models/fixtures/user_blocking.yml | 2 ++ models/fixtures/user_open_id.yml | 2 ++ models/fixtures/user_redirect.yml | 2 ++ models/fixtures/watch.yml | 2 ++ models/fixtures/webauthn_credential.yml | 2 ++ models/fixtures/webhook.yml | 2 ++ 78 files changed, 156 insertions(+) diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 596046e9502..c0aa06c86d2 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -177,3 +177,5 @@ user_id: 40 repo_id: 1 mode: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/access_token.yml b/models/fixtures/access_token.yml index 0744255f664..d85d785da54 100644 --- a/models/fixtures/access_token.yml +++ b/models/fixtures/access_token.yml @@ -31,3 +31,5 @@ created_unix: 946687980 updated_unix: 946687980 # commented out tokens so you can see what they are in plaintext + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index af9ce93ba5c..32f2ae87642 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -73,3 +73,5 @@ is_private: false created_unix: 1680454039 content: '4|' # issueId 5 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_artifact.yml b/models/fixtures/action_artifact.yml index a25dfc205c4..5fcc70aa536 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -177,3 +177,5 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index ac5e8303c35..63fd011e65f 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -160,3 +160,5 @@ updated: 1683636626 need_approval: 0 approved_by: 0 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 04799b73ca0..82023f122a1 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -144,3 +144,5 @@ status: 3 started: 1683636528 stopped: 1683636626 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml index ecb72140065..110ff627a2a 100644 --- a/models/fixtures/action_runner.yml +++ b/models/fixtures/action_runner.yml @@ -49,3 +49,5 @@ repo_id: 0 description: "This runner is going to be deleted" agent_labels: '["runner_to_be_deleted","linux"]' + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_runner_token.yml b/models/fixtures/action_runner_token.yml index 6520b7f6fbe..3af8a28c9c1 100644 --- a/models/fixtures/action_runner_token.yml +++ b/models/fixtures/action_runner_token.yml @@ -33,3 +33,5 @@ is_active: 1 created: 1695617751 updated: 1695617751 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index e1bc588dc59..52a37da7305 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -197,3 +197,5 @@ log_length: 707 log_size: 90179 log_expired: 0 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_task_output.yml b/models/fixtures/action_task_output.yml index 314e9f7115b..741b193b13d 100644 --- a/models/fixtures/action_task_output.yml +++ b/models/fixtures/action_task_output.yml @@ -18,3 +18,5 @@ task_id: 50 output_key: output_b output_value: bbb + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/attachment.yml b/models/fixtures/attachment.yml index 570d4a27daf..0870895a715 100644 --- a/models/fixtures/attachment.yml +++ b/models/fixtures/attachment.yml @@ -166,3 +166,5 @@ download_count: 0 size: 0 created_unix: 946684800 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/badge.yml b/models/fixtures/badge.yml index 438cd0ca5d4..72550be79aa 100644 --- a/models/fixtures/badge.yml +++ b/models/fixtures/badge.yml @@ -3,3 +3,5 @@ slug: badge1 description: just a test badge image_url: badge1.png + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index a17999091e0..e09022a6142 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -249,3 +249,5 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml index 4c3ac367f6b..2de34488090 100644 --- a/models/fixtures/collaboration.yml +++ b/models/fixtures/collaboration.yml @@ -63,3 +63,5 @@ repo_id: 32 user_id: 10 mode: 2 # write + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml index 8fde386e226..6930dbb58e7 100644 --- a/models/fixtures/comment.yml +++ b/models/fixtures/comment.yml @@ -102,3 +102,5 @@ review_id: 22 assignee_id: 5 created_unix: 946684817 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/commit_status.yml b/models/fixtures/commit_status.yml index 87c652e53ab..df3bc425057 100644 --- a/models/fixtures/commit_status.yml +++ b/models/fixtures/commit_status.yml @@ -57,3 +57,5 @@ context: deploy/awesomeness context_hash: ae9547713a6665fc4261d0756904932085a41cf2 creator_id: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/commit_status_index.yml b/models/fixtures/commit_status_index.yml index f63343b042a..9157911bac2 100644 --- a/models/fixtures/commit_status_index.yml +++ b/models/fixtures/commit_status_index.yml @@ -3,3 +3,5 @@ repo_id: 1 sha: "1234123412341234123412341234123412341234" max_index: 5 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/deploy_key.yml b/models/fixtures/deploy_key.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/deploy_key.yml +++ b/models/fixtures/deploy_key.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml index 0f6bd9ee6df..b3c78120af1 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -317,3 +317,5 @@ lower_email: user40@example.com is_activated: true is_primary: true + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/external_login_user.yml b/models/fixtures/external_login_user.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/external_login_user.yml +++ b/models/fixtures/external_login_user.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/follow.yml b/models/fixtures/follow.yml index b8d35828bf1..f8de0e039dc 100644 --- a/models/fixtures/follow.yml +++ b/models/fixtures/follow.yml @@ -17,3 +17,5 @@ id: 4 user_id: 31 follow_id: 33 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/gpg_key.yml b/models/fixtures/gpg_key.yml index 2d54313fdf1..3d2895dc1ca 100644 --- a/models/fixtures/gpg_key.yml +++ b/models/fixtures/gpg_key.yml @@ -21,3 +21,5 @@ can_encrypt_comms: true can_encrypt_storage: true can_certify: true + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/gpg_key_import.yml b/models/fixtures/gpg_key_import.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/gpg_key_import.yml +++ b/models/fixtures/gpg_key_import.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index 6023719b1ee..e19eeb03687 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -35,3 +35,5 @@ "X-Head": "42" } } + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index ca5b1c6cd1d..6da3c9e279a 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -372,3 +372,5 @@ created_unix: 1707270422 updated_unix: 1707270422 is_locked: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_assignees.yml b/models/fixtures/issue_assignees.yml index c40ecad6764..a0bf422dc95 100644 --- a/models/fixtures/issue_assignees.yml +++ b/models/fixtures/issue_assignees.yml @@ -18,3 +18,5 @@ id: 5 assignee_id: 10 issue_id: 6 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_index.yml b/models/fixtures/issue_index.yml index 5aabc08e388..51100684471 100644 --- a/models/fixtures/issue_index.yml +++ b/models/fixtures/issue_index.yml @@ -33,3 +33,5 @@ - group_id: 51 max_index: 1 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_label.yml b/models/fixtures/issue_label.yml index f4ecb1f9232..3754bd78286 100644 --- a/models/fixtures/issue_label.yml +++ b/models/fixtures/issue_label.yml @@ -17,3 +17,5 @@ id: 4 issue_id: 2 label_id: 4 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_pin.yml b/models/fixtures/issue_pin.yml index 14b7a72d847..dc3d1c60d99 100644 --- a/models/fixtures/issue_pin.yml +++ b/models/fixtures/issue_pin.yml @@ -4,3 +4,5 @@ issue_id: 4 is_pull: false pin_order: 1 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_user.yml b/models/fixtures/issue_user.yml index 64824316ea2..756cb7be4b4 100644 --- a/models/fixtures/issue_user.yml +++ b/models/fixtures/issue_user.yml @@ -18,3 +18,5 @@ issue_id: 1 is_read: false is_mentioned: true + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/issue_watch.yml b/models/fixtures/issue_watch.yml index 4bc3ff1b8b9..edc1041abc3 100644 --- a/models/fixtures/issue_watch.yml +++ b/models/fixtures/issue_watch.yml @@ -29,3 +29,5 @@ is_watching: false created_unix: 946684800 updated_unix: 946684800 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml index acfac749686..064f790a771 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -107,3 +107,5 @@ num_issues: 0 num_closed_issues: 0 archived_unix: 0 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/lfs_meta_object.yml b/models/fixtures/lfs_meta_object.yml index ae5ae565425..0fe430f147b 100644 --- a/models/fixtures/lfs_meta_object.yml +++ b/models/fixtures/lfs_meta_object.yml @@ -30,3 +30,5 @@ size: 25 repository_id: 54 created_unix: 1671607299 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/login_source.yml b/models/fixtures/login_source.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/login_source.yml +++ b/models/fixtures/login_source.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/milestone.yml b/models/fixtures/milestone.yml index 87c30cc96c4..c4ed2aea780 100644 --- a/models/fixtures/milestone.yml +++ b/models/fixtures/milestone.yml @@ -52,3 +52,5 @@ num_closed_issues: 0 completeness: 0 deadline_unix: 253370764800 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/mirror.yml b/models/fixtures/mirror.yml index 97bc4ae60dd..1f690654cb3 100644 --- a/models/fixtures/mirror.yml +++ b/models/fixtures/mirror.yml @@ -47,3 +47,5 @@ next_update_unix: 0 lfs_enabled: false lfs_endpoint: "" + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/notice.yml b/models/fixtures/notice.yml index af08f07bfa1..17e26d7634f 100644 --- a/models/fixtures/notice.yml +++ b/models/fixtures/notice.yml @@ -12,3 +12,5 @@ id: 3 type: 1 # NoticeRepository description: description3 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/notification.yml b/models/fixtures/notification.yml index bd279d4bb28..dcfbeada39d 100644 --- a/models/fixtures/notification.yml +++ b/models/fixtures/notification.yml @@ -52,3 +52,5 @@ issue_id: 4 created_unix: 946688800 updated_unix: 946688820 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/oauth2_application.yml b/models/fixtures/oauth2_application.yml index 2f38cb58b61..5b3b00b16e8 100644 --- a/models/fixtures/oauth2_application.yml +++ b/models/fixtures/oauth2_application.yml @@ -18,3 +18,5 @@ created_unix: 1546869730 updated_unix: 1546869730 confidential_client: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/oauth2_authorization_code.yml b/models/fixtures/oauth2_authorization_code.yml index d29502164e6..64d8b175077 100644 --- a/models/fixtures/oauth2_authorization_code.yml +++ b/models/fixtures/oauth2_authorization_code.yml @@ -13,3 +13,5 @@ code_challenge_method: "S256" redirect_uri: "http://127.0.0.1/" valid_until: 3546869730 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/oauth2_grant.yml b/models/fixtures/oauth2_grant.yml index e63286878b2..54f4e45e62e 100644 --- a/models/fixtures/oauth2_grant.yml +++ b/models/fixtures/oauth2_grant.yml @@ -29,3 +29,5 @@ scope: "whatever" created_unix: 1546869730 updated_unix: 1546869730 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml index 73a3e9dba9b..dc35701182a 100644 --- a/models/fixtures/org_user.yml +++ b/models/fixtures/org_user.yml @@ -135,3 +135,5 @@ uid: 20 org_id: 17 is_public: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml index 44d87bce046..e61781fd7f1 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/project.yml @@ -69,3 +69,5 @@ type: 2 created_unix: 1688973000 updated_unix: 1688973000 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml index 3293dea6edf..91d21981714 100644 --- a/models/fixtures/project_board.yml +++ b/models/fixtures/project_board.yml @@ -75,3 +75,5 @@ default: true created_unix: 1588117528 updated_unix: 1588117528 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/project_issue.yml b/models/fixtures/project_issue.yml index b1af05908aa..7d9d5118820 100644 --- a/models/fixtures/project_issue.yml +++ b/models/fixtures/project_issue.yml @@ -21,3 +21,5 @@ issue_id: 5 project_id: 1 project_board_id: 3 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/protected_branch.yml b/models/fixtures/protected_branch.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/protected_branch.yml +++ b/models/fixtures/protected_branch.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/protected_tag.yml b/models/fixtures/protected_tag.yml index 1944e7bd84c..cb83439645e 100644 --- a/models/fixtures/protected_tag.yml +++ b/models/fixtures/protected_tag.yml @@ -22,3 +22,5 @@ allowlist_team_i_ds: "[]" created_unix: 1715596037 updated_unix: 1715596037 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/public_key.yml b/models/fixtures/public_key.yml index 856b0e3fb29..756bca86b61 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -10,3 +10,5 @@ updated_unix: 1565224552 login_source_id: 0 verified: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index 9a16316e5a2..b8da7fe0812 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -117,3 +117,5 @@ index: 1 head_repo_id: 61 base_repo_id: 61 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/reaction.yml b/models/fixtures/reaction.yml index ee571a73a48..9effcc98f7c 100644 --- a/models/fixtures/reaction.yml +++ b/models/fixtures/reaction.yml @@ -37,3 +37,5 @@ comment_id: 2 user_id: 1 created_unix: 1573248005 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/release.yml b/models/fixtures/release.yml index 372a79509f0..be44e331ec0 100644 --- a/models/fixtures/release.yml +++ b/models/fixtures/release.yml @@ -150,3 +150,5 @@ is_prerelease: false is_tag: false created_unix: 946684803 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/renamed_branch.yml b/models/fixtures/renamed_branch.yml index efa5130a2b9..9055080ff85 100644 --- a/models/fixtures/renamed_branch.yml +++ b/models/fixtures/renamed_branch.yml @@ -3,3 +3,5 @@ repo_id: 1 from: dev to: master + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_archiver.yml b/models/fixtures/repo_archiver.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/repo_archiver.yml +++ b/models/fixtures/repo_archiver.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_indexer_status.yml b/models/fixtures/repo_indexer_status.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/repo_indexer_status.yml +++ b/models/fixtures/repo_indexer_status.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_license.yml b/models/fixtures/repo_license.yml index ca780a73aa0..0d1b2f00980 100644 --- a/models/fixtures/repo_license.yml +++ b/models/fixtures/repo_license.yml @@ -1 +1,3 @@ [] # empty + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_redirect.yml b/models/fixtures/repo_redirect.yml index 8850c8d780b..60459d638d6 100644 --- a/models/fixtures/repo_redirect.yml +++ b/models/fixtures/repo_redirect.yml @@ -3,3 +3,5 @@ owner_id: 2 lower_name: oldrepo1 redirect_repo_id: 1 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_topic.yml b/models/fixtures/repo_topic.yml index f166faccc1d..3a4e7edaa96 100644 --- a/models/fixtures/repo_topic.yml +++ b/models/fixtures/repo_topic.yml @@ -25,3 +25,5 @@ - repo_id: 2 topic_id: 6 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_transfer.yml b/models/fixtures/repo_transfer.yml index b12e6b207f4..0a26eaec8ee 100644 --- a/models/fixtures/repo_transfer.yml +++ b/models/fixtures/repo_transfer.yml @@ -29,3 +29,5 @@ repo_id: 5 created_unix: 1553610671 updated_unix: 1553610671 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 4c3e37500f0..69f083ccd75 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -747,3 +747,5 @@ type: 10 config: "{}" created_unix: 946684810 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index dfa514db37f..d8eb7962072 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1788,3 +1788,5 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 5b8bbceca9e..abcc9d3bb27 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -214,3 +214,5 @@ original_author_id: 0 updated_unix: 946684817 created_unix: 946684817 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/star.yml b/models/fixtures/star.yml index 39b51b3736f..96db493448b 100644 --- a/models/fixtures/star.yml +++ b/models/fixtures/star.yml @@ -17,3 +17,5 @@ id: 4 uid: 10 repo_id: 32 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/stopwatch.yml b/models/fixtures/stopwatch.yml index b7919d6fbbd..bbb3852069c 100644 --- a/models/fixtures/stopwatch.yml +++ b/models/fixtures/stopwatch.yml @@ -9,3 +9,5 @@ user_id: 2 issue_id: 2 created_unix: 1500988002 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/system_setting.yml b/models/fixtures/system_setting.yml index dcad176c899..ae612fa9f93 100644 --- a/models/fixtures/system_setting.yml +++ b/models/fixtures/system_setting.yml @@ -13,3 +13,5 @@ version: 1 created: 1653533198 updated: 1653533198 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index b549d0589bc..3c2cb7802a3 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -261,3 +261,5 @@ num_members: 1 includes_all_repositories: true can_create_org_repo: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index a29078107ee..c91f74467a8 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -75,3 +75,5 @@ org_id: 41 team_id: 22 repo_id: 61 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml index 110019eee30..bb950870094 100644 --- a/models/fixtures/team_unit.yml +++ b/models/fixtures/team_unit.yml @@ -340,3 +340,5 @@ team_id: 24 type: 1 # code access_mode: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml index 6b2d153278a..4cceffee6a9 100644 --- a/models/fixtures/team_user.yml +++ b/models/fixtures/team_user.yml @@ -159,3 +159,5 @@ org_id: 35 team_id: 24 uid: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/topic.yml b/models/fixtures/topic.yml index 055addf510e..97ac821fc11 100644 --- a/models/fixtures/topic.yml +++ b/models/fixtures/topic.yml @@ -27,3 +27,5 @@ id: 6 name: topicname2 repo_count: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/tracked_time.yml b/models/fixtures/tracked_time.yml index 768af38d9e2..7c2145a6d86 100644 --- a/models/fixtures/tracked_time.yml +++ b/models/fixtures/tracked_time.yml @@ -69,3 +69,5 @@ time: 100000 created_unix: 947688815 deleted: true + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/two_factor.yml b/models/fixtures/two_factor.yml index d8cb85274b6..13b421b7b49 100644 --- a/models/fixtures/two_factor.yml +++ b/models/fixtures/two_factor.yml @@ -7,3 +7,5 @@ last_used_passcode: created_unix: 1564253724 updated_unix: 1564253724 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 976a236011c..1a33947e047 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -1556,3 +1556,5 @@ repo_admin_change_team_access: false theme: "" keep_activity_private: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/user_blocking.yml b/models/fixtures/user_blocking.yml index 2ec9d99df52..c1714e40c82 100644 --- a/models/fixtures/user_blocking.yml +++ b/models/fixtures/user_blocking.yml @@ -17,3 +17,5 @@ id: 4 blocker_id: 50 blockee_id: 34 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/user_open_id.yml b/models/fixtures/user_open_id.yml index d3a367b99df..72fe7e34b8c 100644 --- a/models/fixtures/user_open_id.yml +++ b/models/fixtures/user_open_id.yml @@ -15,3 +15,5 @@ uid: 2 uri: https://domain1.tld/user2/ show: true + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/user_redirect.yml b/models/fixtures/user_redirect.yml index c668cb6c3b7..1b0f7a94942 100644 --- a/models/fixtures/user_redirect.yml +++ b/models/fixtures/user_redirect.yml @@ -6,3 +6,5 @@ id: 2 lower_name: olduser2 redirect_user_id: 2 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index 18bcd2ed2b4..b7ee121f24d 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -39,3 +39,5 @@ user_id: 10 repo_id: 32 mode: 1 # normal + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/webauthn_credential.yml b/models/fixtures/webauthn_credential.yml index bc43127fcd4..d188a4d76a8 100644 --- a/models/fixtures/webauthn_credential.yml +++ b/models/fixtures/webauthn_credential.yml @@ -7,3 +7,5 @@ clone_warning: false created_unix: 946684800 updated_unix: 946684800 + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/webhook.yml b/models/fixtures/webhook.yml index ec282914b81..f372aaaecb4 100644 --- a/models/fixtures/webhook.yml +++ b/models/fixtures/webhook.yml @@ -50,3 +50,5 @@ events: '{"push_only":true,"branch_filter":"{master,feature*}"}' is_active: true is_system_webhook: false + +# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly From 0914a44a9b77c253811a36911fe67a1abd2bd8b3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 9 Apr 2026 10:36:08 +0200 Subject: [PATCH 003/126] Clean up and improve non-gitea js error filter (#37148) 1. Filter out errors that contain `chrome-extension://` etc protocols 2. Extract filtering into its own function and test it 3. Fix the `window.config.assetUrlPrefix` mock, guaranteed to end with `/assets` 4. Remove useless `??` and `?.` for properties that always exist Co-authored-by: Claude (Opus 4.6) --- web_src/js/modules/errors.test.ts | 17 ++++++++++++++++- web_src/js/modules/errors.ts | 21 ++++++++++++++------- web_src/js/vitest.setup.ts | 2 +- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/web_src/js/modules/errors.test.ts b/web_src/js/modules/errors.test.ts index 8ac0e262b24..efa114a88d0 100644 --- a/web_src/js/modules/errors.test.ts +++ b/web_src/js/modules/errors.test.ts @@ -1,4 +1,19 @@ -import {showGlobalErrorMessage} from './errors.ts'; +import {isGiteaError, showGlobalErrorMessage} from './errors.ts'; + +test('isGiteaError', () => { + expect(isGiteaError('', '')).toBe(true); + expect(isGiteaError('moz-extension://abc/content.js', '')).toBe(false); + expect(isGiteaError('safari-extension://abc/content.js', '')).toBe(false); + expect(isGiteaError('safari-web-extension://abc/content.js', '')).toBe(false); + expect(isGiteaError('chrome-extension://abc/content.js', '')).toBe(false); + expect(isGiteaError('https://other-site.com/script.js', '')).toBe(false); + expect(isGiteaError('http://localhost:3000/some/page', '')).toBe(true); + expect(isGiteaError('http://localhost:3000/assets/js/index.abc123.js', '')).toBe(true); + expect(isGiteaError('', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false); + expect(isGiteaError('', `Error\n at https://other-site.com/script.js:1:1`)).toBe(false); + expect(isGiteaError('', `Error\n at http://localhost:3000/assets/js/index.abc123.js:1:1`)).toBe(true); + expect(isGiteaError('http://localhost:3000/assets/js/index.js', `Error\n at chrome-extension://abc/content.js:1:1`)).toBe(false); +}); test('showGlobalErrorMessage', () => { document.body.innerHTML = '
'; diff --git a/web_src/js/modules/errors.ts b/web_src/js/modules/errors.ts index 882da36977d..ddda0ebe42b 100644 --- a/web_src/js/modules/errors.ts +++ b/web_src/js/modules/errors.ts @@ -23,11 +23,19 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { msgContainer.prepend(msgDiv); } +// Detect whether an error originated from Gitea's own scripts, not from +// browser extensions or other external scripts. +const extensionRe = /(chrome|moz|safari(-web)?)-extension:\/\//; +export function isGiteaError(filename: string, stack: string): boolean { + if (extensionRe.test(filename) || extensionRe.test(stack)) return false; + const assetBaseUrl = new URL(`${window.config.assetUrlPrefix}/`, window.location.origin).href; + if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false; + if (stack && !stack.includes(assetBaseUrl)) return false; + return true; +} + export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { const err = error ?? reason; - const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); - const {runModeIsProd} = window.config ?? {}; - // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a // non-critical event from the browser. We log them but don't show them to users. Examples: // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors @@ -35,12 +43,11 @@ export function processWindowErrorEvent({error, reason, message, type, filename, // - https://github.com/go-gitea/gitea/issues/20240 if (!err) { if (message) console.error(new Error(message)); - if (runModeIsProd) return; + if (window.config.runModeIsProd) return; } - // If the error stack trace does not include the base URL of our script assets, it likely came - // from a browser extension or inline script. Do not show such errors in production. - if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) return; + // Filter out errors from browser extensions or other non-Gitea scripts. + if (!isGiteaError(filename ?? '', err?.stack ?? '')) return; let msg = err?.message ?? message; if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index 5623075a277..a08325bcba3 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -13,7 +13,7 @@ await import('./globals.ts'); window.config = { appUrl: 'http://localhost:3000/', appSubUrl: '', - assetUrlPrefix: '', + assetUrlPrefix: '/assets', sharedWorkerUri: '', runModeIsProd: true, customEmojis: {}, From 04fb6f1c0ba562f5e32fab068fd1251fa64b4ed6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 9 Apr 2026 11:31:05 +0200 Subject: [PATCH 004/126] Replace `rollup-plugin-license` with `rolldown-license-plugin` (#37130) Replace `rollup-plugin-license` and `wrap-ansi` with [`rolldown-license-plugin`](https://github.com/silverwind/rolldown-license-plugin), a zero-dependency plugin with async parallel I/O and built-in word wrapping. - Removes `rollup-plugin-license` (pulls in `lodash`, `moment`) and `wrap-ansi` from the dependency tree - License build time reduced by ~40% (370ms vs 640ms) - Added e2e test for `licenses.txt` Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) --- build/generate-go-licenses.go | 1 + package.json | 5 +- pnpm-lock.yaml | 369 +--------------------------------- tests/e2e/licenses.test.ts | 9 + vite.config.ts | 51 ++--- 5 files changed, 41 insertions(+), 394 deletions(-) create mode 100644 tests/e2e/licenses.test.ts diff --git a/build/generate-go-licenses.go b/build/generate-go-licenses.go index b710fdb841f..057e6a6e49a 100644 --- a/build/generate-go-licenses.go +++ b/build/generate-go-licenses.go @@ -19,6 +19,7 @@ import ( // regexp is based on go-license, excluding README and NOTICE // https://github.com/google/go-licenses/blob/master/licenses/find.go +// also defined in vite.config.ts var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`) // primaryLicenseRe matches exact primary license filenames without suffixes. diff --git a/package.json b/package.json index e3539b9495d..3a70a503fdb 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", "postcss": "8.5.9", - "rollup-plugin-license": "3.7.0", + "rolldown-license-plugin": "2.2.0", "sortablejs": "1.15.7", "swagger-ui-dist": "5.32.2", "tailwindcss": "3.4.19", @@ -73,8 +73,7 @@ "vite-string-plugin": "2.0.2", "vue": "3.5.32", "vue-bar-graph": "2.2.0", - "vue-chartjs": "5.3.3", - "wrap-ansi": "10.0.0" + "vue-chartjs": "5.3.3" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e69fcdb65a3..270d6c03b12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,9 +187,9 @@ importers: postcss: specifier: 8.5.9 version: 8.5.9 - rollup-plugin-license: - specifier: 3.7.0 - version: 3.7.0(picomatch@4.0.4)(rollup@4.60.1) + rolldown-license-plugin: + specifier: 2.2.0 + version: 2.2.0 sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -232,9 +232,6 @@ importers: vue-chartjs: specifier: 5.3.3 version: 5.3.3(chart.js@4.5.1)(vue@3.5.32(typescript@6.0.2)) - wrap-ansi: - specifier: 10.0.0 - version: 10.0.0 devDependencies: '@eslint-community/eslint-plugin-eslint-comments': specifier: 4.7.1 @@ -1258,144 +1255,6 @@ packages: '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} - cpu: [x64] - os: [win32] - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1915,10 +1774,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - ansi_up@6.0.6: resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==} @@ -1939,10 +1794,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-find-index@1.0.2: - resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} - engines: {node: '>=0.10.0'} - asciinema-player@3.15.1: resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==} @@ -2137,9 +1988,6 @@ packages: resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} engines: {node: '>= 12.0.0'} - commenting@1.1.0: - resolution: {integrity: sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==} - compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -3413,9 +3261,6 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - moment@2.30.1: - resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3510,10 +3355,6 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - package-name-regex@2.0.6: - resolution: {integrity: sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==} - engines: {node: '>=12'} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3743,22 +3584,14 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown-license-plugin@2.2.0: + resolution: {integrity: sha512-7a/v9/9o5/pCpPtx4WSX68/xHC8wmmR/cxkofWQ7I7ep5Tvhjb9KkIUdTyuKc52SHiGSz2PxrS0qm/z2PjJyiQ==} + rolldown@1.0.0-rc.13: resolution: {integrity: sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup-plugin-license@3.7.0: - resolution: {integrity: sha512-RvvOIF+GH3fBR3wffgc/vmjQn6qOn72WjppWVDp/v+CLpT0BbcRBdSkPeeIOL6U5XccdYgSIMjUyXgxlKEEFcw==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 - - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -3842,27 +3675,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - spdx-compare@1.0.0: - resolution: {integrity: sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==} - - spdx-exceptions@2.5.0: - resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} - - spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - - spdx-expression-validate@2.0.0: - resolution: {integrity: sha512-b3wydZLM+Tc6CFvaRDBOF9d76oGIHNCLYFeHbftFXUWjnfZWganmDmvtM5sm1cRwJc/VDBMLyGGrsLFd1vOxbg==} - - spdx-license-ids@3.0.23: - resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} - - spdx-ranges@2.1.1: - resolution: {integrity: sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==} - - spdx-satisfies@5.0.1: - resolution: {integrity: sha512-Nwor6W6gzFp8XX4neaKQ7ChV4wmpSh2sSDemMFSzHxpTw460jxFYeOn+jq4ybnSSw/5sc3pjka9MQPouksQNpw==} - spectral-cli-bundle@1.0.7: resolution: {integrity: sha512-vIUC0nwv9tYxWV1xHdR3CTVDOEEtLKaDCcQpARZgO0Db7VmSpSWJ4xrnVPNSmO59hBtGwW2CVzHf0OimJBaKAA==} engines: {node: '>=20'} @@ -4299,10 +4111,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@10.0.0: - resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} - engines: {node: '>=20'} - write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -5274,81 +5082,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rollup/rollup-android-arm-eabi@4.60.1': - optional: true - - '@rollup/rollup-android-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-x64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.1': - optional: true - '@rtsao/scc@1.1.0': {} '@scarf/scarf@1.4.0': {} @@ -5994,8 +5727,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - ansi_up@6.0.6: {} any-promise@1.3.0: {} @@ -6011,8 +5742,6 @@ snapshots: aria-query@5.3.2: {} - array-find-index@1.0.2: {} - asciinema-player@3.15.1: dependencies: '@babel/runtime': 7.29.2 @@ -6180,8 +5909,6 @@ snapshots: comment-parser@1.4.6: {} - commenting@1.1.0: {} - compare-versions@6.1.1: {} concat-map@0.0.1: {} @@ -7627,8 +7354,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - moment@2.30.1: {} - moo@0.5.3: {} ms@2.1.3: {} @@ -7705,8 +7430,6 @@ snapshots: package-manager-detector@1.6.0: {} - package-name-regex@2.0.6: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -7902,6 +7625,8 @@ snapshots: robust-predicates@3.0.3: {} + rolldown-license-plugin@2.2.0: {} + rolldown@1.0.0-rc.13: dependencies: '@oxc-project/types': 0.123.0 @@ -7923,51 +7648,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.13 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.13 - rollup-plugin-license@3.7.0(picomatch@4.0.4)(rollup@4.60.1): - dependencies: - commenting: 1.1.0 - fdir: 6.5.0(picomatch@4.0.4) - lodash: 4.18.1 - magic-string: 0.30.21 - moment: 2.30.1 - package-name-regex: 2.0.6 - rollup: 4.60.1 - spdx-expression-validate: 2.0.0 - spdx-satisfies: 5.0.1 - transitivePeerDependencies: - - picomatch - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 - roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -8042,33 +7722,6 @@ snapshots: source-map-js@1.2.1: {} - spdx-compare@1.0.0: - dependencies: - array-find-index: 1.0.2 - spdx-expression-parse: 3.0.1 - spdx-ranges: 2.1.1 - - spdx-exceptions@2.5.0: {} - - spdx-expression-parse@3.0.1: - dependencies: - spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.23 - - spdx-expression-validate@2.0.0: - dependencies: - spdx-expression-parse: 3.0.1 - - spdx-license-ids@3.0.23: {} - - spdx-ranges@2.1.1: {} - - spdx-satisfies@5.0.1: - dependencies: - spdx-compare: 1.0.0 - spdx-expression-parse: 3.0.1 - spdx-ranges: 2.1.1 - spectral-cli-bundle@1.0.7: optionalDependencies: fsevents: 2.3.3 @@ -8534,12 +8187,6 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@10.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.0 - strip-ansi: 7.2.0 - write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 diff --git a/tests/e2e/licenses.test.ts b/tests/e2e/licenses.test.ts new file mode 100644 index 00000000000..b347ad95e2e --- /dev/null +++ b/tests/e2e/licenses.test.ts @@ -0,0 +1,9 @@ +import {test, expect} from '@playwright/test'; + +test('licenses.txt', async ({page}) => { + const resp = await page.goto('/assets/licenses.txt'); + expect(resp?.status()).toBe(200); + const content = await resp!.text(); + expect(content).toContain('@vue/'); + expect(content).toContain('code.gitea.io/'); +}); diff --git a/vite.config.ts b/vite.config.ts index cc446f6558a..d2d37785092 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,12 @@ import {build, defineConfig} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; +import {licensePlugin, wrap} from 'rolldown-license-plugin'; import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs'; import path, {basename, join, parse} from 'node:path'; import {env} from 'node:process'; import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; -import wrapAnsi from 'wrap-ansi'; -import licensePlugin from 'rollup-plugin-license'; import type {InlineConfig, Plugin, Rolldown} from 'vite'; import {camelize} from 'vue'; @@ -39,10 +38,6 @@ const webComponents = new Set([ 'text-expander', ]); -function formatLicenseText(licenseText: string) { - return wrapAnsi(licenseText || '', 80).trim(); -} - const commonRolldownOptions: Rolldown.RolldownOptions = { checks: { eval: false, // htmx needs eval @@ -314,33 +309,29 @@ export default defineConfig(commonViteOpts({ }, }), isProduction ? licensePlugin({ - thirdParty: { - output: { - file: join(import.meta.dirname, 'public/assets/licenses.txt'), - template(deps) { - const line = '-'.repeat(80); - const goJson = readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8'); - const goModules = JSON.parse(goJson).map(({name, licenseText}: {name: string, licenseText: string}) => { - return {name, body: formatLicenseText(licenseText)}; - }); - const jsModules = deps.map((dep) => { - return {name: dep.name, version: dep.version, body: formatLicenseText(dep.licenseText ?? '')}; - }); - const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); - return modules.map(({name, version, body}: {name: string, version?: string, body: string}) => { - const title = version ? `${name}@${version}` : name; - return `${line}\n${title}\n${line}\n${body}`; - }).join('\n'); - }, - }, - allow(dependency) { - if (dependency.name === 'khroma') return true; // MIT: https://github.com/fabiospampinato/khroma/pull/33 - return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dependency.license ?? ''); - }, + done(deps, context) { + const line = '-'.repeat(80); + const goLicenses = JSON.parse(readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8')); + const combined: Record = {}; + for (const {name, licenseText} of goLicenses) { + combined[name] = wrap(licenseText || '', 80).trim(); + } + for (const {name, version, licenseText} of deps) { + combined[`${name}@${version}`] = wrap(licenseText, 80).trim(); + } + const content = Object.entries(combined) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([title, body]) => `${line}\n${title}\n${line}\n${body}`).join('\n'); + context.emitFile({type: 'asset', fileName: 'licenses.txt', source: content}); + }, + match: /^((UN)?LICEN(S|C)E|COPYING).*$/i, // also defined in build/generate-go-licenses.go + allow(dep) { + if (dep.name === 'khroma') return true; // MIT: https://github.com/fabiospampinato/khroma/pull/33 + return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dep.license); }, }) : { name: 'dev-licenses-stub', - closeBundle() { + configureServer() { writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); }, }, From 980a8995bc0c98b736991b424b7e3897e156e3ab Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 9 Apr 2026 15:03:32 +0200 Subject: [PATCH 005/126] Report structurally invalid workflows to users (#37116) `model.ReadWorkflow` succeeds for YAML that is syntactically valid but fails deeper parsing in `jobparser.Parse` (e.g. blank lines inside `run: |` blocks cause a SetJob round-trip error). Add `ValidateWorkflowContent` which runs the full `jobparser.Parse` to catch these cases, and use it in the file view, the actions workflow list, and the workflow detection loop so users see the error instead of silently getting a 500 or a dropped workflow. Fixes #37115 Signed-off-by: Nicolas Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Zettat123 Co-authored-by: Giteabot Co-authored-by: wxiaoguang --- modules/actions/workflows.go | 10 ++++++++++ modules/actions/workflows_test.go | 22 ++++++++++++++++------ routers/web/repo/actions/actions.go | 9 +++++++++ routers/web/repo/view_file.go | 5 +---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 4ac06def4d5..ba1aee7d72f 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -103,10 +103,20 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { if err != nil { return nil, err } + if err := ValidateWorkflowContent(content); err != nil { + return nil, err + } return events, nil } +// ValidateWorkflowContent catches structural errors (e.g. blank lines in run: | blocks) +// that model.ReadWorkflow alone does not detect. +func ValidateWorkflowContent(content []byte) error { + _, err := jobparser.Parse(content) + return err +} + func DetectWorkflows( gitRepo *git.Repository, commit *git.Commit, diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index ea027366f7e..cda2de13e28 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -9,16 +9,26 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" ) +func fullWorkflowContent(part string) []byte { + return []byte(` +name: test +` + part + ` +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo hello +`) +} + func TestIsWorkflow(t *testing.T) { - oldDirs := setting.Actions.WorkflowDirs - defer func() { - setting.Actions.WorkflowDirs = oldDirs - }() + defer test.MockVariableValue(&setting.Actions.WorkflowDirs)() tests := []struct { name string @@ -218,7 +228,7 @@ func TestDetectMatched(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) @@ -373,7 +383,7 @@ func TestMatchIssuesEvent(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index 988d2d0a993..644a53f28a0 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -151,6 +151,11 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow workflows = append(workflows, workflow) continue } + if err := actions.ValidateWorkflowContent(content); err != nil { + workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error()) + workflows = append(workflows, workflow) + continue + } workflow.Workflow = wf // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run. hasJobWithoutNeeds := false @@ -315,6 +320,10 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) { if !job.Status.IsWaiting() { 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) { diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3ae0dab25b8..65fcb8adba2 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -25,8 +25,6 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" - - "github.com/nektos/act/pkg/model" ) func prepareLatestCommitInfo(ctx *context.Context) bool { @@ -184,8 +182,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { if err != nil { log.Error("actions.GetContentFromEntry: %v", err) } - _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) - if workFlowErr != nil { + if workFlowErr := actions.ValidateWorkflowContent(content); workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) { From c10a5b908a003a3f5e8c19d9f5b6d6a18cd17fdf Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 10 Apr 2026 04:22:17 +0800 Subject: [PATCH 006/126] Remove unneeded doctor sub-commands (#37156) Co-authored-by: Giteabot --- models/actions/run_test.go | 2 +- models/fixtures/action_run.yml | 21 -- models/fixtures/action_run_job.yml | 15 -- models/fixtures/action_task.yml | 20 -- models/user/user.go | 10 - services/doctor/actions.go | 101 --------- services/doctor/actions_test.go | 24 --- services/doctor/breaking.go | 97 --------- services/doctor/dbversion.go | 43 ---- services/doctor/fix16961.go | 328 ----------------------------- services/doctor/fix16961_test.go | 265 ----------------------- services/doctor/mergebase.go | 115 ---------- services/doctor/misc.go | 64 ------ services/doctor/paths.go | 123 ----------- services/doctor/usertype.go | 41 ---- 15 files changed, 1 insertion(+), 1268 deletions(-) delete mode 100644 services/doctor/actions_test.go delete mode 100644 services/doctor/breaking.go delete mode 100644 services/doctor/dbversion.go delete mode 100644 services/doctor/fix16961.go delete mode 100644 services/doctor/fix16961_test.go delete mode 100644 services/doctor/mergebase.go delete mode 100644 services/doctor/paths.go delete mode 100644 services/doctor/usertype.go diff --git a/models/actions/run_test.go b/models/actions/run_test.go index e1c884518fa..e82cbe84b51 100644 --- a/models/actions/run_test.go +++ b/models/actions/run_test.go @@ -32,7 +32,7 @@ func TestUpdateRepoRunsNumbers(t *testing.T) { err = UpdateRepoRunsNumbers(t.Context(), repo) assert.NoError(t, err) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) - assert.Equal(t, 5, repo.NumActionRuns) + assert.Equal(t, 4, repo.NumActionRuns) assert.Equal(t, 3, repo.NumClosedActionRuns) } diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 63fd011e65f..b7d1201189f 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -140,25 +140,4 @@ need_approval: 0 approved_by: 0 -- - id: 805 - title: "update actions" - repo_id: 4 - owner_id: 1 - workflow_id: "artifact.yaml" - index: 191 - trigger_user_id: 1 - ref: "refs/heads/master" - commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" - event: "push" - trigger_event: "push" - is_fork_pull_request: 0 - status: 5 - started: 1683636528 - stopped: 1683636626 - created: 1683636108 - updated: 1683636626 - need_approval: 0 - approved_by: 0 - # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 82023f122a1..2a4e64285f4 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -130,19 +130,4 @@ started: 1683636528 stopped: 1683636626 -- - id: 206 - run_id: 805 - repo_id: 4 - owner_id: 1 - commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 - is_fork_pull_request: 0 - name: job_2 - attempt: 1 - job_id: job_2 - task_id: 56 - status: 3 - started: 1683636528 - stopped: 1683636626 - # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index 52a37da7305..13efe378c43 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -178,24 +178,4 @@ log_size: 0 log_expired: 0 -- - id: 56 - attempt: 1 - runner_id: 1 - status: 3 # 3 is the status code for "cancelled" - started: 1683636528 - stopped: 1683636626 - repo_id: 4 - owner_id: 1 - commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 - is_fork_pull_request: 0 - token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab - token_salt: eeeeeeee - token_last_eight: eeeeeeee - log_filename: artifact-test2/2f/47.log - log_in_storage: 1 - log_length: 707 - log_size: 90179 - log_expired: 0 - # DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly diff --git a/models/user/user.go b/models/user/user.go index 41cf89ad309..ade747401ab 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1462,16 +1462,6 @@ func IsUserVisibleToViewer(ctx context.Context, u, viewer *User) bool { return false } -// CountWrongUserType count OrgUser who have wrong type -func CountWrongUserType(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Count(new(User)) -} - -// FixWrongUserType fix OrgUser who have wrong type -func FixWrongUserType(ctx context.Context) (int64, error) { - return db.GetEngine(ctx).Where(builder.Eq{"type": 0}.And(builder.Neq{"num_teams": 0})).Cols("type").NoAutoTime().Update(&User{Type: 1}) -} - func GetOrderByName() string { if setting.UI.DefaultShowFullName { return "full_name, name" diff --git a/services/doctor/actions.go b/services/doctor/actions.go index cd3d19b724f..28e26c88ebe 100644 --- a/services/doctor/actions.go +++ b/services/doctor/actions.go @@ -7,17 +7,12 @@ import ( "context" "fmt" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/timeutil" repo_service "code.gitea.io/gitea/services/repository" - - "xorm.io/builder" ) func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error { @@ -64,95 +59,6 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo return nil } -func fixUnfinishedRunStatus(ctx context.Context, logger log.Logger, autofix bool) error { - total := 0 - inconsistent := 0 - fixed := 0 - - cond := builder.In("status", []actions_model.Status{ - actions_model.StatusWaiting, - actions_model.StatusRunning, - actions_model.StatusBlocked, - }).And(builder.Lt{"updated": timeutil.TimeStampNow().AddDuration(-setting.Actions.ZombieTaskTimeout)}) - - err := db.Iterate( - ctx, - cond, - func(ctx context.Context, run *actions_model.ActionRun) error { - total++ - - jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID) - if err != nil { - return fmt.Errorf("GetRunJobsByRunID: %w", err) - } - expected := actions_model.AggregateJobStatus(jobs) - if expected == run.Status { - return nil - } - - inconsistent++ - logger.Warn("Run %d (repo_id=%d, index=%d) has status %s, expected %s", run.ID, run.RepoID, run.Index, run.Status, expected) - - if !autofix { - return nil - } - - run.Started, run.Stopped = getRunTimestampsFromJobs(run, expected, jobs) - run.Status = expected - - if err := actions_model.UpdateRun(ctx, run, "status", "started", "stopped"); err != nil { - return fmt.Errorf("UpdateRun: %w", err) - } - fixed++ - - return nil - }, - ) - if err != nil { - logger.Critical("Unable to iterate unfinished runs: %v", err) - return err - } - - if inconsistent == 0 { - logger.Info("Checked %d unfinished runs; all statuses are consistent.", total) - return nil - } - - if autofix { - logger.Info("Checked %d unfinished runs; fixed %d of %d runs.", total, fixed, inconsistent) - } else { - logger.Warn("Checked %d unfinished runs; found %d runs need to be fixed", total, inconsistent) - } - - return nil -} - -func getRunTimestampsFromJobs(run *actions_model.ActionRun, newStatus actions_model.Status, jobs actions_model.ActionJobList) (started, stopped timeutil.TimeStamp) { - started = run.Started - if (newStatus.IsRunning() || newStatus.IsDone()) && started.IsZero() { - var earliest timeutil.TimeStamp - for _, job := range jobs { - if job.Started > 0 && (earliest.IsZero() || job.Started < earliest) { - earliest = job.Started - } - } - started = earliest - } - - stopped = run.Stopped - if newStatus.IsDone() && stopped.IsZero() { - var latest timeutil.TimeStamp - for _, job := range jobs { - if job.Stopped > latest { - latest = job.Stopped - } - } - stopped = latest - } - - return started, stopped -} - func init() { Register(&Check{ Title: "Disable the actions unit for all mirrors", @@ -161,11 +67,4 @@ func init() { Run: disableMirrorActionsUnit, Priority: 9, }) - Register(&Check{ - Title: "Fix inconsistent status for unfinished actions runs", - Name: "fix-actions-unfinished-run-status", - IsDefault: false, - Run: fixUnfinishedRunStatus, - Priority: 9, - }) } diff --git a/services/doctor/actions_test.go b/services/doctor/actions_test.go deleted file mode 100644 index b2fd3d0d553..00000000000 --- a/services/doctor/actions_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "testing" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/log" - - "github.com/stretchr/testify/assert" -) - -func Test_fixUnfinishedRunStatus(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - fixUnfinishedRunStatus(t.Context(), log.GetLogger(log.DEFAULT), true) - - // check if the run is cancelled by id - run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 805}) - assert.Equal(t, actions_model.StatusCancelled, run.Status) -} diff --git a/services/doctor/breaking.go b/services/doctor/breaking.go deleted file mode 100644 index 77e3d4e8ef8..00000000000 --- a/services/doctor/breaking.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "context" - "fmt" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" - - "xorm.io/builder" -) - -func iterateUserAccounts(ctx context.Context, each func(*user.User) error) error { - err := db.Iterate( - ctx, - builder.Gt{"id": 0}, - func(ctx context.Context, bean *user.User) error { - return each(bean) - }, - ) - return err -} - -// Since 1.16.4 new restrictions has been set on email addresses. However users with invalid email -// addresses would be currently facing a error due to their invalid email address. -// Ref: https://github.com/go-gitea/gitea/pull/19085 & https://github.com/go-gitea/gitea/pull/17688 -func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error { - // We could use quirky SQL to get all users that start without a [a-zA-Z0-9], but that would mean - // DB provider-specific SQL and only works _now_. So instead we iterate through all user accounts - // and use the user.ValidateEmail function to be future-proof. - var invalidUserCount int64 - if err := iterateUserAccounts(ctx, func(u *user.User) error { - // Only check for users, skip - if u.Type != user.UserTypeIndividual { - return nil - } - - if err := user.ValidateEmail(u.Email); err != nil { - invalidUserCount++ - logger.Warn("User[id=%d name=%q] have not a valid e-mail: %v", u.ID, u.Name, err) - } - return nil - }); err != nil { - return fmt.Errorf("iterateUserAccounts: %w", err) - } - - if invalidUserCount == 0 { - logger.Info("All users have a valid e-mail.") - } else { - logger.Warn("%d user(s) have a non-valid e-mail.", invalidUserCount) - } - return nil -} - -// From time to time Gitea makes changes to the reserved usernames and which symbols -// are allowed for various reasons. This check helps with detecting users that, according -// to our reserved names, don't have a valid username. -func checkUserName(ctx context.Context, logger log.Logger, _ bool) error { - var invalidUserCount int64 - if err := iterateUserAccounts(ctx, func(u *user.User) error { - if err := user.IsUsableUsername(u.Name); err != nil { - invalidUserCount++ - logger.Warn("User[id=%d] does not have a valid username: %v", u.ID, err) - } - return nil - }); err != nil { - return fmt.Errorf("iterateUserAccounts: %w", err) - } - - if invalidUserCount == 0 { - logger.Info("All users have a valid username.") - } else { - logger.Warn("%d user(s) have a non-valid username.", invalidUserCount) - } - return nil -} - -func init() { - Register(&Check{ - Title: "Check if users has an valid email address", - Name: "check-user-email", - IsDefault: false, - Run: checkUserEmail, - Priority: 9, - }) - Register(&Check{ - Title: "Check if users have a valid username", - Name: "check-user-names", - IsDefault: false, - Run: checkUserName, - Priority: 9, - }) -} diff --git a/services/doctor/dbversion.go b/services/doctor/dbversion.go deleted file mode 100644 index 34279a45e7a..00000000000 --- a/services/doctor/dbversion.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "context" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/migrations" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/services/versioned_migration" -) - -func checkDBVersion(ctx context.Context, logger log.Logger, autofix bool) error { - logger.Info("Expected database version: %d", migrations.ExpectedDBVersion()) - if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil { - if !autofix { - logger.Critical("Error: %v during ensure up to date", err) - return err - } - logger.Warn("Got Error: %v during ensure up to date", err) - logger.Warn("Attempting to migrate to the latest DB version to fix this.") - - err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate) - if err != nil { - logger.Critical("Error: %v during migration", err) - } - return err - } - return nil -} - -func init() { - Register(&Check{ - Title: "Check Database Version", - Name: "check-db-version", - IsDefault: true, - Run: checkDBVersion, - AbortIfFailed: false, - Priority: 2, - }) -} diff --git a/services/doctor/fix16961.go b/services/doctor/fix16961.go deleted file mode 100644 index 63e33350ad5..00000000000 --- a/services/doctor/fix16961.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "bytes" - "context" - "errors" - "fmt" - - "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/timeutil" - - "xorm.io/builder" -) - -// #16831 revealed that the dump command that was broken in 1.14.3-1.14.6 and 1.15.0 (#15885). -// This led to repo_unit and login_source cfg not being converted to JSON in the dump -// Unfortunately although it was hoped that there were only a few users affected it -// appears that many users are affected. - -// We therefore need to provide a doctor command to fix this repeated issue #16961 - -func parseBool16961(bs []byte) (bool, error) { - if bytes.EqualFold(bs, []byte("%!s(bool=false)")) { - return false, nil - } - - if bytes.EqualFold(bs, []byte("%!s(bool=true)")) { - return true, nil - } - - return false, fmt.Errorf("unexpected bool format: %s", string(bs)) -} - -func fixUnitConfig16961(bs []byte, cfg *repo_model.UnitConfig) (fixed bool, err error) { - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - - // Handle #16961 - if string(bs) != "&{}" && len(bs) != 0 { - return false, err - } - - return true, nil -} - -func fixExternalWikiConfig16961(bs []byte, cfg *repo_model.ExternalWikiConfig) (fixed bool, err error) { - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - - if len(bs) < 3 { - return false, err - } - if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' { - return false, err - } - cfg.ExternalWikiURL = string(bs[2 : len(bs)-1]) - return true, nil -} - -func fixExternalTrackerConfig16961(bs []byte, cfg *repo_model.ExternalTrackerConfig) (fixed bool, err error) { - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - // Handle #16961 - if len(bs) < 3 { - return false, err - } - - if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' { - return false, err - } - - parts := bytes.Split(bs[2:len(bs)-1], []byte{' '}) - if len(parts) != 3 { - return false, err - } - - cfg.ExternalTrackerURL = string(bytes.Join(parts[:len(parts)-2], []byte{' '})) - cfg.ExternalTrackerFormat = string(parts[len(parts)-2]) - cfg.ExternalTrackerStyle = string(parts[len(parts)-1]) - return true, nil -} - -func fixPullRequestsConfig16961(bs []byte, cfg *repo_model.PullRequestsConfig) (fixed bool, err error) { - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - - // Handle #16961 - if len(bs) < 3 { - return false, err - } - - if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' { - return false, err - } - - // PullRequestsConfig was the following in 1.14 - // type PullRequestsConfig struct { - // IgnoreWhitespaceConflicts bool - // AllowMerge bool - // AllowRebase bool - // AllowRebaseMerge bool - // AllowSquash bool - // AllowManualMerge bool - // AutodetectManualMerge bool - // } - // - // 1.15 added in addition: - // DefaultDeleteBranchAfterMerge bool - // DefaultMergeStyle MergeStyle - parts := bytes.Split(bs[2:len(bs)-1], []byte{' '}) - if len(parts) < 7 { - return false, err - } - - var parseErr error - cfg.IgnoreWhitespaceConflicts, parseErr = parseBool16961(parts[0]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowMerge, parseErr = parseBool16961(parts[1]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowRebase, parseErr = parseBool16961(parts[2]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowRebaseMerge, parseErr = parseBool16961(parts[3]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowSquash, parseErr = parseBool16961(parts[4]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowManualMerge, parseErr = parseBool16961(parts[5]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AutodetectManualMerge, parseErr = parseBool16961(parts[6]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - - // 1.14 unit - if len(parts) == 7 { - return true, nil - } - - if len(parts) < 9 { - return false, err - } - - cfg.DefaultDeleteBranchAfterMerge, parseErr = parseBool16961(parts[7]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - - cfg.DefaultMergeStyle = repo_model.MergeStyle(string(bytes.Join(parts[8:], []byte{' '}))) - return true, nil -} - -func fixIssuesConfig16961(bs []byte, cfg *repo_model.IssuesConfig) (fixed bool, err error) { - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - - // Handle #16961 - if len(bs) < 3 { - return false, err - } - - if bs[0] != '&' || bs[1] != '{' || bs[len(bs)-1] != '}' { - return false, err - } - - parts := bytes.Split(bs[2:len(bs)-1], []byte{' '}) - if len(parts) != 3 { - return false, err - } - var parseErr error - cfg.EnableTimetracker, parseErr = parseBool16961(parts[0]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.AllowOnlyContributorsToTrackTime, parseErr = parseBool16961(parts[1]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - cfg.EnableDependencies, parseErr = parseBool16961(parts[2]) - if parseErr != nil { - return false, errors.Join(err, parseErr) - } - return true, nil -} - -func fixBrokenRepoUnit16961(repoUnit *repo_model.RepoUnit, bs []byte) (fixed bool, err error) { - // Shortcut empty or null values - if len(bs) == 0 { - return false, nil - } - - var cfg any - err = json.UnmarshalHandleDoubleEncode(bs, &cfg) - if err == nil { - return false, nil - } - - switch repoUnit.Type { - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: - cfg := &repo_model.UnitConfig{} - repoUnit.Config = cfg - if fixed, err := fixUnitConfig16961(bs, cfg); !fixed { - return false, err - } - case unit.TypeExternalWiki: - cfg := &repo_model.ExternalWikiConfig{} - repoUnit.Config = cfg - - if fixed, err := fixExternalWikiConfig16961(bs, cfg); !fixed { - return false, err - } - case unit.TypeExternalTracker: - cfg := &repo_model.ExternalTrackerConfig{} - repoUnit.Config = cfg - if fixed, err := fixExternalTrackerConfig16961(bs, cfg); !fixed { - return false, err - } - case unit.TypePullRequests: - cfg := &repo_model.PullRequestsConfig{} - repoUnit.Config = cfg - - if fixed, err := fixPullRequestsConfig16961(bs, cfg); !fixed { - return false, err - } - case unit.TypeIssues: - cfg := &repo_model.IssuesConfig{} - repoUnit.Config = cfg - if fixed, err := fixIssuesConfig16961(bs, cfg); !fixed { - return false, err - } - default: - panic(fmt.Sprintf("unrecognized repo unit type: %v", repoUnit.Type)) - } - return true, nil -} - -func fixBrokenRepoUnits16961(ctx context.Context, logger log.Logger, autofix bool) error { - // RepoUnit describes all units of a repository - type RepoUnit struct { - ID int64 - RepoID int64 - Type unit.Type - Config []byte - CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` - } - - count := 0 - - err := db.Iterate( - ctx, - builder.Gt{ - "id": 0, - }, - func(ctx context.Context, unit *RepoUnit) error { - bs := unit.Config - repoUnit := &repo_model.RepoUnit{ - ID: unit.ID, - RepoID: unit.RepoID, - Type: unit.Type, - CreatedUnix: unit.CreatedUnix, - } - - if fixed, err := fixBrokenRepoUnit16961(repoUnit, bs); !fixed { - return err - } - - count++ - if !autofix { - return nil - } - - return repo_model.UpdateRepoUnitConfig(ctx, repoUnit) - }, - ) - if err != nil { - logger.Critical("Unable to iterate across repounits to fix the broken units: Error %v", err) - return err - } - - if !autofix { - if count == 0 { - logger.Info("Found no broken repo_units") - } else { - logger.Warn("Found %d broken repo_units", count) - } - return nil - } - logger.Info("Fixed %d broken repo_units", count) - - return nil -} - -func init() { - Register(&Check{ - Title: "Check for incorrectly dumped repo_units (See #16961)", - Name: "fix-broken-repo-units", - IsDefault: false, - Run: fixBrokenRepoUnits16961, - Priority: 7, - }) -} diff --git a/services/doctor/fix16961_test.go b/services/doctor/fix16961_test.go deleted file mode 100644 index 11a128620c5..00000000000 --- a/services/doctor/fix16961_test.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "testing" - - repo_model "code.gitea.io/gitea/models/repo" - - "github.com/stretchr/testify/assert" -) - -func Test_fixUnitConfig_16961(t *testing.T) { - tests := []struct { - name string - bs string - wantFixed bool - wantErr bool - }{ - { - name: "normal: {}", - bs: "{}", - wantFixed: false, - wantErr: false, - }, - { - name: "broken but fixable: &{}", - bs: "&{}", - wantFixed: true, - wantErr: false, - }, - { - name: "broken but unfixable: &{asdasd}", - bs: "&{asdasd}", - wantFixed: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotFixed, err := fixUnitConfig16961([]byte(tt.bs), &repo_model.UnitConfig{}) - if (err != nil) != tt.wantErr { - t.Errorf("fixUnitConfig_16961() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFixed != tt.wantFixed { - t.Errorf("fixUnitConfig_16961() = %v, want %v", gotFixed, tt.wantFixed) - } - }) - } -} - -func Test_fixExternalWikiConfig_16961(t *testing.T) { - tests := []struct { - name string - bs string - expected string - wantFixed bool - wantErr bool - }{ - { - name: "normal: {\"ExternalWikiURL\":\"http://someurl\"}", - bs: "{\"ExternalWikiURL\":\"http://someurl\"}", - expected: "http://someurl", - wantFixed: false, - wantErr: false, - }, - { - name: "broken: &{http://someurl}", - bs: "&{http://someurl}", - expected: "http://someurl", - wantFixed: true, - wantErr: false, - }, - { - name: "broken but unfixable: http://someurl", - bs: "http://someurl", - wantFixed: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &repo_model.ExternalWikiConfig{} - gotFixed, err := fixExternalWikiConfig16961([]byte(tt.bs), cfg) - if (err != nil) != tt.wantErr { - t.Errorf("fixExternalWikiConfig_16961() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFixed != tt.wantFixed { - t.Errorf("fixExternalWikiConfig_16961() = %v, want %v", gotFixed, tt.wantFixed) - } - if cfg.ExternalWikiURL != tt.expected { - t.Errorf("fixExternalWikiConfig_16961().ExternalWikiURL = %v, want %v", cfg.ExternalWikiURL, tt.expected) - } - }) - } -} - -func Test_fixExternalTrackerConfig_16961(t *testing.T) { - tests := []struct { - name string - bs string - expected repo_model.ExternalTrackerConfig - wantFixed bool - wantErr bool - }{ - { - name: "normal", - bs: `{"ExternalTrackerURL":"a","ExternalTrackerFormat":"b","ExternalTrackerStyle":"c"}`, - expected: repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: "a", - ExternalTrackerFormat: "b", - ExternalTrackerStyle: "c", - }, - wantFixed: false, - wantErr: false, - }, - { - name: "broken", - bs: "&{a b c}", - expected: repo_model.ExternalTrackerConfig{ - ExternalTrackerURL: "a", - ExternalTrackerFormat: "b", - ExternalTrackerStyle: "c", - }, - wantFixed: true, - wantErr: false, - }, - { - name: "broken - too many fields", - bs: "&{a b c d}", - wantFixed: false, - wantErr: true, - }, - { - name: "broken - wrong format", - bs: "a b c d}", - wantFixed: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &repo_model.ExternalTrackerConfig{} - gotFixed, err := fixExternalTrackerConfig16961([]byte(tt.bs), cfg) - if (err != nil) != tt.wantErr { - t.Errorf("fixExternalTrackerConfig_16961() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFixed != tt.wantFixed { - t.Errorf("fixExternalTrackerConfig_16961() = %v, want %v", gotFixed, tt.wantFixed) - } - if cfg.ExternalTrackerFormat != tt.expected.ExternalTrackerFormat { - t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerFormat = %v, want %v", tt.expected.ExternalTrackerFormat, cfg.ExternalTrackerFormat) - } - if cfg.ExternalTrackerStyle != tt.expected.ExternalTrackerStyle { - t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerStyle = %v, want %v", tt.expected.ExternalTrackerStyle, cfg.ExternalTrackerStyle) - } - if cfg.ExternalTrackerURL != tt.expected.ExternalTrackerURL { - t.Errorf("fixExternalTrackerConfig_16961().ExternalTrackerURL = %v, want %v", tt.expected.ExternalTrackerURL, cfg.ExternalTrackerURL) - } - }) - } -} - -func Test_fixPullRequestsConfig_16961(t *testing.T) { - tests := []struct { - name string - bs string - expected repo_model.PullRequestsConfig - wantFixed bool - wantErr bool - }{ - { - name: "normal", - bs: `{"IgnoreWhitespaceConflicts":false,"AllowMerge":false,"AllowRebase":false,"AllowRebaseMerge":false,"AllowSquash":false,"AllowManualMerge":false,"AutodetectManualMerge":false,"DefaultDeleteBranchAfterMerge":false,"DefaultMergeStyle":""}`, - }, - { - name: "broken - 1.14", - bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false)}`, - expected: repo_model.PullRequestsConfig{ - IgnoreWhitespaceConflicts: false, - AllowMerge: true, - AllowRebase: true, - AllowRebaseMerge: true, - AllowSquash: true, - AllowManualMerge: false, - AutodetectManualMerge: false, - }, - wantFixed: true, - }, - { - name: "broken - 1.15", - bs: `&{%!s(bool=false) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=true) %!s(bool=false) %!s(bool=false) %!s(bool=false) merge}`, - expected: repo_model.PullRequestsConfig{ - AllowMerge: true, - AllowRebase: true, - AllowRebaseMerge: true, - AllowSquash: true, - DefaultMergeStyle: repo_model.MergeStyleMerge, - }, - wantFixed: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &repo_model.PullRequestsConfig{} - gotFixed, err := fixPullRequestsConfig16961([]byte(tt.bs), cfg) - if (err != nil) != tt.wantErr { - t.Errorf("fixPullRequestsConfig_16961() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFixed != tt.wantFixed { - t.Errorf("fixPullRequestsConfig_16961() = %v, want %v", gotFixed, tt.wantFixed) - } - assert.Equal(t, &tt.expected, cfg) - }) - } -} - -func Test_fixIssuesConfig_16961(t *testing.T) { - tests := []struct { - name string - bs string - expected repo_model.IssuesConfig - wantFixed bool - wantErr bool - }{ - { - name: "normal", - bs: `{"EnableTimetracker":true,"AllowOnlyContributorsToTrackTime":true,"EnableDependencies":true}`, - expected: repo_model.IssuesConfig{ - EnableTimetracker: true, - AllowOnlyContributorsToTrackTime: true, - EnableDependencies: true, - }, - }, - { - name: "broken", - bs: `&{%!s(bool=true) %!s(bool=true) %!s(bool=true)}`, - expected: repo_model.IssuesConfig{ - EnableTimetracker: true, - AllowOnlyContributorsToTrackTime: true, - EnableDependencies: true, - }, - wantFixed: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := &repo_model.IssuesConfig{} - gotFixed, err := fixIssuesConfig16961([]byte(tt.bs), cfg) - if (err != nil) != tt.wantErr { - t.Errorf("fixIssuesConfig_16961() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotFixed != tt.wantFixed { - t.Errorf("fixIssuesConfig_16961() = %v, want %v", gotFixed, tt.wantFixed) - } - assert.Equal(t, &tt.expected, cfg) - }) - } -} diff --git a/services/doctor/mergebase.go b/services/doctor/mergebase.go deleted file mode 100644 index a76ed8afb7b..00000000000 --- a/services/doctor/mergebase.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "context" - "fmt" - "strings" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/log" - - "xorm.io/builder" -) - -func iteratePRs(ctx context.Context, repo *repo_model.Repository, each func(*repo_model.Repository, *issues_model.PullRequest) error) error { - return db.Iterate( - ctx, - builder.Eq{"base_repo_id": repo.ID}, - func(ctx context.Context, bean *issues_model.PullRequest) error { - return each(repo, bean) - }, - ) -} - -func checkPRMergeBase(ctx context.Context, logger log.Logger, autofix bool) error { - numRepos := 0 - numPRs := 0 - numPRsUpdated := 0 - err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { - numRepos++ - return iteratePRs(ctx, repo, func(repo *repo_model.Repository, pr *issues_model.PullRequest) error { - numPRs++ - pr.BaseRepo = repo - - oldMergeBase := pr.MergeBase - - if !pr.HasMerged { - var err error - pr.MergeBase, _, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, pr.GetGitHeadRefName())) - if err != nil { - var err2 error - pr.MergeBase, _, err2 = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch)) - if err2 != nil { - logger.Warn("Unable to get merge base for PR ID %d, #%d onto %s in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2) - return nil - } - } - } else { - parentsString, _, err := gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID)) - if err != nil { - logger.Warn("Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err) - return nil - } - parents := strings.Split(strings.TrimSpace(parentsString), " ") - if len(parents) < 2 { - return nil - } - - refs := append([]string{}, parents[1:]...) - refs = append(refs, pr.GetGitHeadRefName()) - cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = gitrepo.RunCmdString(ctx, repo, cmd) - if err != nil { - logger.Warn("Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err) - return nil - } - } - pr.MergeBase = strings.TrimSpace(pr.MergeBase) - if pr.MergeBase != oldMergeBase { - if autofix { - if err := pr.UpdateCols(ctx, "merge_base"); err != nil { - logger.Critical("Failed to update merge_base. ERROR: %v", err) - return fmt.Errorf("Failed to update merge_base. ERROR: %w", err) - } - } else { - logger.Info("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase) - } - numPRsUpdated++ - } - return nil - }) - }) - - if autofix { - logger.Info("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos) - } else { - if numPRsUpdated == 0 { - logger.Info("All %d PRs in %d repos have a correct mergebase", numPRs, numRepos) - } else if err == nil { - logger.Critical("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos) - return fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos) - } else { - logger.Warn("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos) - } - } - - return err -} - -func init() { - Register(&Check{ - Title: "Recalculate merge bases", - Name: "recalculate-merge-bases", - IsDefault: false, - Run: checkPRMergeBase, - Priority: 7, - }) -} diff --git a/services/doctor/misc.go b/services/doctor/misc.go index 8765cfa0250..71f3efa4b1b 100644 --- a/services/doctor/misc.go +++ b/services/doctor/misc.go @@ -6,17 +6,13 @@ package doctor import ( "context" "fmt" - "os/exec" - "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" lru "github.com/hashicorp/golang-lru/v2" @@ -34,16 +30,6 @@ func iterateRepositories(ctx context.Context, each func(*repo_model.Repository) return err } -func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error { - path, err := exec.LookPath(setting.ScriptType) - if err != nil { - logger.Critical("ScriptType \"%q\" is not on the current PATH. Error: %v", setting.ScriptType, err) - return fmt.Errorf("ScriptType \"%q\" is not on the current PATH. Error: %w", setting.ScriptType, err) - } - logger.Info("ScriptType %s is on the current PATH at %s", setting.ScriptType, path) - return nil -} - func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error { if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { results, err := gitrepo.CheckDelegateHooks(ctx, repo) @@ -82,42 +68,6 @@ func checkUserStarNum(ctx context.Context, logger log.Logger, autofix bool) erro return nil } -func checkEnablePushOptions(ctx context.Context, logger log.Logger, autofix bool) error { - numRepos := 0 - numNeedUpdate := 0 - - if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { - numRepos++ - - if autofix { - return gitrepo.GitConfigSet(ctx, repo, "receive.advertisePushOptions", "true") - } - - value, err := gitrepo.GitConfigGet(ctx, repo, "receive.advertisePushOptions") - if err != nil { - return err - } - - result, valid := git.ParseBool(strings.TrimSpace(value)) - if !result || !valid { - numNeedUpdate++ - logger.Info("%s: does not have receive.advertisePushOptions set correctly: %q", repo.FullName(), value) - } - return nil - }); err != nil { - logger.Critical("Unable to EnablePushOptions: %v", err) - return err - } - - if autofix { - logger.Info("Enabled push options for %d repositories.", numRepos) - } else { - logger.Info("Checked %d repositories, %d need updates.", numRepos, numNeedUpdate) - } - - return nil -} - func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) error { numRepos := 0 numNeedUpdate := 0 @@ -244,13 +194,6 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro } func init() { - Register(&Check{ - Title: "Check if SCRIPT_TYPE is available", - Name: "script-type", - IsDefault: false, - Run: checkScriptType, - Priority: 5, - }) Register(&Check{ Title: "Check if hook files are up-to-date and executable", Name: "hooks", @@ -265,13 +208,6 @@ func init() { Run: checkUserStarNum, Priority: 6, }) - Register(&Check{ - Title: "Check that all git repositories have receive.advertisePushOptions set to true", - Name: "enable-push-options", - IsDefault: false, - Run: checkEnablePushOptions, - Priority: 7, - }) Register(&Check{ Title: "Check git-daemon-export-ok files", Name: "check-git-daemon-export-ok", diff --git a/services/doctor/paths.go b/services/doctor/paths.go deleted file mode 100644 index 4214c36b1a9..00000000000 --- a/services/doctor/paths.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "context" - "fmt" - "os" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -type configurationFile struct { - Name string - Path string - IsDirectory bool - Required bool - Writable bool -} - -func checkConfigurationFile(logger log.Logger, autofix bool, fileOpts configurationFile) error { - logger.Info(`%-26s %q`, log.NewColoredValue(fileOpts.Name+":", log.Reset), fileOpts.Path) - fi, err := os.Stat(fileOpts.Path) - if err != nil { - if os.IsNotExist(err) && autofix && fileOpts.IsDirectory { - if err := os.MkdirAll(fileOpts.Path, 0o777); err != nil { - logger.Error(" Directory does not exist and could not be created. ERROR: %v", err) - return fmt.Errorf("Configuration directory: \"%q\" does not exist and could not be created. ERROR: %w", fileOpts.Path, err) - } - fi, err = os.Stat(fileOpts.Path) - } - } - if err != nil { - if fileOpts.Required { - logger.Error(" Is REQUIRED but is not accessible. ERROR: %v", err) - return fmt.Errorf("Configuration file \"%q\" is not accessible but is required. Error: %w", fileOpts.Path, err) - } - logger.Warn(" NOTICE: is not accessible (Error: %v)", err) - // this is a non-critical error - return nil - } - - if fileOpts.IsDirectory && !fi.IsDir() { - logger.Error(" ERROR: not a directory") - return fmt.Errorf("Configuration directory \"%q\" is not a directory. Error: %w", fileOpts.Path, err) - } else if !fileOpts.IsDirectory && !fi.Mode().IsRegular() { - logger.Error(" ERROR: not a regular file") - return fmt.Errorf("Configuration file \"%q\" is not a regular file. Error: %w", fileOpts.Path, err) - } else if fileOpts.Writable { - if err := isWritableDir(fileOpts.Path); err != nil { - logger.Error(" ERROR: is required to be writable but is not writable: %v", err) - return fmt.Errorf("Configuration file \"%q\" is required to be writable but is not. Error: %w", fileOpts.Path, err) - } - } - return nil -} - -func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix bool) error { - if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() { - logger.Error("Failed to find configuration file at '%s'.", setting.CustomConf) - logger.Error("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf) - logger.Error("Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.") - logger.Critical("Cannot proceed without a configuration file") - return err - } - - setting.MustInstalled() - - configurationFiles := []configurationFile{ - {"Configuration File Path", setting.CustomConf, false, true, false}, - {"Repository Root Path", setting.RepoRootPath, true, true, true}, - {"Data Root Path", setting.AppDataPath, true, true, true}, - {"Custom File Root Path", setting.CustomPath, true, false, false}, - {"Work directory", setting.AppWorkPath, true, true, false}, - {"Log Root Path", setting.Log.RootPath, true, true, true}, - } - - if !setting.HasBuiltinBindata { - configurationFiles = append(configurationFiles, configurationFile{"Static File Root Path", setting.StaticRootPath, true, true, false}) - } - - numberOfErrors := 0 - for _, configurationFile := range configurationFiles { - if err := checkConfigurationFile(logger, autofix, configurationFile); err != nil { - numberOfErrors++ - } - } - - if numberOfErrors > 0 { - logger.Critical("Please check your configuration files and try again.") - return fmt.Errorf("%d configuration files with errors", numberOfErrors) - } - - return nil -} - -func isWritableDir(path string) error { - // There's no platform-independent way of checking if a directory is writable - // https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable - tmpFile, err := os.CreateTemp(path, "doctors-order") - if err != nil { - return err - } - if err := os.Remove(tmpFile.Name()); err != nil { - log.Warn("can't remove temporary file: %q", tmpFile.Name()) - } - _ = tmpFile.Close() - return nil -} - -func init() { - Register(&Check{ - Title: "Check paths and basic configuration", - Name: "paths", - IsDefault: true, - Run: checkConfigurationFiles, - AbortIfFailed: true, - SkipDatabaseInitialization: true, - Priority: 1, - }) -} diff --git a/services/doctor/usertype.go b/services/doctor/usertype.go deleted file mode 100644 index ab32b78e62f..00000000000 --- a/services/doctor/usertype.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package doctor - -import ( - "context" - - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" -) - -func checkUserType(ctx context.Context, logger log.Logger, autofix bool) error { - count, err := user_model.CountWrongUserType(ctx) - if err != nil { - logger.Critical("Error: %v whilst counting wrong user types") - return err - } - if count > 0 { - if autofix { - if count, err = user_model.FixWrongUserType(ctx); err != nil { - logger.Critical("Error: %v whilst fixing wrong user types") - return err - } - logger.Info("%d users with wrong type fixed", count) - } else { - logger.Warn("%d users with wrong type exist", count) - } - } - return nil -} - -func init() { - Register(&Check{ - Title: "Check if user with wrong type exist", - Name: "check-user-type", - IsDefault: true, - Run: checkUserType, - Priority: 3, - }) -} From 45c80bfec1e588db6a368364c4921bbf81a0f9b4 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 10 Apr 2026 07:54:39 +0800 Subject: [PATCH 007/126] Make Markdown fenced code block work with more syntaxes (#37154) --- modules/markup/markdown/goldmark.go | 2 ++ modules/markup/markdown/markdown_test.go | 19 +++++++++++ .../markup/markdown/transform_codeblock.go | 32 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 modules/markup/markdown/transform_codeblock.go diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 555a171685b..4a560517f22 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -70,6 +70,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } case *ast.CodeSpan: g.transformCodeSpan(ctx, v, reader) + case *ast.FencedCodeBlock: + g.transformFencedCodeblock(v, reader) case *ast.Blockquote: return g.transformBlockquote(v, reader) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index e231b037cc1..2f14a0fae98 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -600,3 +600,22 @@ func TestMarkdownUlDir(t *testing.T) { `, string(result)) } + +func TestMarkdownFencedCodeBlock(t *testing.T) { + testRender := func(input, expected string) { + buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) + } + const nl = "\n" + const prefix = `
`
+	const suffix = `
` + + testRender("```\ncode\n```", prefix+`code`+nl+``+suffix) + + const jsCommon = prefix + `code` + nl + `` + suffix + testRender("```js\ncode\n```", jsCommon) + testRender("```js:app.ts\ncode\n```", jsCommon) + testRender("```js,ignore\ncode\n```", jsCommon) + testRender("```js ignore\ncode\n```", jsCommon) +} diff --git a/modules/markup/markdown/transform_codeblock.go b/modules/markup/markdown/transform_codeblock.go new file mode 100644 index 00000000000..de9264c4c49 --- /dev/null +++ b/modules/markup/markdown/transform_codeblock.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +func (g *ASTTransformer) transformFencedCodeblock(v *ast.FencedCodeBlock, reader text.Reader) { + // * Some engines support a meta syntax for appending the filename after the language, separated by a colon + // * https://www.glukhov.org/documentation-tools/markdown/markdown-codeblocks/ + // * Some engines support additional "options" after the language, separated by a space or comma: ```rust,ignore``` + // * https://docs.readme.com/rdmd/docs/code-blocks + // * https://next-book.vercel.app/reference/fencedcode + if v.Info == nil { + return + } + info := v.Info.Segment.Value(reader.Source()) + newEnd := -1 + for i, b := range info { + if b == ' ' || b == ',' || b == ':' { + newEnd = i + break + } + } + if newEnd != -1 { + start := v.Info.Segment.Start + v.Info = ast.NewTextSegment(text.NewSegment(start, start+newEnd)) + } +} From 681c4074e56b7f560dc95c36dbfa4b3f254dd4d9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 10 Apr 2026 12:39:28 +0200 Subject: [PATCH 008/126] Add missing `//nolint:depguard` (#37162) When running `golangci-lint` without `GOEXPERIMENT=jsonv2`, a lint error `import 'encoding/json' is not allowed` is seen. All other files in the module that import `encodings/json` have `//nolint` already, so add it. --- This PR was written with the help of Claude Opus 4.6 Co-authored-by: Claude (Opus 4.6) --- modules/json/jsonlegacy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/json/jsonlegacy.go b/modules/json/jsonlegacy.go index 83eabad4526..99875b96f12 100644 --- a/modules/json/jsonlegacy.go +++ b/modules/json/jsonlegacy.go @@ -6,7 +6,7 @@ package json import ( - "encoding/json" + "encoding/json" //nolint:depguard // this package wraps it "io" ) From 16d781733889a041151a0b0bd608174e6a2e6435 Mon Sep 17 00:00:00 2001 From: Elisei Roca Date: Fri, 10 Apr 2026 15:02:35 +0200 Subject: [PATCH 009/126] Implement logout redirection for reverse proxy auth setups (#36085) When authentication is handled externally by a reverse proxy SSO provider, users can be redirected to an external logout URL or relative path defined on the reverse proxy. --------- Co-authored-by: wxiaoguang --- custom/conf/app.example.ini | 5 +++++ modules/setting/security.go | 2 ++ routers/web/auth/auth.go | 7 ++++++- tests/integration/signout_test.go | 31 ++++++++++++++++++++++++------- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 26c512b6f6e..c714ed96600 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -461,6 +461,11 @@ INTERNAL_TOKEN = ;; Name of cookie used to store authentication information. ;COOKIE_REMEMBER_NAME = gitea_incredible ;; +;; URL or path that Gitea should redirect users to *after* performing its own logout. +;; Use this, if needed, when authentication is handled by a reverse proxy or SSO. +;; For example: "/my-sso/logout?return=/my-sso/home" +;REVERSE_PROXY_LOGOUT_REDIRECT = +;; ;; Reverse proxy authentication header name of user name, email, and full name ;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER ;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL diff --git a/modules/setting/security.go b/modules/setting/security.go index a1fd0bce2e5..152bcffd9fa 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -31,6 +31,7 @@ var ( ReverseProxyAuthEmail string ReverseProxyAuthFullName string ReverseProxyLimit int + ReverseProxyLogoutRedirect string ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool @@ -124,6 +125,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME") ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1) + ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String() ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",") if len(ReverseProxyTrustedProxies) == 0 { ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"} diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index e5e30667f07..d705062b3d8 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -493,12 +493,17 @@ func SignOut(ctx *context.Context) { } func buildSignOutRedirectURL(ctx *context.Context) string { - // TODO: can also support REVERSE_PROXY_AUTHENTICATION logout URL in the future if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 { if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" { return s } } + + // The assumption is: if reverse proxy auth is enabled, then the users should only sign-in via reverse proxy auth. + // TODO: in the future, if we need to distinguish different sign-in methods, we need to save the sign-in method in session and check here + if setting.Service.EnableReverseProxyAuth && setting.ReverseProxyLogoutRedirect != "" { + return setting.ReverseProxyLogoutRedirect + } return setting.AppSubURL + "/" } diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go index 0c0ac5dd87c..ff35dcf7a50 100644 --- a/tests/integration/signout_test.go +++ b/tests/integration/signout_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -16,13 +17,29 @@ import ( func TestSignOut(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") + t.Run("NormalLogout", func(t *testing.T) { + session := loginUser(t, "user2") - req := NewRequest(t, "GET", "/user/logout") - resp := session.MakeRequest(t, req, http.StatusSeeOther) - assert.Equal(t, "/", test.RedirectURL(resp)) + req := NewRequest(t, "GET", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", resp.Header().Get("Location")) - // try to view a private repo, should fail - req = NewRequest(t, "GET", "/user2/repo2") - session.MakeRequest(t, req, http.StatusNotFound) + // logged out, try to view a private repo, should fail + req = NewRequest(t, "GET", "/user2/repo2") + session.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("ReverseProxyLogoutRedirect", func(t *testing.T) { + defer test.MockVariableValue(&setting.Service.EnableReverseProxyAuth, true)() + defer test.MockVariableValue(&setting.ReverseProxyLogoutRedirect, "/my-sso/logout?return_to=/my-sso/home")() + + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/my-sso/logout?return_to=/my-sso/home", resp.Header().Get("Location")) + + // logged out, try to view a private repo, should fail + req = NewRequest(t, "GET", "/user2/repo2") + session.MakeRequest(t, req, http.StatusNotFound) + }) } From 09c2677b21e18c07afb7c97715b83270e2c1ccc8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 10 Apr 2026 19:34:12 +0200 Subject: [PATCH 010/126] Fix flaky `TestCatFileBatch/QueryTerminated` test (#37159) `TestCatFileBatch/QueryTerminated` relied on timing to distinguish `os.ErrClosed` vs `io.EOF` error paths. Replace `time.Sleep`-based synchronization with a channel-based hook on pipe close, making both error paths fully deterministic regardless of CI runner speed. Ref: https://github.com/go-gitea/gitea/actions/runs/24193070536/job/70615366804 Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- modules/git/catfile_batch_reader.go | 65 +++++++++++++++-------------- modules/git/catfile_batch_test.go | 45 ++++++++++---------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/modules/git/catfile_batch_reader.go b/modules/git/catfile_batch_reader.go index 0c8fc740bee..5727c4a8ac3 100644 --- a/modules/git/catfile_batch_reader.go +++ b/modules/git/catfile_batch_reader.go @@ -13,63 +13,45 @@ import ( "strconv" "strings" "sync/atomic" - "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) -var catFileBatchDebugWaitClose atomic.Int64 - type catFileBatchCommunicator struct { - closeFunc func(err error) + closeFunc atomic.Pointer[func(err error)] reqWriter io.Writer respReader *bufio.Reader debugGitCmd *gitcmd.Command } -func (b *catFileBatchCommunicator) Close() { - if b.closeFunc != nil { - b.closeFunc(nil) - b.closeFunc = nil +func (b *catFileBatchCommunicator) Close(err ...error) { + if fn := b.closeFunc.Swap(nil); fn != nil { + (*fn)(util.OptionalArg(err)) } } -// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function -func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) (ret *catFileBatchCommunicator) { +// newCatFileBatch opens git cat-file --batch/--batch-check/--batch-command command and prepares the stdin/stdout pipes for communication. +func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator { ctx, ctxCancel := context.WithCancelCause(ctx) - - // We often want to feed the commits in order into cat-file --batch, followed by their trees and subtrees as necessary. stdinWriter, stdoutReader, stdPipeClose := cmdCatFile.MakeStdinStdoutPipe() - pipeClose := func() { - if delay := catFileBatchDebugWaitClose.Load(); delay > 0 { - time.Sleep(time.Duration(delay)) // for testing purpose only - } - stdPipeClose() - } - closeFunc := func(err error) { - ctxCancel(err) - pipeClose() - } - return newCatFileBatchWithCloseFunc(ctx, repoPath, cmdCatFile, stdinWriter, stdoutReader, closeFunc) -} - -func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command, - stdinWriter gitcmd.PipeWriter, stdoutReader gitcmd.PipeReader, closeFunc func(err error), -) *catFileBatchCommunicator { ret := &catFileBatchCommunicator{ debugGitCmd: cmdCatFile, - closeFunc: closeFunc, reqWriter: stdinWriter, respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations } + ret.closeFunc.Store(new(func(err error) { + ctxCancel(err) + stdPipeClose() + })) err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx) if err != nil { log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err) // ideally here it should return the error, but it would require refactoring all callers // so just return a dummy communicator that does nothing, almost the same behavior as before, not bad - closeFunc(err) + ret.Close(err) return ret } @@ -78,12 +60,33 @@ func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFi if err != nil && !errors.Is(err, context.Canceled) { log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err) } - closeFunc(err) + ret.Close(err) }() return ret } +func (b *catFileBatchCommunicator) debugKill() (ret struct { + beforeClose chan struct{} + blockClose chan struct{} + afterClose chan struct{} +}, +) { + ret.beforeClose = make(chan struct{}) + ret.blockClose = make(chan struct{}) + ret.afterClose = make(chan struct{}) + oldCloseFunc := b.closeFunc.Load() + b.closeFunc.Store(new(func(err error) { + b.closeFunc.Store(nil) + close(ret.beforeClose) + <-ret.blockClose + (*oldCloseFunc)(err) + close(ret.afterClose) + })) + b.debugGitCmd.DebugKill() + return ret +} + // catFileBatchParseInfoLine reads the header line from cat-file --batch // We expect: SP SP LF // then leaving the rest of the stream " LF" to be read diff --git a/modules/git/catfile_batch_test.go b/modules/git/catfile_batch_test.go index 69662ffc1a7..782d34d2493 100644 --- a/modules/git/catfile_batch_test.go +++ b/modules/git/catfile_batch_test.go @@ -7,9 +7,7 @@ import ( "io" "os" "path/filepath" - "sync" "testing" - "time" "code.gitea.io/gitea/modules/test" @@ -39,13 +37,22 @@ func testCatFileBatch(t *testing.T) { require.Error(t, err) }) - simulateQueryTerminated := func(pipeCloseDelay, pipeReadDelay time.Duration) (errRead error) { - catFileBatchDebugWaitClose.Store(int64(pipeCloseDelay)) - defer catFileBatchDebugWaitClose.Store(0) + simulateQueryTerminated := func(t *testing.T, errBeforePipeClose, errAfterPipeClose error) { + readError := func(t *testing.T, r io.Reader, expectedErr error) { + if expectedErr == nil { + return // expectedErr == nil means this read should be skipped + } + n, err := r.Read(make([]byte, 100)) + assert.Zero(t, n) + assert.ErrorIs(t, err, expectedErr) + } + batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) defer batch.Close() - _, _ = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.NoError(t, err) + var c *catFileBatchCommunicator switch b := batch.(type) { case *catFileBatchLegacy: @@ -58,24 +65,18 @@ func testCatFileBatch(t *testing.T) { t.FailNow() } - wg := sync.WaitGroup{} - wg.Go(func() { - time.Sleep(pipeReadDelay) - var n int - n, errRead = c.respReader.Read(make([]byte, 100)) - assert.Zero(t, n) - }) - time.Sleep(10 * time.Millisecond) - c.debugGitCmd.DebugKill() - wg.Wait() - return errRead - } + require.NotEqual(t, errBeforePipeClose == nil, errAfterPipeClose == nil, "must set exactly one of the expected errors") + inceptor := c.debugKill() + <-inceptor.beforeClose // wait for the command's Close to be called, the pipe is not closed yet + readError(t, c.respReader, errBeforePipeClose) // then caller will read on an open pipe which will be closed soon + close(inceptor.blockClose) // continue to close the pipe + <-inceptor.afterClose // wait for the pipe to be closed + readError(t, c.respReader, errAfterPipeClose) // then caller will read on a closed pipe + } t.Run("QueryTerminated", func(t *testing.T) { - err := simulateQueryTerminated(0, 20*time.Millisecond) - assert.ErrorIs(t, err, os.ErrClosed) // pipes are closed faster - err = simulateQueryTerminated(40*time.Millisecond, 20*time.Millisecond) - assert.ErrorIs(t, err, io.EOF) // reader is faster + simulateQueryTerminated(t, io.EOF, nil) // reader is faster + simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster }) batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) From d913fae237bd555b30cad39750f8b00e6cf68019 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 11 Apr 2026 12:41:56 +0200 Subject: [PATCH 011/126] Remove dead CSS rules (#37173) Remove CSS rules whose HTML classes/IDs are no longer referenced in any template, Go source, or JavaScript/TypeScript file: - `.archived-icon`: removed from templates in c85bb62635 - `.bottom-line`: removed from blame rendering in 9c6aeb47f7 - `.commit-status-link`: removed from templates in f3c4baa84b - `.instruct-toggle`: removed from templates in 75e85c25c1 - `.runner-new-text`, `#runner-new`: never referenced outside CSS - `.ap-terminal`: stale, asciinema-player uses `.ap-term`, still not needed - `.scrolling.dimmable.dimmed`: dimmer stand-in never adds this class - `.markup span.align-center/align-right/float-left/float-right`: never produced by any renderer, sanitizer strips class attributes - `.markup ul.no-list`, `.markup ol.no-list`: same as above --- This PR was written with the help of Claude Opus 4.6 --------- Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- web_src/css/actions.css | 8 ---- web_src/css/base.css | 13 ------ web_src/css/markup/asciicast.css | 4 +- web_src/css/markup/content.css | 69 -------------------------------- web_src/css/modules/modal.css | 8 ---- web_src/css/repo.css | 9 ----- 6 files changed, 3 insertions(+), 108 deletions(-) diff --git a/web_src/css/actions.css b/web_src/css/actions.css index c43ebe21a05..14cf65f273d 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -6,14 +6,6 @@ overflow-x: auto; } -.runner-container .runner-new-text { - color: var(--color-white); -} - -.runner-container #runner-new:hover .runner-new-text { - color: var(--color-white) !important; -} - .runner-container .task-status-success { background-color: var(--color-green); color: var(--color-white); diff --git a/web_src/css/base.css b/web_src/css/base.css index a8d9dea2a25..c278132a001 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -643,10 +643,6 @@ overflow-menu .ui.label { color: var(--color-primary-contrast); } -.archived-icon { - color: var(--color-secondary-dark-2) !important; -} - .oauth2-authorize-application-box { margin-top: 3em !important; } @@ -670,10 +666,6 @@ overflow-menu .ui.label { min-width: 50px; } -.lines-num span.bottom-line::after { - border-bottom: 1px solid var(--color-secondary); -} - .lines-num span::after { content: attr(data-line-number); line-height: var(--line-height-code) !important; @@ -783,11 +775,6 @@ tr.top-line-blame:first-of-type { border-top: none; /* merge code lines belonging to the same commit into one block */ } -.lines-code .bottom-line, -.lines-commit .bottom-line { - border-bottom: 1px solid var(--color-secondary); -} - .migrate .svg.gitea-git { color: var(--color-git); } diff --git a/web_src/css/markup/asciicast.css b/web_src/css/markup/asciicast.css index 89696bc7105..a45daaa8e8b 100644 --- a/web_src/css/markup/asciicast.css +++ b/web_src/css/markup/asciicast.css @@ -3,6 +3,8 @@ height: auto; } -.ap-terminal { +/* 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/markup/content.css b/web_src/css/markup/content.css index e7a967a7c64..efa6947ef14 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -154,12 +154,6 @@ In markup content, we always use bottom margin for all elements */ padding-inline-start: 2em; } -.markup ul.no-list, -.markup ol.no-list { - padding: 0; - list-style-type: none; -} - .markup .task-list-item { list-style-type: none; } @@ -357,69 +351,6 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { color: var(--color-text); } -.markup span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markup span.align-center > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markup span.align-center span img, -.markup span.align-center span video { - margin: 0 auto; - text-align: center; -} - -.markup span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markup span.align-right > span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markup span.align-right span img, -.markup span.align-right span video { - margin: 0; - text-align: right; -} - -.markup span.float-left { - display: block; - float: left; - margin-inline-end: 13px; - overflow: hidden; -} - -.markup span.float-left span { - margin: 13px 0 0; -} - -.markup span.float-right { - display: block; - float: right; - margin-inline-start: 13px; - overflow: hidden; -} - -.markup span.float-right > span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - .markup code, .markup tt { padding: 0.2em 0.4em; diff --git a/web_src/css/modules/modal.css b/web_src/css/modules/modal.css index 5d686746cbe..d45e54b947b 100644 --- a/web_src/css/modules/modal.css +++ b/web_src/css/modules/modal.css @@ -159,19 +159,11 @@ display: block; } -.scrolling.dimmable.dimmed { - overflow: hidden; -} - .scrolling.dimmable > .dimmer { justify-content: flex-start; position: fixed; } -.scrolling.dimmable.dimmed > .dimmer { - overflow: auto; -} - .modals.dimmer .ui.scrolling.modal { margin: 2rem auto; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 95d6ca21695..923aef04ba4 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -287,10 +287,6 @@ td .commit-summary { min-width: 100px; } -.repository.view.issue .instruct-toggle { - display: inline-block; -} - /* issue title & meta & edit */ .issue-title-header { width: 100%; @@ -1463,11 +1459,6 @@ tbody.commit-list { } } -.commit-list .commit-status-link { - display: inline-block; - vertical-align: middle; -} - .commit-body { margin: 0.25em 0; white-space: pre-wrap; From ba9258c4788855e82c9dab5c7fa8f829797c7bd7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 11 Apr 2026 16:38:56 +0200 Subject: [PATCH 012/126] Indicate form field readonly via background (#37175) The `Run As Username` field on the install page was a `readonly` input that looked editable but wasn't, confusing users. Style `readonly` inputs with a subtle background, matching other frameworks. Fixes: #37174 Signed-off-by: wxiaoguang Signed-off-by: silverwind Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- options/locale/locale_en-US.json | 2 +- templates/devtest/form-fields.tmpl | 109 +++++++++++++++++++++++++++++ templates/install.tmpl | 2 +- web_src/css/modules/form.css | 8 ++- 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 templates/devtest/form-fields.tmpl diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9d61e3f1d77..778104fcc89 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -269,7 +269,7 @@ "install.lfs_path": "Git LFS Root Path", "install.lfs_path_helper": "Files tracked by Git LFS will be stored in this directory. Leave empty to disable.", "install.run_user": "Run As Username", - "install.run_user_helper": "The operating system username that Gitea runs as. Note that this user must have access to the repository root path.", + "install.run_user_helper": "The operating system username that Gitea runs as, it must have write access to the data paths. This value is auto-detected and cannot be changed here. To use a different user, restart Gitea under that account.", "install.domain": "Server Domain", "install.domain_helper": "Domain or host address for the server.", "install.ssh_port": "SSH Server Port", diff --git a/templates/devtest/form-fields.tmpl b/templates/devtest/form-fields.tmpl new file mode 100644 index 00000000000..ee6df2e813f --- /dev/null +++ b/templates/devtest/form-fields.tmpl @@ -0,0 +1,109 @@ +{{template "devtest/devtest-header"}} +
+
+

Input

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Textarea

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Dropdown

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Required

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+{{template "devtest/devtest-footer"}} diff --git a/templates/install.tmpl b/templates/install.tmpl index 45f14d5c575..bc6fed08e95 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -117,7 +117,7 @@ {{ctx.Locale.Tr "install.lfs_path_helper"}}
-
+
{{ctx.Locale.Tr "install.run_user_helper"}} diff --git a/web_src/css/modules/form.css b/web_src/css/modules/form.css index 2d315786c6f..2999f64cf6b 100644 --- a/web_src/css/modules/form.css +++ b/web_src/css/modules/form.css @@ -99,6 +99,13 @@ textarea:focus, color: var(--color-input-text); } +.ui.form input:not([type="checkbox"], [type="radio"])[readonly], +.ui.form textarea[readonly], +.ui.form select[readonly], +.ui.form .ui.selection.dropdown[readonly] { + background: var(--color-secondary-bg); +} + .ui.input { color: var(--color-input-text); } @@ -198,7 +205,6 @@ textarea:focus, background-color: var(--color-error-bg); border-color: var(--color-error-border); color: var(--color-error-text); - border-radius: 0; } .ui.form .field.error textarea:focus, .ui.form .field.error select:focus, From 8fcbdf05b0a94f2997dffbb40aa587e2b5c56e87 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 10:17:25 +0800 Subject: [PATCH 013/126] Refactor flash message and remove SanitizeHTML template func (#37179) 1. Fix the "flash message" layout problem for different cases * I am sure most of the users should have ever seen the ugly center-aligned error message with multiple lines. 2. Fix inconsistent "Details" flash message EOL handling, sometimes `\n`, sometimes `
` * Now, always use "\n" and use `
` to render
3. Remove SanitizeHTML template func because it is not useful and can be
easily abused.
* But it is still kept for mail templates, for example:
https://github.com/go-gitea/gitea/issues/36049
4. Clarify PostProcessCommitMessage's behavior and add FIXME comment

By the way: cleaned up some devtest pages, move embedded style block to
CSS file
---
 models/issues/comment.go                      |  7 ++++
 modules/markup/html.go                        | 10 +++--
 modules/templates/helper.go                   | 18 ++++-----
 modules/templates/helper_test.go              |  2 +-
 modules/templates/mail.go                     | 17 ++++----
 modules/templates/util_render.go              | 39 +++++++++++++++++--
 routers/utils/utils.go                        |  9 +++--
 routers/utils/utils_test.go                   |  9 +++--
 routers/web/auth/oauth.go                     |  2 +-
 routers/web/devtest/devtest.go                | 28 +++++++++++--
 routers/web/feed/convert.go                   |  3 +-
 routers/web/repo/branch.go                    |  2 +-
 routers/web/repo/commit.go                    |  3 +-
 routers/web/repo/editor_error.go              |  6 +--
 routers/web/repo/issue_new.go                 |  2 +-
 routers/web/repo/issue_view.go                |  5 +--
 routers/web/repo/pull.go                      | 16 ++++----
 templates/base/alert.tmpl                     | 28 +++----------
 templates/base/alert_details.tmpl             |  6 +--
 templates/devtest/devtest-header.tmpl         |  2 +-
 templates/devtest/fetch-action.tmpl           | 18 ++-------
 .../{toast.tmpl => toast-and-message.tmpl}    |  2 +-
 templates/repo/commit_page.tmpl               |  2 +-
 .../repo/issue/view_content/comments.tmpl     |  2 +-
 tests/integration/pull_merge_test.go          |  5 +--
 tests/integration/signin_test.go              |  3 +-
 tests/integration/user_settings_test.go       |  4 +-
 web_src/css/base.css                          | 12 +++++-
 web_src/css/devtest.css                       | 10 +++--
 29 files changed, 159 insertions(+), 113 deletions(-)
 rename templates/devtest/{toast.tmpl => toast-and-message.tmpl} (96%)

diff --git a/models/issues/comment.go b/models/issues/comment.go
index 34ce7f35004..84a7150b9f7 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/htmlutil"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/optional"
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/structs"
@@ -543,6 +544,12 @@ func (c *Comment) EventTag() string {
 	return fmt.Sprintf("event-%d", c.ID)
 }
 
+func (c *Comment) GetSanitizedContentHTML() template.HTML {
+	// mainly for type=4 CommentTypeCommitRef
+	// the content is a link like message title (from CreateRefComment)
+	return markup.Sanitize(c.Content)
+}
+
 // LoadLabel if comment.Type is CommentTypeLabel, then load Label
 func (c *Comment) LoadLabel(ctx context.Context) error {
 	var label Label
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 1c2ae6918de..0fe37ae3052 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -6,6 +6,7 @@ package markup
 import (
 	"bytes"
 	"fmt"
+	"html/template"
 	"io"
 	"regexp"
 	"slices"
@@ -149,9 +150,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
 	return postProcess(ctx, procs, input, output)
 }
 
-// PostProcessCommitMessage will use the same logic as PostProcess, but will disable
-// the shortLinkProcessor.
-func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) {
+// 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) {
 	procs := []processor{
 		fullIssuePatternProcessor,
 		comparePatternProcessor,
@@ -165,7 +166,8 @@ func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error
 		emojiProcessor,
 		emojiShortCodeProcessor,
 	}
-	return postProcessString(ctx, procs, content)
+	s, err := postProcessString(ctx, procs, string(content))
+	return template.HTML(s), err
 }
 
 var emojiProcessors = []processor{
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index f81be1255ab..3e4289c8ada 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -32,13 +32,12 @@ func newFuncMapWebPage() template.FuncMap {
 
 		// -----------------------------------------------------------------
 		// html/template related functions
-		"dict":         dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
-		"Iif":          iif,
-		"Eval":         evalTokens,
-		"HTMLFormat":   htmlFormat,
-		"QueryEscape":  queryEscape,
-		"QueryBuild":   QueryBuild,
-		"SanitizeHTML": SanitizeHTML,
+		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
+		"Iif":         iif,
+		"Eval":        evalTokens,
+		"HTMLFormat":  htmlFormat,
+		"QueryEscape": queryEscape,
+		"QueryBuild":  QueryBuild,
 
 		"PathEscape":         url.PathEscape,
 		"PathEscapeSegments": util.PathEscapeSegments,
@@ -146,9 +145,8 @@ func newFuncMapWebPage() template.FuncMap {
 	}
 }
 
-// SanitizeHTML sanitizes the input by default sanitization rules.
-func SanitizeHTML(s string) template.HTML {
-	return markup.Sanitize(s)
+func sanitizeHTML(msg string) template.HTML {
+	return markup.Sanitize(msg)
 }
 
 func htmlFormat(s any, args ...any) template.HTML {
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index f90818c0ad9..cf1db324768 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -58,7 +58,7 @@ func TestSubjectBodySeparator(t *testing.T) {
 }
 
 func TestSanitizeHTML(t *testing.T) {
-	assert.Equal(t, template.HTML(`link xss 
inline
`), SanitizeHTML(`link xss
inline
`)) + assert.Equal(t, template.HTML(`link xss
inline
`), sanitizeHTML(`link xss
inline
`)) } func TestTemplateIif(t *testing.T) { diff --git a/modules/templates/mail.go b/modules/templates/mail.go index 181c6312b03..f81073902f3 100644 --- a/modules/templates/mail.go +++ b/modules/templates/mail.go @@ -65,13 +65,16 @@ func mailBodyFuncMap() template.FuncMap { "NIL": func() any { return nil }, // html/template related functions - "dict": dict, - "Iif": iif, - "Eval": evalTokens, - "HTMLFormat": htmlFormat, - "QueryEscape": queryEscape, - "QueryBuild": QueryBuild, - "SanitizeHTML": SanitizeHTML, + "dict": dict, + "Iif": iif, + "Eval": evalTokens, + "HTMLFormat": htmlFormat, + "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, + + // deprecated, use "HTMLFormat" instead, but some user custom mail templates still use it + // see: https://github.com/go-gitea/gitea/issues/36049 + "SanitizeHTML": sanitizeHTML, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 081a13fb9ec..6641ee79594 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -40,7 +40,7 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { // RenderCommitMessage renders commit message with XSS-safe and special links. func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { - cleanMsg := template.HTMLEscapeString(msg) + 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) @@ -48,7 +48,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te log.Error("PostProcessCommitMessage: %v", err) return "" } - msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") + msgLines := strings.Split(strings.TrimSpace(string(fullMessage)), "\n") if len(msgLines) == 0 { return "" } @@ -91,12 +91,14 @@ func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) templ return "" } - renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) + rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) + htmlContent := template.HTML(template.HTMLEscapeString(msgLine)) + renderedMessage, err := markup.PostProcessCommitMessage(rctx, htmlContent) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" } - return template.HTML(renderedMessage) + return renderedMessage } // Match text that is between back ticks. @@ -279,6 +281,35 @@ func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize in return htmlutil.HTMLFormat(`
%s %s %s
`, info.GetDescription(), icon, info.DisplayName, extraIcon) } +func (ut *RenderUtils) RenderFlashMessage(typ, msg string) template.HTML { + msg = strings.TrimSpace(msg) + if msg == "" { + return "" + } + + cls := typ + // legacy logic: "negative" for error, "positive" for success + switch cls { + case "error": + cls = "negative" + case "success": + cls = "positive" + } + + var msgContent template.HTML + if strings.Contains(msg, "
") || strings.Contains(msg, "") || strings.Contains(msg, "") || strings.Contains(msg, "
") { + // If the message contains some known "block" elements, no need to do more alignment or line-break processing, just sanitize it directly. + msgContent = sanitizeHTML(msg) + } else if !strings.Contains(msg, "\n") { + // If the message is a single line, center-align it by wrapping it + msgContent = htmlutil.HTMLFormat(`
%s
`, sanitizeHTML(msg)) + } else { + // For a multi-line message, preserve line breaks, and left-align it. + msgContent = htmlutil.HTMLFormat(`%s`, sanitizeHTML(strings.ReplaceAll(msg, "\n", "
"))) + } + return htmlutil.HTMLFormat(`
%s
`, cls, typ, msgContent) +} + func (ut *RenderUtils) RenderUnicodeEscapeToggleButton(escapeStatus *charset.EscapeStatus) template.HTML { if escapeStatus == nil || !escapeStatus.Escaped { return "" diff --git a/routers/utils/utils.go b/routers/utils/utils.go index 3035073d5c5..47ca13b2aa5 100644 --- a/routers/utils/utils.go +++ b/routers/utils/utils.go @@ -5,10 +5,11 @@ package utils import ( "html" - "strings" + "html/template" ) -// SanitizeFlashErrorString will sanitize a flash error string -func SanitizeFlashErrorString(x string) string { - return strings.ReplaceAll(html.EscapeString(x), "\n", "
") +// EscapeFlashErrorString will escape the flash error string +// Maybe do more sanitization in the future, e.g.: hide sensitive information, etc. +func EscapeFlashErrorString(x string) template.HTML { + return template.HTML(html.EscapeString(x)) } diff --git a/routers/utils/utils_test.go b/routers/utils/utils_test.go index cc7c888a758..5dfc5447774 100644 --- a/routers/utils/utils_test.go +++ b/routers/utils/utils_test.go @@ -4,16 +4,17 @@ package utils import ( + "html/template" "testing" "github.com/stretchr/testify/assert" ) -func TestSanitizeFlashErrorString(t *testing.T) { +func TestEscapeFlashErrorString(t *testing.T) { tests := []struct { name string arg string - want string + want template.HTML }{ { name: "no error", @@ -28,13 +29,13 @@ func TestSanitizeFlashErrorString(t *testing.T) { { name: "line break error", arg: "some error:\n\nawesome!", - want: "some error:

awesome!", + want: "some error:\n\nawesome!", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := SanitizeFlashErrorString(tt.arg) + got := EscapeFlashErrorString(tt.arg) assert.Equal(t, tt.want, got) }) } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index c103d200645..8645aedbdee 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -79,7 +79,7 @@ func SignInOAuthCallback(ctx *context.Context) { } } sort.Strings(errorKeyValues) - ctx.Flash.Error(strings.Join(errorKeyValues, "
"), true) + ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true) } // first look if the provider is still active diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 5cfe08c7fe0..8bc5947df8f 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -45,8 +45,8 @@ func List(ctx *context.Context) { func FetchActionTest(ctx *context.Context) { _ = ctx.Req.ParseForm() - ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "
" + - "Form: " + ctx.Req.Form.Encode() + "
" + + ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + + "Form: " + ctx.Req.Form.Encode() + "\n" + "PostForm: " + ctx.Req.PostForm.Encode(), ) time.Sleep(2 * time.Second) @@ -192,11 +192,31 @@ func prepareMockData(ctx *context.Context) { prepareMockDataBadgeActionsSvg(ctx) case "/devtest/relative-time": prepareMockDataRelativeTime(ctx) + case "/devtest/toast-and-message": + prepareMockDataToastAndMessage(ctx) case "/devtest/unicode-escape": prepareMockDataUnicodeEscape(ctx) } } +func prepareMockDataToastAndMessage(ctx *context.Context) { + msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with details ", + "Summary": "summary with details", + "Details": "details line 1\n details line 2\n details line 3", + }) + msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{ + "Message": "message with summary ", + "Summary": "summary only", + }) + + ctx.Flash.ErrorMsg = string(msgWithDetails) + ctx.Flash.WarningMsg = string(msgWithSummary) + ctx.Flash.InfoMsg = "a long message with line break\nthe second line " + ctx.Flash.SuccessMsg = "single line message " + ctx.Data["Flash"] = ctx.Flash +} + func prepareMockDataUnicodeEscape(ctx *context.Context) { content := "// demo code\n" content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n" @@ -223,8 +243,8 @@ func TmplCommon(ctx *context.Context) { prepareMockData(ctx) if ctx.Req.Method == http.MethodPost { _ = ctx.Req.ParseForm() - ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"
"+ - "Form: "+ctx.Req.Form.Encode()+"
"+ + ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ + "Form: "+ctx.Req.Form.Encode()+"\n"+ "PostForm: "+ctx.Req.PostForm.Encode(), true, ) diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go index a5c379e01a9..5d208bb2861 100644 --- a/routers/web/feed/convert.go +++ b/routers/web/feed/convert.go @@ -15,6 +15,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio } } if len(content) == 0 { - content = templates.SanitizeHTML(desc) + content = markup.Sanitize(desc) } items = append(items, &feeds.Item{ diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5e5cfec5c2b..c566e465e9d 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -231,7 +231,7 @@ func CreateBranch(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.push_rejected"), "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(e.Message), + "Details": utils.EscapeFlashErrorString(e.Message), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 168d9594940..34e588b1416 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -410,7 +410,8 @@ func Diff(ctx *context.Context) { ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))}) - ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) + 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 diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go index e1473a34b39..f23b2738e5e 100644 --- a/routers/web/repo/editor_error.go +++ b/routers/web/repo/editor_error.go @@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": message, "Summary": summary, - "Details": utils.SanitizeFlashErrorString(details), + "Details": utils.EscapeFlashErrorString(details), }) if err == nil { ctx.JSONError(flashError) } else { - log.Error("RenderToHTML: %v", err) - ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err) + ctx.JSONError("Unable to render error details, see server logs") // it should never happen } } diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 98fb842ddf7..592d902ba8e 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -170,7 +170,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) templat flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.issues.choose.ignore_invalid_templates"), "Summary": ctx.Tr("repo.issues.choose.invalid_templates", len(errs)), - "Details": utils.SanitizeFlashErrorString(strings.Join(lines, "\n")), + "Details": utils.EscapeFlashErrorString(strings.Join(lines, "\n")), }) if err != nil { log.Debug("render flash error: %v", err) diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 250a54fc24a..f678f838784 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -29,7 +29,6 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web/middleware" @@ -781,14 +780,14 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } else if comment.Type == issues_model.CommentTypeAddTimeManual || comment.Type == issues_model.CommentTypeStopTracking || comment.Type == issues_model.CommentTypeDeleteTimeManual { - // drop error since times could be pruned from DB.. + // drop error since times could be pruned from DB _ = comment.LoadTime(ctx) if comment.Content != "" { // Content before v1.21 did store the formatted string instead of seconds, // so "|" is used as delimiter to mark the new format if comment.Content[0] != '|' { // handle old time comments that have formatted text stored - comment.RenderedContent = templates.SanitizeHTML(comment.Content) + comment.RenderedContent = markup.Sanitize(comment.Content) comment.Content = "" } else { // else it's just a duration in seconds to pass on to the frontend diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index e312fc9d2a8..efcdaac6740 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1042,7 +1042,7 @@ func UpdatePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.merge_conflict"), "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1054,9 +1054,9 @@ func UpdatePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("UpdatePullRequest.HTMLString", err) @@ -1191,7 +1191,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.editor.merge_conflict"), "Summary": ctx.Tr("repo.editor.merge_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1202,9 +1202,9 @@ func MergePullRequest(ctx *context.Context) { } else if pull_service.IsErrRebaseConflicts(err) { conflictError := err.(pull_service.ErrRebaseConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)), + "Message": ctx.Tr("repo.pulls.rebase_conflict", utils.EscapeFlashErrorString(conflictError.CommitSHA)), "Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"), - "Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "
" + utils.SanitizeFlashErrorString(conflictError.StdOut), + "Details": utils.EscapeFlashErrorString(conflictError.StdErr) + "\n" + utils.EscapeFlashErrorString(conflictError.StdOut), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1234,7 +1234,7 @@ func MergePullRequest(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("MergePullRequest.HTMLString", err) @@ -1454,7 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ "Message": ctx.Tr("repo.pulls.push_rejected"), "Summary": ctx.Tr("repo.pulls.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(pushrejErr.Message), + "Details": utils.EscapeFlashErrorString(pushrejErr.Message), }) if err != nil { ctx.ServerError("CompareAndPullRequest.HTMLString", err) diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 5ebe1917712..242f6278ea7 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -1,25 +1,9 @@ -{{- if .Flash.ErrorMsg -}} -
-

{{.Flash.ErrorMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.SuccessMsg -}} -
-

{{.Flash.SuccessMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.InfoMsg -}} -
-

{{.Flash.InfoMsg | SanitizeHTML}}

-
-{{- end -}} -{{- if .Flash.WarningMsg -}} -
-

{{.Flash.WarningMsg | SanitizeHTML}}

-
-{{- end -}} +{{- if .Flash.ErrorMsg}}{{ctx.RenderUtils.RenderFlashMessage "error" .Flash.ErrorMsg}}{{end -}} +{{- if .Flash.WarningMsg}}{{ctx.RenderUtils.RenderFlashMessage "warning" .Flash.WarningMsg}}{{end -}} +{{- if .Flash.InfoMsg}}{{ctx.RenderUtils.RenderFlashMessage "info" .Flash.InfoMsg}}{{end -}} +{{- if .Flash.SuccessMsg}}{{ctx.RenderUtils.RenderFlashMessage "success" .Flash.SuccessMsg}}{{end -}} {{- if .ShowTwoFactorRequiredMessage -}} -
-

{{ctx.Locale.Tr "auth.twofa_required"}}

+ {{- end -}} diff --git a/templates/base/alert_details.tmpl b/templates/base/alert_details.tmpl index 6380a72498c..da8aba452a6 100644 --- a/templates/base/alert_details.tmpl +++ b/templates/base/alert_details.tmpl @@ -2,10 +2,8 @@ {{if .Details}}
{{.Summary}} - {{.Details | SanitizeHTML}} +
{{.Details}}
{{else}} -
- {{.Summary}} -
+
{{.Summary}}
{{end}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 628e4388a0c..c9d7b3047fe 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,4 +1,4 @@ {{template "base/head" ctx.RootData}}
-{{template "base/alert" .}} +
{{template "base/alert" ctx.RootData}}
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl index 4ee824f04be..cd4da52aac3 100644 --- a/templates/devtest/fetch-action.tmpl +++ b/templates/devtest/fetch-action.tmpl @@ -1,6 +1,5 @@ {{template "devtest/devtest-header"}}
- {{template "base/alert" .}}

link-action

@@ -17,29 +16,20 @@

form-fetch-action

Use "window.fetch" to send a form request to backend
-
-
+
+ -
+
-
+
bad action url
- {{template "devtest/devtest-footer"}} diff --git a/templates/devtest/toast.tmpl b/templates/devtest/toast-and-message.tmpl similarity index 96% rename from templates/devtest/toast.tmpl rename to templates/devtest/toast-and-message.tmpl index 597b4154695..c4056b6fc6b 100644 --- a/templates/devtest/toast.tmpl +++ b/templates/devtest/toast-and-message.tmpl @@ -1,5 +1,5 @@ {{template "devtest/devtest-header"}} -
+

Toast

diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 179f6018d3d..8451a6f3cf1 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -195,7 +195,7 @@ {{DateUtils.TimeSince .NoteCommit.Author.When}}
-
{{.NoteRendered | SanitizeHTML}}
+
{{.NoteRendered}}
{{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 8c08f1c1f1c..6a67ef754d7 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -164,7 +164,7 @@
{{svg "octicon-git-commit"}} {{/* the content is a link like message title (from CreateRefComment) */}} - {{.Content | SanitizeHTML}} + {{.GetSanitizedContentHTML}}
{{else if eq .Type 7}} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 53709e6ff4b..d0213962ee7 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -317,9 +317,8 @@ func TestPullCleanUpAfterMerge(t *testing.T) { resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - - assert.Equal(t, "Branch \"user1/repo1:feature/test\" has been deleted.", resultMsg) + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) + assert.Equal(t, `Branch "user1/repo1:feature/test" has been deleted.`, resultMsg) }) } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index ff35baae9db..4000c7ebe1d 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -36,8 +36,7 @@ func testLoginFailed(t *testing.T, username, password, message string) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - resultMsg := htmlDoc.doc.Find(".ui.message>p").Text() - + resultMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, message, resultMsg) } diff --git a/tests/integration/user_settings_test.go b/tests/integration/user_settings_test.go index 20c758dc85a..b3527dd467a 100644 --- a/tests/integration/user_settings_test.go +++ b/tests/integration/user_settings_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "strings" "testing" "code.gitea.io/gitea/modules/container" @@ -309,8 +310,7 @@ func TestUserSettingsApplications(t *testing.T) { }) resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) - - msg := doc.Find(".flash-error p").Text() + msg := strings.TrimSpace(doc.Find(".ui.message.flash-message").Text()) assert.Equal(t, `form.RedirectURIs"ftp://127.0.0.1" is not a valid URL.`, msg) }) diff --git a/web_src/css/base.css b/web_src/css/base.css index c278132a001..4448af68e0c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -431,8 +431,9 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -.ui .message.flash-message { - text-align: center; +.ui.message.flash-message pre { + white-space: pre-line; + margin: 0; } .ui .header > i + .content { @@ -865,6 +866,13 @@ table th[data-sortt-desc] .svg { align-items: stretch; } +/* can be used to replace "ui relaxed list" or "tw-flex tw-flex-col tw-gap-xxx" when we need more flexible layout */ +.flex-relaxed-list { + display: flex; + flex-direction: column; + gap: var(--gap-block); +} + .ui.list.flex-items-block > .item, .ui.vertical.menu.flex-items-block > .item, .ui.form .field > label.flex-text-block, /* override fomantic "block" style */ diff --git a/web_src/css/devtest.css b/web_src/css/devtest.css index a7b00e1e561..c344d99058b 100644 --- a/web_src/css/devtest.css +++ b/web_src/css/devtest.css @@ -1,3 +1,8 @@ +h1, h2 { + margin: 0; + padding: 10px 0; +} + .button-sample-groups { margin: 0; padding: 0; } @@ -10,7 +15,6 @@ margin-bottom: 5px; } -h1, h2 { - margin: 0; - padding: 10px 0; +.fetch-action-demo-forms .form-fetch-action { + border: 1px red dashed; /* show the border for demo purpose */ } From c2fa157731962334c3e7233e8431598f6be804b3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 12 Apr 2026 10:52:12 +0800 Subject: [PATCH 014/126] Remove outdated RunUser logic (#37180) That logic is from 2014~2015, it unclear why it is necessary or whether it is still needed (whether Windows is still special) The comment "so just use current one if config says default" is not right anymore: "git" isn't the "default" value of RunUser (Comment out app.example.ini #15807). The RunUser's value is from current session's username. --- custom/conf/app.example.ini | 4 ++-- modules/setting/setting.go | 2 +- options/locale/locale_en-US.json | 1 - routers/install/install.go | 18 +----------------- 4 files changed, 4 insertions(+), 21 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index c714ed96600..ef276e4da58 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -41,10 +41,10 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; App name that shows in every page title -APP_NAME = ; Gitea: Git with a cup of tea +;APP_NAME = Gitea: Git with a cup of tea ;; ;; RUN_USER will automatically detect the current user - but you can set it here change it if you run locally -RUN_USER = ; git +;RUN_USER = ;; ;; Application run mode, affects performance and debugging: "dev" or "prod", default is "prod" ;; Mode "dev" makes Gitea easier to develop and debug, values other than "dev" are treated as "prod" which is for production use. diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 2009be0bbd3..3c1ad144282 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -201,7 +201,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) { if HasInstallLock(rootCfg) { currentUser, match := IsRunUserMatchCurrentUser(RunUser) if !match { - log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser) + log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser) } } } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 778104fcc89..b9d5247b3d4 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -316,7 +316,6 @@ "install.invalid_db_table": "The database table \"%s\" is invalid: %v", "install.invalid_repo_path": "The repository root path is invalid: %v", "install.invalid_app_data_path": "The app data path is invalid: %v", - "install.run_user_not_match": "The 'run as' username is not the current username: %s -> %s", "install.internal_token_failed": "Failed to generate internal token: %v", "install.secret_key_failed": "Failed to generate secret key: %v", "install.save_config_failed": "Failed to save configuration: %v", diff --git a/routers/install/install.go b/routers/install/install.go index dec0b31e5cd..a0f32fb939c 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -26,7 +26,6 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/user" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/routers/common" @@ -87,15 +86,7 @@ func Install(ctx *context.Context) { form.AppName = setting.AppName form.RepoRootPath = setting.RepoRootPath form.LFSRootPath = setting.LFS.Storage.Path - - // Note(unknown): it's hard for Windows users change a running user, - // so just use current one if config says default. - if setting.IsWindows && setting.RunUser == "git" { - form.RunUser = user.CurrentUsername() - } else { - form.RunUser = setting.RunUser - } - + form.RunUser = setting.RunUser form.Domain = setting.Domain form.SSHPort = setting.SSH.Port form.HTTPPort = setting.HTTPPort @@ -272,13 +263,6 @@ func SubmitInstall(ctx *context.Context) { return } - currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser) - if !match { - ctx.Data["Err_RunUser"] = true - ctx.RenderWithErrDeprecated(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) - return - } - // Check logic loophole between disable self-registration and no admin account. if form.DisableRegistration && len(form.AdminName) == 0 { ctx.Data["Err_Services"] = true From 355aafd1f97926af1794f70d49c8ff68378db15e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:51:54 +0000 Subject: [PATCH 015/126] Update Nix flake (#37183) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 25ce7939b35..8ec14d28526 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1775036866, - "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", + "lastModified": 1775710090, + "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", + "rev": "4c1018dae018162ec878d42fec712642d214fdfa", "type": "github" }, "original": { From 47fdf3e284308c6b648936b5c15e136b08f5e1da Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 12 Apr 2026 20:26:02 +0200 Subject: [PATCH 016/126] Improve Contributing docs and set a release schedule (#37109) This PR updates `CONTRIBUTING.md` for clarity (code review, maintainers, PR workflow) ## Suggestion - majors about every **three months**, with a more predictable cadence from **v1.26** onward. - target dates such as **v1.26.0** (April 2026), **v1.27.0** (June 2026), **v1.28.0** (September 2026), **v1.29.0** (December 2026). - announce feature freeze **two weeks** before each release. ## Other doc changes - Reviewing PRs: separate guidance for reviewers vs authors; small edits to maintaining PRs, merge queue, commit messages, co-authors. - Maintainers: clearer subsections; links to GitHub Docs for 2FA / GPG. - Split the Contributing.md into more useful markdown files --------- Signed-off-by: Nicolas --- CONTRIBUTING.md | 399 ++--------------------------------- docs/community-governance.md | 198 +++++++++++++++++ docs/guideline-backend.md | 58 +++++ docs/guideline-frontend.md | 17 ++ docs/release-management.md | 115 ++++++++++ 5 files changed, 411 insertions(+), 376 deletions(-) create mode 100644 docs/community-governance.md create mode 100644 docs/guideline-backend.md create mode 100644 docs/guideline-frontend.md create mode 100644 docs/release-management.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b32631d6a7..aea31640b32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,14 @@ # Contribution Guidelines +This document explains how to contribute changes to the Gitea project. Topic-specific guides live in separate files so the essentials are easier to find. + +| Topic | Document | +| :---- | :------- | +| Backend (Go modules, API v1) | [docs/guideline-backend.md](docs/guideline-backend.md) | +| Frontend (npm, UI guidelines) | [docs/guideline-frontend.md](docs/guideline-frontend.md) | +| Maintainers, TOC, labels, merge queue, commit format for mergers | [docs/community-governance.md](docs/community-governance.md) | +| Release cycle, backports, tagging releases | [docs/release-management.md](docs/release-management.md) | +
Table of Contents - [Contribution Guidelines](#contribution-guidelines) @@ -11,10 +20,6 @@ - [Discuss your design before the implementation](#discuss-your-design-before-the-implementation) - [Issue locking](#issue-locking) - [Building Gitea](#building-gitea) - - [Dependencies](#dependencies) - - [Backend](#backend) - - [Frontend](#frontend) - - [Design guideline](#design-guideline) - [Styleguide](#styleguide) - [Copyright](#copyright) - [Testing](#testing) @@ -22,47 +27,19 @@ - [Code review](#code-review) - [Pull request format](#pull-request-format) - [PR title and summary](#pr-title-and-summary) - - [Milestone](#milestone) - - [Labels](#labels) - [Breaking PRs](#breaking-prs) - [What is a breaking PR?](#what-is-a-breaking-pr) - [How to handle breaking PRs?](#how-to-handle-breaking-prs) - [Maintaining open PRs](#maintaining-open-prs) - - [Getting PRs merged](#getting-prs-merged) - - [Final call](#final-call) - - [Commit messages](#commit-messages) - - [PR Co-authors](#pr-co-authors) - - [PRs targeting `main`](#prs-targeting-main) - - [Backport PRs](#backport-prs) + - [Reviewing PRs](#reviewing-prs) + - [For PR authors](#for-pr-authors) - [Documentation](#documentation) - - [API v1](#api-v1) - - [GitHub API compatibility](#github-api-compatibility) - - [Adding/Maintaining API routes](#addingmaintaining-api-routes) - - [When to use what HTTP method](#when-to-use-what-http-method) - - [Requirements for API routes](#requirements-for-api-routes) - - [Backports and Frontports](#backports-and-frontports) - - [What is backported?](#what-is-backported) - - [How to backport?](#how-to-backport) - - [Format of backport PRs](#format-of-backport-prs) - - [Frontports](#frontports) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - - [Release Cycle](#release-cycle) - - [Maintainers](#maintainers) - - [Technical Oversight Committee (TOC)](#technical-oversight-committee-toc) - - [TOC election process](#toc-election-process) - - [Current TOC members](#current-toc-members) - - [Previous TOC/owners members](#previous-tocowners-members) - - [Governance Compensation](#governance-compensation) - - [TOC \& Working groups](#toc--working-groups) - - [Roadmap](#roadmap) - - [Versions](#versions) - - [Releasing Gitea](#releasing-gitea)
## Introduction -This document explains how to contribute changes to the Gitea project. \ It assumes you have followed the [installation instructions](https://docs.gitea.com/category/installation). \ Sensitive security-related issues should be reported to [security@gitea.io](mailto:security@gitea.io). @@ -131,34 +108,6 @@ If further discussion is needed, we encourage you to open a new issue instead an See the [development setup instructions](https://docs.gitea.com/development/hacking-on-gitea). -## Dependencies - -### Backend - -Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ -You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). - -Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ -Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. - -The `go.mod`, `go.sum` update needs to be justified as part of the PR description, -and must be verified by the reviewers and/or merger to always reference -an existing upstream commit. - -### Frontend - -For the frontend, we use [npm](https://www.npmjs.com/). - -The same restrictions apply for frontend dependencies as for backend dependencies, with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. - -## Design guideline - -Depending on your change, please read the - -- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) -- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) -- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) - ## Styleguide You should always run `make fmt` before committing to conform to Gitea's styleguide. @@ -216,6 +165,8 @@ The tool `go run build/backport-locale.go` can be used to backport locales from ## Code review +How labels, milestones, and the merge queue work is documented in [docs/community-governance.md](docs/community-governance.md). + ### Pull request format Please try to make your pull request easy to review for us. \ @@ -260,29 +211,6 @@ Fixes/Closes/Resolves #. to your summary. \ Each issue that will be closed must stand on a separate line. -### Milestone - -A PR should only be assigned to a milestone if it will likely be merged into the given version. \ -As a rule of thumb, assume that a PR will stay open for an additional month for every 100 added lines. \ -PRs without a milestone may not be merged. - -### Labels - -Almost all labels used inside Gitea can be classified as one of the following: - -- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. -- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. -- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). -- `issue/…` / `pr/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `pr/need-2-approvals` - -Every PR should be labeled correctly with every label that applies. - -There are also some labels that will be managed automatically.\ -In particular, these are - -- the amount of pending required approvals -- has all `backport`s or needs a manual backport - ### Breaking PRs #### What is a breaking PR? @@ -311,165 +239,29 @@ Breaking PRs will not be merged as long as not both of these requirements are me ### Maintaining open PRs -The moment you create a non-draft PR or the moment you convert a draft PR to a non-draft PR is the moment code review starts for it. \ -Once that happens, do not rebase or squash your branch anymore as it makes it difficult to review the new changes. \ -Merge the base branch into your branch only when you really need to, i.e. because of conflicting changes in the mean time. \ -This reduces unnecessary CI runs. \ -Don't worry about merge commits messing up your commit history as every PR will be squash merged. \ -This means that all changes are joined into a single new commit whose message is as described below. +Code review starts when you open a non-draft PR or move a draft out of draft state. After that, do not rebase or squash your branch; it makes new changes harder to review. -### Getting PRs merged +Merge the base branch into yours only when you need to, for example because of conflicting changes elsewhere. That limits unnecessary CI runs. -Changes to Gitea must be reviewed before they are accepted — no matter who -makes the change, even if they are an owner or a maintainer. \ -The only exception are critical bugs that prevent Gitea from being compiled or started. \ -Specifically, we require two approvals from maintainers for every PR. \ -Once this criteria has been met, your PR receives the `lgtm/done` label. \ -From this point on, your only responsibility is to fix merge conflicts or respond to/implement requests by maintainers. \ -It is the responsibility of the maintainers from this point to get your PR merged. +Every PR is squash-merged, so merge commits on your branch do not matter for final history. The squash produces a single commit; mergers follow the [commit message format](docs/community-governance.md#commit-messages) in the governance guide. -If a PR has the `lgtm/done` label and there are no open discussions or merge conflicts anymore, any maintainer can add the `reviewed/wait-merge` label. \ -This label means that the PR is part of the merge queue and will be merged as soon as possible. \ -The merge queue will be cleared in the order of the list below: +### Reviewing PRs - +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. -Gitea uses it's own tool, the to automate parts of the review process. \ -This tool does the things listed below automatically: +#### For PR authors -- create a backport PR if needed once the initial PR was merged -- remove the PR from the merge queue after the PR merged -- keep the oldest branch in the merge queue up to date with merges +- **Response**: When answering reviewer questions, use real-world cases or examples and avoid speculation. +- **Discussion**: A discussion is always welcome and should be used to clarify the changes and the intent of the PR. +- **Help**: If you need help with the PR or comments are unclear, ask for clarification. -### Final call - -If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. - -After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. - -However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). - -### Commit messages - -Mergers are able and required to rewrite the PR title and summary (the first comment of a PR) so that it can produce an easily understandable commit message if necessary. \ -The final commit message should no longer contain any uncertainty such as `hopefully, won't happen anymore`. Replace uncertainty with certainty. - -#### PR Co-authors - -A person counts as a PR co-author the moment they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. \ -Mergers are required to remove such "false-positive" co-authors when writing the commit message. \ -The true co-authors must remain in the commit message. - -#### PRs targeting `main` - -The commit message of PRs targeting `main` is always - -```bash -$PR_TITLE ($PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` - -#### Backport PRs - -The commit message of backport PRs is always - -```bash -$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) - -$REWRITTEN_PR_SUMMARY -``` +Guidance for reviewers, the merge queue, and the squash commit message format is in [docs/community-governance.md](docs/community-governance.md). ## Documentation If you add a new feature or change an existing aspect of Gitea, the documentation for that feature must be created or updated in another PR at [https://gitea.com/gitea/docs](https://gitea.com/gitea/docs). **The docs directory on main repository will be removed at some time. We will have a yaml file to store configuration file's meta data. After that completed, configuration documentation should be in the main repository.** -## API v1 - -The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). - -### GitHub API compatibility - -Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ -If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ -If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ -Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ -The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). - -### Adding/Maintaining API routes - -All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ -All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ -and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ -They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ -All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ -and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ -They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). - -### When to use what HTTP method - -In general, HTTP methods are chosen as follows: - -- **GET** endpoints return the requested object(s) and status **OK (200)** -- **DELETE** endpoints return the status **No Content (204)** and no content either -- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object -- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either -- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** - -### Requirements for API routes - -All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). - -Endpoints returning lists must - -- support pagination (`page` & `limit` options in query) -- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) - -## Backports and Frontports - -### What is backported? - -We backport PRs given the following circumstances: - -1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. -2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. -3. We never backport new features. -4. We never backport breaking changes except when - 1. The breaking change has no effect on the vast majority of users - 2. The component triggering the breaking change is marked as experimental - -### How to backport? - -In the past, it was necessary to manually backport your PRs. \ -Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR - -- does not have the label `backport/manual` -- has the label `backport/` - -The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. - -### Format of backport PRs - -The title of backport PRs should be - -``` - (#) -``` - -The first two lines of the summary of the backporting PR should be - -``` -Backport # - -``` - -with the rest of the summary and labels matching the original PR. - -### Frontports - -Frontports behave exactly as described above for backports. - ## Developer Certificate of Origin (DCO) We consider the act of contributing to the code by submitting a Pull Request as the "Sign off" or agreement to the certifications and terms of the [DCO](DCO) and [MIT license](LICENSE). \ @@ -483,148 +275,3 @@ Signed-off-by: Joe Smith If you set the `user.name` and `user.email` Git config options, you can add the line to the end of your commits automatically with `git commit -s`. We assume in good faith that the information you provide is legally binding. - -## Release Cycle - -We adopted a release schedule to streamline the process of working on, finishing, and issuing releases. \ -The overall goal is to make a major release every three or four months, which breaks down into two or three months of general development followed by one month of testing and polishing known as the release freeze. \ -All the feature pull requests should be -merged before feature freeze. All feature pull requests haven't been merged before this feature freeze will be moved to next milestone, please notice our feature freeze announcement on discord. And, during the frozen period, a corresponding -release branch is open for fixes backported from main branch. Release candidates -are made during this period for user testing to -obtain a final version that is maintained in this branch. - -During a development cycle, we may also publish any necessary minor releases -for the previous version. For example, if the latest, published release is -v1.2, then minor changes for the previous release—e.g., v1.1.0 -> v1.1.1—are -still possible. - -## Maintainers - -To make sure every PR is checked, we have [maintainers](MAINTAINERS). \ -Every PR **must** be reviewed by at least two maintainers (or owners) before it can get merged. \ -For refactoring PRs after a week and documentation only PRs, the approval of only one maintainer is enough. \ -A maintainer should be a contributor of Gitea and contributed at least -4 accepted PRs. A contributor should apply as a maintainer in the -[Discord](https://discord.gg/Gitea) `#develop` channel. The team maintainers may invite the contributor. A maintainer -should spend some time on code reviews. If a maintainer has no -time to do that, they should apply to leave the maintainers team -and we will give them the honor of being a member of the [advisors -team](https://github.com/orgs/go-gitea/teams/advisors). Of course, if -an advisor has time to code review, we will gladly welcome them back -to the maintainers team. If a maintainer is inactive for more than 3 -months and forgets to leave the maintainers team, the owners may move -him or her from the maintainers team to the advisors team. -For security reasons, Maintainers should use 2FA for their accounts and -if possible provide GPG signed commits. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ -https://help.github.com/articles/signing-commits-with-gpg/ - -Furthermore, any account with write access (like bots and TOC members) **must** use 2FA. -https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/ - -## Technical Oversight Committee (TOC) - -At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. -https://blog.gitea.com/quarterly-23q1/ - -### TOC election process - -Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. -A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. -If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. - -The TOC is elected for one year, the TOC election happens yearly. -After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. -If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. -Refusals result in the person with the next highest vote getting the same choice. -As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. - -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. - -### Current TOC members - -- 2024-01-01 ~ 2024-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> - - [delvh](https://gitea.com/delvh) - - [John Olheiser](https://gitea.com/jolheiser) - -### Previous TOC/owners members - -Here's the history of the owners and the time they served: - -- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [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), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 -- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 -- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 -- [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 -- [John Olheiser](https://gitea.com/jolheiser) - 2023 -- [Jason Song](https://gitea.com/wolfogre) - 2023 - -## Governance Compensation - -Each member of the community elected TOC will be granted $500 each month as compensation for their work. - -Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. - -These funds will come from community sources like the OpenCollective rather than directly from the company. -Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. -Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. - -## TOC & Working groups - -With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. - -The currently proposed groups are: - -- **Core Group**: maintain the primary Gitea repository -- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. -- **Documentation Group**: maintain related documents and repositories -- **Translation Group**: coordinate with translators and maintain translations -- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items - -## Roadmap - -Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. -TOC members need to review the roadmap every year and work together on the direction of the project. - -When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. - -You can visit our roadmap on the wiki. - -## Versions - -Gitea has the `main` branch as a tip branch and has version branches -such as `release/v1.19`. `release/v1.19` is a release branch and we will -tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept -pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, -after bringing the bug fix also to the main branch. - -Since the `main` branch is a tip version, if you wish to use Gitea -in production, please download the latest release tag version. All the -branches will be protected via GitHub, all the PRs to every branch must -be reviewed by two maintainers and must pass the automatic tests. - -## Releasing Gitea - -- Let $vmaj, $vmin and $vpat be Major, Minor and Patch version numbers, $vpat should be rc1, rc2, 0, 1, ...... $vmaj.$vmin will be kept the same as milestones on github or gitea in future. -- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. -- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: - - Create `-dev` tag as `git tag -s -F release.notes v$vmaj.$vmin.0-dev` and push the tag as `git push origin v$vmaj.$vmin.0-dev`. - - When CI has finished building tag then you have to create a new branch named `release/v$vmaj.$vmin` -- If it is bugfix version create PR for changelog on branch `release/v$vmaj.$vmin` and wait till it is reviewed and merged. -- Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. -- And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) -- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. -- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. -- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: - - bump the version of https://dl.gitea.com/gitea/version.json - - merge the blog post PR - - announce the release in discord `#announcements` diff --git a/docs/community-governance.md b/docs/community-governance.md new file mode 100644 index 00000000000..dbf24813295 --- /dev/null +++ b/docs/community-governance.md @@ -0,0 +1,198 @@ +# Community governance and review process + +This document describes maintainer expectations, project governance, and the detailed pull request review workflow (labels, merge queue, commit message format for mergers). For what contributors should do when opening and updating a PR, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Code review + +### Milestone + +A PR should only be assigned to a milestone if it will likely be merged into the given version. \ +PRs without a milestone may not be merged. + +### Labels + +Almost all labels used inside Gitea can be classified as one of the following: + +- `modifies/…`: Determines which parts of the codebase are affected. These labels will be set through the CI. +- `topic/…`: Determines the conceptual component of Gitea that is affected, i.e. issues, projects, or authentication. At best, PRs should only target one component but there might be overlap. Must be set manually. +- `type/…`: Determines the type of an issue or PR (feature, refactoring, docs, bug, …). If GitHub supported scoped labels, these labels would be exclusive, so you should set **exactly** one, not more or less (every PR should fall into one of the provided categories, and only one). +- `issue/…` / `lgtm/…`: Labels that are specific to issues or PRs respectively and that are only necessary in a given context, i.e. `issue/not-a-bug` or `lgtm/need 2` + +Every PR should be labeled correctly with every label that applies. + +There are also some labels that will be managed automatically.\ +In particular, these are + +- the amount of pending required approvals +- has all `backport`s or needs a manual backport + +### Reviewing PRs + +Maintainers are encouraged to review pull requests in areas where they have expertise or particular interest. + +#### For reviewers + +- **Verification**: Verify that the PR accurately reflects the changes, and verify that the tests and documentation are complete and aligned with the implementation. +- **Actionable feedback**: Say what should change and why, and distinguish required changes from optional suggestions. +- **Feedback**: Focus feedback on the issue itself and avoid comments about the contributor's abilities. +- **Request changes**: If you request changes (i.e., block a PR), give a clear rationale and, whenever possible, a concrete path to resolution. +- **Approval**: Only approve a PR when you are fully satisfied with its current state - "rubber-stamp" approvals need to be highlighted as such. + +### Getting PRs merged + +Changes to Gitea must be reviewed before they are accepted, including changes from owners and maintainers. The exception is critical bugs that prevent Gitea from compiling or starting. + +We require two maintainer approvals for every PR. When that is satisfied, your PR gets the `lgtm/done` label. After that, you mainly fix merge conflicts and respond to or implement maintainer requests; maintainers drive getting the PR merged. + +If a PR has `lgtm/done`, no open discussions, and no merge conflicts, any maintainer may add `reviewed/wait-merge`. That puts the PR in the merge queue. PRs are merged from the queue in the order of this list: + + + +Gitea uses its own tool, , to automate parts of the review process. The backporter: + +- Creates a backport PR when needed after the initial PR merges. +- Removes the PR from the merge queue after it merges. +- Keeps the oldest branch in the merge queue up to date with merges. + +### Final call + +If a PR has been ignored for more than 7 days with no comments or reviews, and the author or any maintainer believes it will not survive a long wait (such as a refactoring PR), they can send "final call" to the TOC by mentioning them in a comment. + +After another 7 days, if there is still zero approval, this is considered a polite refusal, and the PR will be closed to avoid wasting further time. Therefore, the "final call" has a cost, and should be used cautiously. + +However, if there are no objections from maintainers, the PR can be merged with only one approval from the TOC (not the author). + +### Commit messages + +Mergers are required to rewrite the PR title and the first comment (the summary) when necessary so the squash commit message is clear. + +The final commit message should not hedge: replace phrases like `hopefully, won't happen anymore` with definite wording. + +#### PR Co-authors + +A person counts as a PR co-author once they (co-)authored a commit that is not simply a `Merge base branch into branch` commit. Mergers must remove such false-positive co-authors when writing the squash message. Every true co-author must remain in the commit message. + +#### PRs targeting `main` + +The commit message of PRs targeting `main` is always + +```bash +$PR_TITLE ($PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +#### Backport PRs + +The commit message of backport PRs is always + +```bash +$PR_TITLE ($INITIAL_PR_INDEX) ($BACKPORT_PR_INDEX) + +$REWRITTEN_PR_SUMMARY +``` + +## Maintainers + +We list [maintainers](../MAINTAINERS) so every PR gets proper review. + +#### Review expectations + +Every PR **must** be reviewed by at least two maintainers (or owners) before merge. **Exception:** after one week, refactoring PRs and documentation-only PRs need only one maintainer approval. + +Maintainers are expected to spend time on code reviews. + +#### Becoming a maintainer + +A maintainer should already be a Gitea contributor with at least four merged PRs. To apply, use the [Discord](https://discord.gg/Gitea) `#develop` channel. Maintainer teams may also invite contributors. + +#### Stepping down, advisors, and inactivity + +If you cannot keep reviewing, apply to leave the maintainers team. You can join the [advisors team](https://github.com/orgs/go-gitea/teams/advisors); advisors who want to review again are welcome back as maintainers. + +If a maintainer is inactive for more than three months and has not left the team, owners may move them to the advisors team. + +#### Account security + +For security, maintainers should enable 2FA and sign commits with GPG when possible: + +- [Two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication) +- [Signing commits with GPG](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) + +Any account with write access (including bots and TOC members) **must** use [2FA](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication). + +## Technical Oversight Committee (TOC) + +At the start of 2023, the `Owners` team was dissolved. Instead, the governance charter proposed a technical oversight committee (TOC) which expands the ownership team of the Gitea project from three elected positions to six positions. Three positions are elected as it has been over the past years, and the other three consist of appointed members from the Gitea company. +https://blog.gitea.com/quarterly-23q1/ + +### TOC election process + +Any maintainer is eligible to be part of the community TOC if they are not associated with the Gitea company. +A maintainer can either nominate themselves, or can be nominated by other maintainers to be a candidate for the TOC election. +If you are nominated by someone else, you must first accept your nomination before the vote starts to be a candidate. + +The TOC is elected for one year, the TOC election happens yearly. +After the announcement of the results of the TOC election, elected members have two weeks time to confirm or refuse the seat. +If an elected member does not answer within this timeframe, they are automatically assumed to refuse the seat. +Refusals result in the person with the next highest vote getting the same choice. +As long as seats are empty in the TOC, members of the previous TOC can fill them until an elected member accepts the seat. + +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. + +### Current TOC members + +- 2024-01-01 ~ 2024-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> + - [delvh](https://gitea.com/delvh) + - [John Olheiser](https://gitea.com/jolheiser) + +### Previous TOC/owners members + +Here's the history of the owners and the time they served: + +- [Lunny Xiao](https://gitea.com/lunny) - 2016, 2017, [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), [2022](https://github.com/go-gitea/gitea/issues/17872), 2023 +- [Kim Carlbäcker](https://github.com/bkcsoft) - 2016, 2017 +- [Thomas Boerger](https://gitea.com/tboerger) - 2016, 2017 +- [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 +- [John Olheiser](https://gitea.com/jolheiser) - 2023 +- [Jason Song](https://gitea.com/wolfogre) - 2023 + +## Governance Compensation + +Each member of the community elected TOC will be granted $500 each month as compensation for their work. + +Furthermore, any community release manager for a specific release or LTS will be compensated $500 for the delivery of said release. + +These funds will come from community sources like the OpenCollective rather than directly from the company. +Only non-company members are eligible for this compensation, and if a member of the community TOC takes the responsibility of release manager, they would only be compensated for their TOC duties. +Gitea Ltd employees are not eligible to receive any funds from the OpenCollective unless it is reimbursement for a purchase made for the Gitea project itself. + +## TOC & Working groups + +With Gitea covering many projects outside of the main repository, several groups will be created to help focus on specific areas instead of requiring maintainers to be a jack-of-all-trades. Maintainers are of course more than welcome to be part of multiple groups should they wish to contribute in multiple places. + +The currently proposed groups are: + +- **Core Group**: maintain the primary Gitea repository +- **Integration Group**: maintain the Gitea ecosystem's related tools, including go-sdk/tea/changelog/bots etc. +- **Documentation Group**: maintain related documents and repositories +- **Translation Group**: coordinate with translators and maintain translations +- **Security Group**: managed by TOC directly, members are decided by TOC, maintains security patches/responsible for security items + +## Roadmap + +Each year a roadmap will be discussed with the entire Gitea maintainers team, and feedback will be solicited from various stakeholders. +TOC members need to review the roadmap every year and work together on the direction of the project. + +When a vote is required for a proposal or other change, the vote of community elected TOC members count slightly more than the vote of company elected TOC members. With this approach, we both avoid ties and ensure that changes align with the mission statement and community opinion. + +You can visit our roadmap on the wiki. diff --git a/docs/guideline-backend.md b/docs/guideline-backend.md new file mode 100644 index 00000000000..bc3e71113f2 --- /dev/null +++ b/docs/guideline-backend.md @@ -0,0 +1,58 @@ +# Backend development + +This document covers backend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +For coding style and architecture, see also the [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) on the documentation site. + +## Dependencies + +Go dependencies are managed using [Go Modules](https://go.dev/cmd/go/#hdr-Module_maintenance). \ +You can find more details in the [go mod documentation](https://go.dev/ref/mod) and the [Go Modules Wiki](https://github.com/golang/go/wiki/Modules). + +Pull requests should only modify `go.mod` and `go.sum` where it is related to your change, be it a bugfix or a new feature. \ +Apart from that, these files should only be modified by Pull Requests whose only purpose is to update dependencies. + +The `go.mod`, `go.sum` update needs to be justified as part of the PR description, +and must be verified by the reviewers and/or merger to always reference +an existing upstream commit. + +## API v1 + +The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest). + +### GitHub API compatibility + +Gitea's API should use the same endpoints and fields as the GitHub API as far as possible, unless there are good reasons to deviate. \ +If Gitea provides functionality that GitHub does not, a new endpoint can be created. \ +If information is provided by Gitea that is not provided by the GitHub API, a new field can be used that doesn't collide with any GitHub fields. \ +Updating an existing API should not remove existing fields unless there is a really good reason to do so. \ +The same applies to status responses. If you notice a problem, feel free to leave a comment in the code for future refactoring to API v2 (which is currently not planned). + +### Adding/Maintaining API routes + +All expected results (errors, success, fail messages) must be documented ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L319-L327)). \ +All JSON input types must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L76-L91)) \ +and referenced in [routers/api/v1/swagger/options.go](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/options.go). \ +They can then be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L318). \ +All JSON responses must be defined as a struct in [modules/structs/](modules/structs/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/modules/structs/issue.go#L36-L68)) \ +and referenced in its category in [routers/api/v1/swagger/](routers/api/v1/swagger/) ([example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/swagger/issue.go#L11-L16)) \ +They can be used like [this example](https://github.com/go-gitea/gitea/blob/c620eb5b2d0d874da68ebd734d3864c5224f71f7/routers/api/v1/repo/issue.go#L277-L279). + +### When to use what HTTP method + +In general, HTTP methods are chosen as follows: + +- **GET** endpoints return the requested object(s) and status **OK (200)** +- **DELETE** endpoints return the status **No Content (204)** and no content either +- **POST** endpoints are used to **create** new objects (e.g. a User) and return the status **Created (201)** and the created object +- **PUT** endpoints are used to **add/assign** existing Objects (e.g. a user to a team) and return the status **No Content (204)** and no content either +- **PATCH** endpoints are used to **edit/change** an existing object and return the changed object and the status **OK (200)** + +### Requirements for API routes + +All parameters of endpoints changing/editing an object must be optional (except the ones to identify the object, which are required). + +Endpoints returning lists must + +- support pagination (`page` & `limit` options in query) +- set `X-Total-Count` header via **SetTotalCountHeader** ([example](https://github.com/go-gitea/gitea/blob/7aae98cc5d4113f1e9918b7ee7dd09f67c189e3e/routers/api/v1/repo/issue.go#L444)) diff --git a/docs/guideline-frontend.md b/docs/guideline-frontend.md new file mode 100644 index 00000000000..80ebe821777 --- /dev/null +++ b/docs/guideline-frontend.md @@ -0,0 +1,17 @@ +# Frontend development + +This document covers frontend-specific contribution expectations. For general contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Dependencies + +For the frontend, we use [npm](https://www.npmjs.com/). + +The same restrictions apply for frontend dependencies as for [backend dependencies](guideline-backend.md#dependencies), with the exceptions that the files for it are `package.json` and `package-lock.json`, and that new versions must always reference an existing version. + +## Design guideline + +Depending on your change, please read the + +- [backend development guideline](https://docs.gitea.com/contributing/guidelines-backend) +- [frontend development guideline](https://docs.gitea.com/contributing/guidelines-frontend) +- [refactoring guideline](https://docs.gitea.com/contributing/guidelines-refactoring) diff --git a/docs/release-management.md b/docs/release-management.md new file mode 100644 index 00000000000..be8d9e1abf2 --- /dev/null +++ b/docs/release-management.md @@ -0,0 +1,115 @@ +# Release management + +This document describes the release cycle, backports, versioning, and the release manager checklist. For everyday contribution workflow, see [CONTRIBUTING.md](../CONTRIBUTING.md). + +## Backports and Frontports + +### What is backported? + +We backport PRs given the following circumstances: + +1. Feature freeze is active, but `-rc0` has not been released yet. Here, we backport as much as possible. +2. `rc0` has been released. Here, we only backport bug- and security-fixes, and small enhancements. Large PRs such as refactors are not backported anymore. +3. We never backport new features. +4. We never backport breaking changes except when + 1. The breaking change has no effect on the vast majority of users + 2. The component triggering the breaking change is marked as experimental + +### How to backport? + +In the past, it was necessary to manually backport your PRs. \ +Now, that's not a requirement anymore as our [backport bot](https://github.com/GiteaBot) tries to create backports automatically once the PR is merged when the PR + +- does not have the label `backport/manual` +- has the label `backport/` + +The `backport/manual` label signifies either that you want to backport the change yourself, or that there were conflicts when backporting, thus you **must** do it yourself. + +### Format of backport PRs + +The title of backport PRs should be + +``` + (#) +``` + +The first two lines of the summary of the backporting PR should be + +``` +Backport # + +``` + +with the rest of the summary and labels matching the original PR. + +### Frontports + +Frontports behave exactly as described above for backports. + +## Release Cycle + +We use a release schedule so work, stabilization, and releases stay predictable. + +### Cadence + +- Aim for a major release about every three or four months. +- Roughly two or three months of general development, then about one month of testing and polish called the **release freeze**. +- *Starting with v1.26 the release cycle will be more predictable and follow a more regular schedule.* + +### Release schedule + +We will try to publish a new major version every three months: + +- v1.26.0 in April 2026 +- v1.27.0 in June 2026 +- v1.28.0 in September 2026 +- v1.29.0 in December 2026 + +#### How is the release handled? +- The release manager will tag the release candidate (e.g. `v1.26.0-rc0`) and publish it for testing in the **first week of the release month**. +- If there are no major issues, the release manager will check with the other maintainers and then tag the final release (e.g. `v1.26.0`) in the **one or two weeks following the release candidate**. + +### Feature freeze + +- Merge feature PRs before the freeze when you can. +- Feature PRs still open at the freeze move to the next milestone. Watch Discord for the freeze announcement. +- During the freeze, a **release branch** takes fixes backported from `main`. Release candidates ship for testing; the final release for that line is maintained from that branch. + +### Patch releases + +During a cycle we may ship patch releases for an older line. For example, if the latest release is v1.2, we can still publish v1.1.1 after v1.1.0. + +### End of life (EOL) + +We support per standard the last major release. For example, if the latest release is v1.26, we support v1.26 and v1.25, but not v1.24 anymore. We will only publish security fixes for the last major release, so if you are using an older release, please upgrade to a supported release as soon as possible. +Also we always try to support the latest on main branch, so if you are using the latest on main, you should be fine. + +## Versions + +Gitea has the `main` branch as a tip branch and has version branches +such as `release/v1.19`. `release/v1.19` is a release branch and we will +tag `v1.19.0` for binary download. If `v1.19.0` has bugs, we will accept +pull requests on the `release/v1.19` branch and publish a `v1.19.1` tag, +after bringing the bug fix also to the main branch. + +Since the `main` branch is a tip version, if you wish to use Gitea +in production, please download the latest release tag version. All the +branches will be protected via GitHub, all the PRs to every branch must +be reviewed by two maintainers and must pass the automatic tests. + +## Releasing Gitea + +- Let MAJOR, MINOR and PATCH be Major, Minor and Patch version numbers, PATCH should be rc1, rc2, 0, 1, ...... MAJOR.MINOR will be kept the same as milestones on github or gitea in future. +- Before releasing, confirm all the version's milestone issues or PRs has been resolved. Then discuss the release on Discord channel #maintainers and get agreed with almost all the owners and mergers. Or you can declare the version and if nobody is against it in about several hours. +- If this is a big version first you have to create PR for changelog on branch `main` with PRs with label `changelog` and after it has been merged do following steps: + - Create `-dev` tag as `git tag -s -F release.notes vMAJOR.MINOR.0-dev` and push the tag as `git push origin vMAJOR.MINOR.0-dev`. + - When CI has finished building tag then you have to create a new branch named `release/vMAJOR.MINOR` +- If it is bugfix version create PR for changelog on branch `release/vMAJOR.MINOR` and wait till it is reviewed and merged. +- Add a tag as `git tag -s -F release.notes vMAJOR.MINOR.PATCH`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. +- And then push the tag as `git push origin vMAJOR.MINOR.$`. CI will automatically create a release and upload all the compiled binary. (But currently it doesn't add the release notes automatically. Maybe we should fix that.) +- If needed send a frontport PR for the changelog to branch `main` and update the version in `docs/config.yaml` to refer to the new version. +- Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. +- Verify all release assets were correctly published through CI on dl.gitea.com and GitHub releases. Once ACKed: + - bump the version of https://dl.gitea.com/gitea/version.json + - merge the blog post PR + - announce the release in discord `#announcements` From 80585adab4616a96eb414b161f7f74b305118bce Mon Sep 17 00:00:00 2001 From: Mohit Swarnkar Date: Mon, 13 Apr 2026 07:22:46 +0530 Subject: [PATCH 017/126] fix(api): handle fork-only commits in compare API (#37185) Fix 500 error when comparing branches across fork repositories ## Problem The compare API returns a 500 Internal Server Error when comparing branches where the head commit exists only in the fork repository. ## Cause The API was using the base repository's GitRepo and repository context when converting commits. This fails when the commit does not exist in the base repository, resulting in a "fatal: bad object" error. ## Solution Use the head repository and HeadGitRepo when available to ensure commits are resolved in the correct repository context. ## Result * Fixes "fatal: bad object" error * Enables proper comparison between base and fork repositories * Prevents 500 Internal Server Error Fixes #37168 --------- Co-authored-by: wxiaoguang --- routers/api/v1/repo/compare.go | 11 +++- tests/integration/api_repo_compare_test.go | 58 ++++++++++++++-------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 6285138c27d..69dceb6db9d 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) { apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits)) userCache := make(map[string]*user_model.User) + for i := 0; i < len(compareInfo.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache, + apiCommit, err := convert.ToCommit( + ctx, + compareInfo.HeadRepo, + compareInfo.HeadGitRepo, + compareInfo.Commits[i], + userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, Files: files, - }) + }, + ) if err != nil { ctx.APIErrorInternal(err) return diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 9565e4d2090..8aa0035b0a9 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -5,46 +5,60 @@ package integration import ( "net/http" + "net/url" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPICompareBranches(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session2 := loginUser(t, "user2") + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - // Login as User2. - session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("CompareBranches", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("CompareBranches", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) + }) - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + t.Run("CompareCommits", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - assert.Equal(t, 2, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 2) - }) + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) - t.Run("CompareCommits", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("CompareForkOnlyCommit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) + repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + user13Sess := loginUser(t, "user13") + user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository) - assert.Equal(t, 1, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 1) + _, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"}) + require.NoError(t, err) + req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) }) } From fa8f7f15ef78207d6b53c3cff26a5f22dc155245 Mon Sep 17 00:00:00 2001 From: Xing Hong <39619359+xingxing21@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:25:58 +0900 Subject: [PATCH 018/126] Always show owner/repo name in compare page dropdowns (#37172) Fixes: https://github.com/go-gitea/gitea/issues/36677 --------- Co-authored-by: wxiaoguang --- templates/repo/diff/compare.tmpl | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl index 87c783f4460..afd44f26a47 100644 --- a/templates/repo/diff/compare.tmpl +++ b/templates/repo/diff/compare.tmpl @@ -13,22 +13,16 @@ {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} - {{$BaseCompareName := $.BaseName -}} - {{- $HeadCompareName := $.HeadRepo.OwnerName -}} - {{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}} - {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} - {{- end -}} - {{- $OwnForkCompareName := "" -}} - {{- if .OwnForkRepo -}} - {{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}} - {{- end -}} - {{- $RootRepoCompareName := "" -}} - {{- if .RootRepo -}} - {{- $RootRepoCompareName = .RootRepo.OwnerName -}} - {{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}} - {{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}} - {{- end -}} - {{- end -}} + {{$BaseCompareName := $.Repository.FullName -}} + {{$HeadCompareName := $.HeadRepo.FullName -}} + {{$OwnForkCompareName := "" -}} + {{if $.OwnForkRepo -}} + {{$OwnForkCompareName = $.OwnForkRepo.FullName -}} + {{end -}} + {{$RootRepoCompareName := "" -}} + {{if $.RootRepo -}} + {{$RootRepoCompareName = $.RootRepo.FullName -}} + {{end -}}
{{svg "octicon-git-compare"}} From 6eae04241d010b1490e4e80a1ff5680b2a9ab4d7 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 13 Apr 2026 20:10:43 +0200 Subject: [PATCH 019/126] Fix encoding for Matrix Webhooks (#37190) `url.PathEscape` unnecessarily encodes ! to %21, causing Matrix homeservers to reject the request with 401. Replace %21 back to ! after escaping. Fixes #36012 --------- Signed-off-by: wxiaoguang Co-authored-by: Claude Sonnet 4.6 Co-authored-by: wxiaoguang --- routers/web/repo/setting/webhook.go | 11 ++++++++++- routers/web/repo/setting/webhook_test.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 routers/web/repo/setting/webhook_test.go diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index b0f3a5cfee8..8c57a68b250 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -450,12 +450,21 @@ func MatrixHooksEditPost(ctx *context.Context) { editWebhook(ctx, matrixHookParams(ctx)) } +func matrixRoomIDEncode(roomID string) string { + // See https://spec.matrix.org/latest/appendices/#room-ids + // Some (unrelated) demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation + // API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room + // Some of their examples show links like: "PUT /rooms/!roomid:domain/state/m.example.event" + return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID)) +} + func matrixHookParams(ctx *context.Context) webhookParams { form := web.GetForm(ctx).(*forms.NewMatrixHookForm) + // TODO: need to migrate to the latest (v3) API: https://spec.matrix.org/v1.18/client-server-api/ return webhookParams{ Type: webhook_module.MATRIX, - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)), ContentType: webhook.ContentTypeJSON, HTTPMethod: http.MethodPut, WebhookForm: form.WebhookForm, diff --git a/routers/web/repo/setting/webhook_test.go b/routers/web/repo/setting/webhook_test.go new file mode 100644 index 00000000000..ca4a21e0755 --- /dev/null +++ b/routers/web/repo/setting/webhook_test.go @@ -0,0 +1,15 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWebhookMatrix(t *testing.T) { + assert.Equal(t, "!roomid:domain", matrixRoomIDEncode("!roomid:domain")) + assert.Equal(t, "!room%23id:domain", matrixRoomIDEncode("!room#id:domain")) // maybe it should never really happen in real world +} From 6bcb666a9d7ed14f3a77d3bea91e4c63f52e9cdb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 14 Apr 2026 02:53:55 +0800 Subject: [PATCH 020/126] Refactor htmx and fetch-action related code (#37186) This is the first step (the hardest part): * repo file list last commit message lazy load * admin server status monitor * watch/unwatch (normal page, watchers page) * star/unstar (normal page, watchers page) * project view, delete column * workflow dispatch, switch the branch * commit page: load branches and tags referencing this commit The legacy "data-redirect" attribute is removed, it only makes the page reload (sometimes using an incorrect link). Also did cleanup for some devtest pages. --- eslint.config.ts | 1 - modules/web/middleware/cookie.go | 5 + routers/common/redirect.go | 4 +- routers/web/devtest/devtest.go | 7 +- routers/web/repo/star.go | 1 - routers/web/repo/view.go | 6 +- routers/web/repo/watch.go | 1 - routers/web/web.go | 2 +- services/context/base.go | 10 +- services/context/base_test.go | 7 +- templates/admin/dashboard.tmpl | 5 +- templates/admin/notice.tmpl | 2 +- templates/admin/system_status.tmpl | 2 +- templates/base/head.tmpl | 2 +- templates/devtest/fetch-action.tmpl | 5 +- templates/devtest/fomantic-modal.tmpl | 22 +- templates/devtest/gitea-ui.tmpl | 11 - templates/projects/view.tmpl | 2 +- templates/repo/actions/workflow_dispatch.tmpl | 13 +- .../actions/workflow_dispatch_inputs.tmpl | 2 + .../repo/commit_load_branches_and_tags.tmpl | 2 +- .../view_content/update_branch_by_merge.tmpl | 2 +- templates/repo/settings/webhook/history.tmpl | 2 +- templates/repo/star_unstar.tmpl | 32 +- templates/repo/user_cards.tmpl | 13 +- templates/repo/view_list.tmpl | 12 +- templates/repo/watch_unwatch.tmpl | 32 +- templates/status/500.tmpl | 1 - types.d.ts | 7 + web_src/css/repo/home-file-list.css | 5 + web_src/js/features/admin/common.ts | 2 +- web_src/js/features/common-fetch-action.ts | 382 ++++++++++++++---- web_src/js/features/comp/WebHookEditor.ts | 7 +- web_src/js/features/repo-diff-commit.ts | 2 +- web_src/js/features/repo-issue-pull.ts | 3 - web_src/js/globals.ts | 1 - web_src/js/index.ts | 2 - web_src/js/modules/devtest.ts | 49 ++- web_src/js/modules/fetch.ts | 14 +- web_src/js/utils.test.ts | 14 +- web_src/js/utils.ts | 5 - 41 files changed, 457 insertions(+), 242 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 5b7884bdce3..36c3b8d1e5b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -575,7 +575,6 @@ export default defineConfig([ 'no-restricted-imports': [2, {paths: [ {name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}, {name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true}, - {name: 'idiomorph/htmx', message: 'Loaded in globals.ts', allowTypeImports: true}, ]}], 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go index 336c276fe8f..5456e3c6b70 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -35,6 +35,11 @@ func DeleteRedirectToCookie(resp http.ResponseWriter) { } func RedirectLinkUserLogin(req *http.Request) string { + if req.Header.Get("X-Gitea-Fetch-Action") != "" { + // when building the redirect link for a fetch request, the current link might be a partial page, + // so we only redirect to the login page without redirect_to parameter + return setting.AppSubURL + "/user/login" + } return setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(setting.AppSubURL+req.URL.RequestURI()) } diff --git a/routers/common/redirect.go b/routers/common/redirect.go index d64f74ec82a..2e1f315f2d8 100644 --- a/routers/common/redirect.go +++ b/routers/common/redirect.go @@ -16,8 +16,8 @@ func FetchRedirectDelegate(resp http.ResponseWriter, req *http.Request) { // 2. when use "window.reload()", the hash is not respected, the newly loaded page won't scroll to the hash target. // The typical page is "issue comment" page. The backend responds "/owner/repo/issues/1#comment-2", // then frontend needs this delegate to redirect to the new location with hash correctly. - redirect := req.PostFormValue("redirect") - if !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { + redirect := req.FormValue("redirect") + if req.Method != http.MethodPost || !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) { resp.WriteHeader(http.StatusBadRequest) return } diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 8bc5947df8f..af548430169 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -45,7 +45,7 @@ func List(ctx *context.Context) { func FetchActionTest(ctx *context.Context) { _ = ctx.Req.ParseForm() - ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + + ctx.Flash.Info("fetch action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" + "Form: " + ctx.Req.Form.Encode() + "\n" + "PostForm: " + ctx.Req.PostForm.Encode(), ) @@ -241,9 +241,8 @@ func prepareMockDataUnicodeEscape(ctx *context.Context) { func TmplCommon(ctx *context.Context) { prepareMockData(ctx) - if ctx.Req.Method == http.MethodPost { - _ = ctx.Req.ParseForm() - ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ + if ctx.Req.Method == http.MethodPost && ctx.FormBool("mock_response_delay") { + ctx.Flash.Info("form submit: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+ "Form: "+ctx.Req.Form.Encode()+"\n"+ "PostForm: "+ctx.Req.PostForm.Encode(), true, diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go index 00c06b7d02d..8cfbfefdf14 100644 --- a/routers/web/repo/star.go +++ b/routers/web/repo/star.go @@ -26,6 +26,5 @@ func ActionStar(ctx *context.Context) { ctx.ServerError("GetRepositoryByName", err) return } - ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.HTML(http.StatusOK, tplStarUnstar) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 46661f0df0f..7f59e6b3893 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -310,13 +310,15 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return nil } - { + { // this block is for testing purpose only if timeout != 0 && !setting.IsProd && !setting.IsInTesting { log.Debug("first call to get directory file commit info") clearFilesCommitInfo := func() { log.Warn("clear directory file commit info to force async loading on frontend") for i := range files { - files[i].Commit = nil + if i%2 == 0 { // for testing purpose, only clear half of the files' commit info + files[i].Commit = nil + } } } _ = clearFilesCommitInfo diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go index 70c548b8cea..a7fbfc168be 100644 --- a/routers/web/repo/watch.go +++ b/routers/web/repo/watch.go @@ -26,6 +26,5 @@ func ActionWatch(ctx *context.Context) { ctx.ServerError("GetRepositoryByName", err) return } - ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl ctx.HTML(http.StatusOK, tplWatchUnwatch) } diff --git a/routers/web/web.go b/routers/web/web.go index 61d1fdc1421..879a6f3afd6 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1710,7 +1710,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/forks", repo.Forks) m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff) - m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) + m.Get("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit) }, optSignIn, context.RepoAssignment, reqUnitCodeReader) // end "/{username}/{reponame}": repo code diff --git a/services/context/base.go b/services/context/base.go index 8d44de5bc72..c5ec4b419a2 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -159,12 +159,10 @@ func (b *Base) Redirect(location string, status ...int) { // So in this case, we should remove the session cookie from the response header removeSessionCookieHeader(b.Resp) } - // in case the request is made by htmx, have it redirect the browser instead of trying to follow the redirect inside htmx - if b.Req.Header.Get("HX-Request") == "true" { - b.Resp.Header().Set("HX-Redirect", location) - // we have to return a non-redirect status code so XMLHTTPRequest will not immediately follow the redirect - // so as to give htmx redirect logic a chance to run - b.Status(http.StatusNoContent) + // In case the request is made by "fetch-action" module, make JS redirect to the new location + // Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected. + if b.Req.Header.Get("X-Gitea-Fetch-Action") != "" { + b.JSON(http.StatusOK, map[string]any{"redirect": location}) return } http.Redirect(b.Resp, b.Req, location, code) diff --git a/services/context/base_test.go b/services/context/base_test.go index 2a4f86dddf8..f9bbe717290 100644 --- a/services/context/base_test.go +++ b/services/context/base_test.go @@ -38,9 +38,10 @@ func TestRedirect(t *testing.T) { req, _ = http.NewRequest(http.MethodGet, "/", nil) resp := httptest.NewRecorder() - req.Header.Add("HX-Request", "true") + req.Header.Add("X-Gitea-Fetch-Action", "1") b := NewBaseContextForTest(resp, req) b.Redirect("/other") - assert.Equal(t, "/other", resp.Header().Get("HX-Redirect")) - assert.Equal(t, http.StatusNoContent, resp.Code) + assert.Contains(t, resp.Header().Get("Content-Type"), "application/json") + assert.JSONEq(t, `{"redirect":"/other"}`, resp.Body.String()) + assert.Equal(t, http.StatusOK, resp.Code) } diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 834c56672f0..f44dc243622 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -76,10 +76,7 @@ {{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
-
-
- {{template "admin/system_status" .}} -
+ {{template "admin/system_status" .}}
{{template "admin/layout_footer" .}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 7e08946f23b..55ee9da8520 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -50,7 +50,7 @@
- diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl index 7b5c9be6ccc..8134db9eb26 100644 --- a/templates/admin/system_status.tmpl +++ b/templates/admin/system_status.tmpl @@ -1,4 +1,4 @@ -
+
{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}
{{.SysStatus.StartTime}}
{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}
diff --git a/templates/base/head.tmpl b/templates/base/head.tmpl index 381e559b9db..90f6b7546e5 100644 --- a/templates/base/head.tmpl +++ b/templates/base/head.tmpl @@ -23,7 +23,7 @@ {{template "base/head_script" .}} {{template "custom/header" .}} - + {{template "custom/body_outer_pre" .}}
diff --git a/templates/devtest/fetch-action.tmpl b/templates/devtest/fetch-action.tmpl index cd4da52aac3..e8fddf17b09 100644 --- a/templates/devtest/fetch-action.tmpl +++ b/templates/devtest/fetch-action.tmpl @@ -3,7 +3,7 @@

link-action

- Use "window.fetch" to send a request to backend, the request is defined in an "A" or "BUTTON" element. + The request is defined in an "A" or "BUTTON" element. It might be renamed to "link-fetch-action" to match the "form-fetch-action".
@@ -15,7 +15,6 @@

form-fetch-action

-
Use "window.fetch" to send a form request to backend
@@ -25,7 +24,7 @@
-
+
bad action url
diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl index 8e769790b25..98c3f332ae7 100644 --- a/templates/devtest/fomantic-modal.tmpl +++ b/templates/devtest/fomantic-modal.tmpl @@ -1,21 +1,9 @@ {{template "devtest/devtest-header"}}
- {{template "base/alert" .}} - - diff --git a/templates/repo/actions/workflow_dispatch_inputs.tmpl b/templates/repo/actions/workflow_dispatch_inputs.tmpl index 085fd553de5..10badeb6177 100644 --- a/templates/repo/actions/workflow_dispatch_inputs.tmpl +++ b/templates/repo/actions/workflow_dispatch_inputs.tmpl @@ -1,3 +1,4 @@ +
{{if not .WorkflowDispatchConfig}}
{{/* using "ui message" in "ui form" needs to force to display */}} {{if not .CurWorkflowExists}} @@ -44,3 +45,4 @@
{{end}} {{end}} +
diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl index ecb210c575c..162d805a29e 100644 --- a/templates/repo/commit_load_branches_and_tags.tmpl +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -8,7 +8,7 @@
{{end}}
diff --git a/templates/repo/issue/view_content/update_branch_by_merge.tmpl b/templates/repo/issue/view_content/update_branch_by_merge.tmpl index 5b306e3cbea..f6fa66eba76 100644 --- a/templates/repo/issue/view_content/update_branch_by_merge.tmpl +++ b/templates/repo/issue/view_content/update_branch_by_merge.tmpl @@ -9,7 +9,7 @@ {{if and $.UpdateAllowed $.UpdateByRebaseAllowed}}
- diff --git a/templates/repo/star_unstar.tmpl b/templates/repo/star_unstar.tmpl index dea965ab307..7e4c61aa286 100644 --- a/templates/repo/star_unstar.tmpl +++ b/templates/repo/star_unstar.tmpl @@ -1,13 +1,19 @@ -
- -
+
+ {{$buttonText := ctx.Locale.Tr "repo.star"}} + {{if $.IsStaringRepo}}{{$buttonText = ctx.Locale.Tr "repo.unstar"}}{{end}} + + + {{CountFmt .Repository.NumStars}} + +
diff --git a/templates/repo/user_cards.tmpl b/templates/repo/user_cards.tmpl index 6c43300a6aa..ee33e4b18e8 100644 --- a/templates/repo/user_cards.tmpl +++ b/templates/repo/user_cards.tmpl @@ -1,14 +1,5 @@ - -
-
+{{/* need to reload after "watch/unwatch" or "star/unstar" fetch actions */}} +
{{if .CardsTitle}}

{{.CardsTitle}} diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index dae4ed5f5b1..34485fc65d7 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,5 +1,11 @@ {{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}} -
+
{{template "repo/latest_commit" .}}
{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}
@@ -15,7 +21,7 @@ {{$entry := $item.Entry}} {{$commit := $item.Commit}} {{$submoduleFile := $item.SubmoduleFile}} -

- {{$isMember := .IsOrganizationMember}} - {{range .Members}} - {{if or $isMember (call $.IsPublicMember .ID)}} - {{ctx.AvatarUtils.Avatar . 48}} + {{range $memberUser := .OrgOverviewMembers}} + {{if or $.IsOrganizationMember (call $.IsPublicMember $memberUser.ID)}} + {{template "shared/user/avatarlink" dict "user" $memberUser "size" 32 "tooltip" true}} {{end}} {{end}}
@@ -74,7 +73,7 @@ {{.Org.NumTeams}} {{svg "octicon-chevron-right"}}
- {{range .Teams}} + {{range .OrgOverviewTeams}}
{{.Name}}

diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index abf728fc544..f8785bb466a 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -20,7 +20,7 @@

- + {{ctx.Locale.Tr "org.team_desc_helper"}}
{{if not (eq .Team.LowerName "owners")}} diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 8678ed74544..645c94d4162 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -1,17 +1,16 @@
-

+

{{.Team.Name}} -
+
{{if .Team.IsMember ctx $.SignedUser.ID}} -
- -
+ {{else if .IsOrganizationOwner}}
- +
{{end}}
@@ -85,12 +84,12 @@
{{end}}

- -
{{end}} +
+
+ + +
+
+
- {{range .Teams}} -
-
- {{.Name}} -
- {{ctx.Locale.Tr "view"}} + {{range $team := $.OrgListTeams}} +
+ -
- {{range .Members}} - {{template "shared/user/avatarlink" dict "user" .}} - {{end}} + {{if $team.Description}} +
+ {{if $team.Description}}{{$team.Description}}{{end}}
-
-

{{.NumMembers}} {{ctx.Locale.Tr "org.lower_members"}} · {{.NumRepos}} {{ctx.Locale.Tr "org.lower_repositories"}}

+ {{end}} +
+
+ {{range .Members}} + {{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}} + {{else}} + {{ctx.Locale.Tr "org.teams.add_team_member"}} + {{end}} +
{{end}}
+ {{template "base/paginate" .}}
- {{if not .ReadmeInList}}
@@ -90,15 +92,15 @@
- {{if not .IsMarkup}} + {{if not .RenderAsMarkup}} {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{end}} -
+
{{if .IsFileTooLarge}} {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} {{else if not .FileSize}} {{template "shared/fileisempty"}} - {{else if .IsMarkup}} + {{else if .RenderAsMarkup}} {{.FileContent}} {{else if .IsPlainText}}
{{if .FileContent}}{{.FileContent}}{{end}}
diff --git a/templates/swagger/openapi-viewer.tmpl b/templates/swagger/openapi-viewer.tmpl index f2f01fc0cd3..792364157d7 100644 --- a/templates/swagger/openapi-viewer.tmpl +++ b/templates/swagger/openapi-viewer.tmpl @@ -3,8 +3,8 @@ {{ctx.HeadMetaContentSecurityPolicy}} Gitea API - {{/* HINT: SWAGGER-OPENAPI-VIEWER: another place is "modules/markup/external/openapi.go" */}} + {{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}} diff --git a/tests/e2e/external-render.test.ts b/tests/e2e/external-render.test.ts index 50adb6429e0..b989c354ff5 100644 --- a/tests/e2e/external-render.test.ts +++ b/tests/e2e/external-render.test.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; import {expect, test} from '@playwright/test'; -import {login, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertNoJsError, randomString} from './utils.ts'; +import {login, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts'; test('external file', async ({page, request}) => { const repoName = `e2e-external-render-${randomString(8)}`; @@ -17,6 +17,7 @@ test('external file', async ({page, request}) => { await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`)); const frame = page.frameLocator('iframe.external-render-iframe'); await expect(frame.locator('p')).toContainText('rendered content'); + await assertFlushWithParent(iframe, page.locator('.file-view')); await assertNoJsError(page); } finally { await apiDeleteRepo(request, owner, repoName); @@ -31,13 +32,28 @@ test('openapi file', async ({page, request}) => { login(page), ]); try { - const spec = 'openapi: "3.0.0"\ninfo:\n title: Test API\n version: "1.0"\npaths: {}\n'; - await apiCreateFile(request, owner, repoName, 'openapi.yaml', spec); - await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.yaml`); + const title = 'Test & "quoted"'; + const spec = JSON.stringify({ + openapi: '3.0.0', + info: {title, version: '1.0'}, + paths: {'/pets': {get: {responses: {'200': {description: 'OK', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}}}}}, + components: {schemas: {Pet: {type: 'object', properties: {children: {type: 'array', items: {$ref: '#/components/schemas/Pet'}}}}}}, + }); + await apiCreateFile(request, owner, repoName, 'openapi.json', spec); + await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`); const iframe = page.locator('iframe.external-render-iframe'); await expect(iframe).toBeVisible(); - const frame = page.frameLocator('iframe.external-render-iframe'); - await expect(frame.locator('#swagger-ui .swagger-ui')).toBeVisible(); + const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer'); + await expect(viewer.locator('.swagger-ui')).toBeVisible(); + await expect(viewer.locator('.info .title')).toContainText(title); + // expanding the operation triggers swagger-ui's $ref resolver, which fetches window.location + // (about:srcdoc since the iframe is loaded via srcdoc); failure surfaces as "Could not resolve reference" + await viewer.locator('.opblock-tag').first().click(); + await viewer.locator('.opblock').first().click(); + await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0); + // poll: postMessage resize may not have settled yet when the visibility checks pass + await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300); + await assertFlushWithParent(iframe, page.locator('.file-view')); await assertNoJsError(page); } finally { await apiDeleteRepo(request, owner, repoName); diff --git a/tests/e2e/file-view-render.test.ts b/tests/e2e/file-view-render.test.ts new file mode 100644 index 00000000000..a3afe85b267 --- /dev/null +++ b/tests/e2e/file-view-render.test.ts @@ -0,0 +1,69 @@ +import {env} from 'node:process'; +import {expect, test} from '@playwright/test'; +import {apiCreateBranch, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts'; + +test('3d model file', async ({page, request}) => { + const repoName = `e2e-3d-render-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + await apiCreateRepo(request, {name: repoName}); + try { + const stl = 'solid test\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid test\n'; + await apiCreateFile(request, owner, repoName, 'test.stl', stl); + await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`); + const iframe = page.locator('iframe.external-render-iframe'); + await expect(iframe).toBeVisible(); + const frame = page.frameLocator('iframe.external-render-iframe'); + const viewer = frame.locator('#frontend-render-viewer'); + await expect(viewer.locator('canvas')).toBeVisible(); + expect((await viewer.boundingBox())!.height).toBeGreaterThan(300); + await assertFlushWithParent(iframe, page.locator('.file-view')); + // bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent + const [parentBg, iframeBg] = await Promise.all([ + page.evaluate(() => getComputedStyle(document.body).backgroundColor), + frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor), + ]); + expect(iframeBg).toBe(parentBg); + await assertNoJsError(page); + } finally { + await apiDeleteRepo(request, owner, repoName); + } +}); + +test('pdf file', async ({page, request}) => { + // headless playwright cannot render PDFs (PDFObject.embed returns false), so this is a limited test + const repoName = `e2e-pdf-render-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + await apiCreateRepo(request, {name: repoName}); + try { + await apiCreateFile(request, owner, repoName, 'test.pdf', '%PDF-1.0\n%%EOF\n'); + await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`); + const container = page.locator('.file-view-render-container'); + await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer'); + expect((await container.boundingBox())!.height).toBeGreaterThan(300); + await assertFlushWithParent(container, page.locator('.file-view')); + } finally { + await apiDeleteRepo(request, owner, repoName); + } +}); + +test('asciicast file', async ({page, request}) => { + // regression for repo_file.go's RefTypeNameSubURL double-escape: readme.cast on a non-ASCII branch + // is rendered via view_readme.go (no metas override), exposing the bug as a broken player URL + const repoName = `e2e-asciicast-render-${randomString(8)}`; + const owner = env.GITEA_TEST_E2E_USER; + const branch = '日本語-branch'; + const branchEnc = encodeURIComponent(branch); + await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]); + try { + const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n'; + await apiCreateFile(request, owner, repoName, 'readme.cast', cast); + await apiCreateBranch(request, owner, repoName, branch); + await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`); + const container = page.locator('.asciinema-player-container'); + await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`); + await expect(container.locator('.ap-wrapper')).toBeVisible(); + expect((await container.boundingBox())!.height).toBeGreaterThan(300); + } finally { + await apiDeleteRepo(request, owner, repoName); + } +}); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 7a4a91c2699..08de0241268 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -1,6 +1,6 @@ import {env} from 'node:process'; import {expect} from '@playwright/test'; -import type {APIRequestContext, Page} from '@playwright/test'; +import type {APIRequestContext, Locator, Page} from '@playwright/test'; /** Generate a random alphanumeric string. */ export function randomString(length: number): string { @@ -67,6 +67,13 @@ export async function apiCreateFile(requestContext: APIRequestContext, owner: st }), 'apiCreateFile'); } +export async function apiCreateBranch(requestContext: APIRequestContext, owner: string, repo: string, newBranch: string) { + await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/branches`, { + headers: apiHeaders(), + data: {new_branch_name: newBranch}, + }), 'apiCreateBranch'); +} + export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) { await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, { headers: apiHeaders(), @@ -115,6 +122,15 @@ export async function assertNoJsError(page: Page) { await expect(page.locator('.js-global-error')).toHaveCount(0); } +/* asserts the child has no horizontal inset from its parent — catches padding/border anywhere + * in between regardless of which element declares it */ +export async function assertFlushWithParent(child: Locator, parent: Locator) { + const [childBox, parentBox] = await Promise.all([child.boundingBox(), parent.boundingBox()]); + if (!childBox || !parentBox) throw new Error('boundingBox returned null'); + expect(childBox.x).toBe(parentBox.x); + expect(childBox.width).toBe(parentBox.width); +} + export async function logout(page: Page) { await page.context().clearCookies(); // workaround issues related to fomantic dropdown await page.goto('/'); diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 681b981a4e1..97ef7e0b22a 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -108,7 +108,12 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, + ``+ + ``+ + `
<script></script>
`, + respSub.Body.String(), + ) }) }) @@ -129,9 +134,14 @@ func TestExternalMarkupRenderer(t *testing.T) { }) t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { - req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") + req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer?a=1%2f2") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, + ``+ + ``+ + ``, + respSub.Body.String(), + ) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) diff --git a/vite.config.ts b/vite.config.ts index 7249cb902eb..731726c3183 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -152,9 +152,15 @@ function iifePlugin(sourceFileName: string): Plugin { if (!entry) throw new Error('IIFE build produced no output'); const manifestPath = join(outDir, '.vite', 'manifest.json'); - const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8')); - manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true}; - writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2)); + try { + const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8')); + manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true}; + writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2)); + } catch { + // FIXME: if it throws error here, the real Vite compilation error will be hidden, and makes the debug very difficult + // Need to find a correct way to handle errors. + console.error(`Failed to update manifest for ${sourceFileName}`); + } }, }; } @@ -165,6 +171,7 @@ function reducedSourcemapPlugin(): Plugin { 'js/index.', 'js/iife.', 'js/swagger.', + 'js/external-render-frontend.', 'js/external-render-helper.', 'js/eventsource.sharedworker.', ]; @@ -251,8 +258,10 @@ export default defineConfig(commonViteOpts({ manifest: true, rolldownOptions: { input: { + // FIXME: INCORRECT-VITE-MANIFEST-PARSER: the "css importing" logic in backend is wrong index: join(import.meta.dirname, 'web_src/js/index.ts'), swagger: join(import.meta.dirname, 'web_src/js/swagger.ts'), + 'external-render-frontend': join(import.meta.dirname, 'web_src/js/external-render-frontend.ts'), 'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/eventsource.sharedworker.ts'), devtest: join(import.meta.dirname, 'web_src/css/devtest.css'), ...themes, diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index efa6947ef14..d90e3e01ec5 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -458,9 +458,11 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] { } .external-render-iframe { + display: block; /* removes the inline baseline gap below the iframe */ width: 100%; height: max(300px, 80vh); border: none; + border-radius: 0 0 var(--border-radius) var(--border-radius); } .markup-content-iframe { diff --git a/web_src/js/external-render-frontend.ts b/web_src/js/external-render-frontend.ts new file mode 100644 index 00000000000..9d969bcf900 --- /dev/null +++ b/web_src/js/external-render-frontend.ts @@ -0,0 +1,67 @@ +import type {FrontendRenderFunc, FrontendRenderOptions} from './render/plugin.ts'; + +type LazyLoadFunc = () => Promise<{frontendRender: FrontendRenderFunc}>; + +// It must use a wrapper function to avoid the "import" statement being treated +// as static import and cause the all plugins being loaded together, +// We only need to load the plugins we need. +const frontendPlugins: Record = { + 'viewer-3d': () => import('./render/plugins/frontend-viewer-3d.ts'), + 'openapi-swagger': () => import('./render/plugins/frontend-openapi-swagger.ts'), +}; + +class Options implements FrontendRenderOptions { + container: HTMLElement; + treePath: string; + rawEncoding: string; + rawString: string; + cachedBytes: Uint8Array | null = null; + cachedString: string | null = null; + constructor(container: HTMLElement, treePath: string, rawEncoding: string, rawString: string) { + this.container = container; + this.treePath = treePath; + this.rawEncoding = rawEncoding; + this.rawString = rawString; + } + decodeBase64(): Uint8Array { + return Uint8Array.from(atob(this.rawString), (c) => c.charCodeAt(0)); + } + contentBytes(): Uint8Array { + if (this.cachedBytes === null) { + this.cachedBytes = this.rawEncoding === 'base64' ? this.decodeBase64() : new TextEncoder().encode(this.rawString); + } + return this.cachedBytes; + } + contentString(): string { + if (this.cachedString === null) { + this.cachedString = this.rawEncoding === 'base64' ? new TextDecoder('utf-8').decode(this.decodeBase64()) : this.rawString; + } + return this.cachedString; + } +} + +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')!; + + 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; + for (const name of renderNames) { + if (!(name in frontendPlugins)) continue; + const plugin = await frontendPlugins[name](); + found = true; + if (await plugin.frontendRender(opts)) break; + } + + if (!found) { + viewerContainer.textContent = 'No frontend render plugin found for this file, but backend declares that there must be one, there must be a bug'; + } +} + +initFrontendExternalRender(); diff --git a/web_src/js/external-render-helper.ts b/web_src/js/external-render-helper.ts index 9162d0f550d..f92aeb9c6c9 100644 --- a/web_src/js/external-render-helper.ts +++ b/web_src/js/external-render-helper.ts @@ -26,14 +26,16 @@ function isValidCssColor(s: string | null): boolean { return reHex.test(s) || reRgb.test(s); } -const url = new URL(window.location.href); +const thisScriptElem = document.querySelector('script#gitea-external-render-helper'); +const queryString = thisScriptElem?.getAttribute('data-render-query-string') ?? window.location.search.substring(1); +const queryParams = new URLSearchParams(queryString); -const isDarkTheme = url.searchParams.get('gitea-is-dark-theme') === 'true'; +const isDarkTheme = queryParams.get('gitea-is-dark-theme') === 'true'; if (isDarkTheme) { document.documentElement.setAttribute('data-gitea-theme-dark', String(isDarkTheme)); } -const backgroundColor = url.searchParams.get('gitea-iframe-bgcolor'); +const backgroundColor = queryParams.get('gitea-iframe-bgcolor'); if (isValidCssColor(backgroundColor)) { // create a style element to set background color, then it can be overridden by the content page's own style if needed const style = document.createElement('style'); @@ -41,12 +43,13 @@ if (isValidCssColor(backgroundColor)) { :root { --gitea-iframe-bgcolor: ${backgroundColor}; } +html, body { margin: 0; padding: 0 } body { background: ${backgroundColor}; } `; document.head.append(style); } -const iframeId = url.searchParams.get('gitea-iframe-id'); +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 = {}) => { diff --git a/web_src/js/features/file-view.ts b/web_src/js/features/file-view.ts index ff9e8cfa263..b9a5dd5094f 100644 --- a/web_src/js/features/file-view.ts +++ b/web_src/js/features/file-view.ts @@ -1,29 +1,19 @@ -import type {FileRenderPlugin} from '../render/plugin.ts'; -import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts'; -import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts'; +import type {InplaceRenderPlugin} from '../render/plugin.ts'; +import {newInplacePluginPdfViewer} from '../render/plugins/inplace-pdf-viewer.ts'; import {registerGlobalInitFunc} from '../modules/observer.ts'; -import {createElementFromHTML, showElem, toggleElemClass} from '../utils/dom.ts'; +import {createElementFromHTML} from '../utils/dom.ts'; import {html} from '../utils/html.ts'; import {basename} from '../utils.ts'; -const plugins: FileRenderPlugin[] = []; +const inplacePlugins: InplaceRenderPlugin[] = []; -function initPluginsOnce(): void { - if (plugins.length) return; - plugins.push(newRenderPlugin3DViewer(), newRenderPluginPdfViewer()); +function initInplacePluginsOnce(): void { + if (inplacePlugins.length) return; + inplacePlugins.push(newInplacePluginPdfViewer()); } -function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null { - return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null; -} - -function showRenderRawFileButton(elFileView: HTMLElement, renderContainer: HTMLElement | null): void { - const toggleButtons = elFileView.querySelector('.file-view-toggle-buttons')!; - showElem(toggleButtons); - const displayingRendered = Boolean(renderContainer); - toggleElemClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist - toggleElemClass(toggleButtons.querySelector('.file-view-toggle-rendered')!, 'active', displayingRendered); - // TODO: if there is only one button, hide it? +function findInplaceRenderPlugin(filename: string, mimeType: string): InplaceRenderPlugin | null { + return inplacePlugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null; } async function renderRawFileToContainer(container: HTMLElement, rawFileLink: string, mimeType: string) { @@ -32,7 +22,7 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str let rendered = false, errorMsg = ''; try { - const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType); + const plugin = findInplaceRenderPlugin(basename(rawFileLink), mimeType); if (plugin) { container.classList.add('is-loading'); container.setAttribute('data-render-name', plugin.name); // not used yet @@ -61,16 +51,13 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str export function initRepoFileView(): void { registerGlobalInitFunc('initRepoFileView', async (elFileView: HTMLElement) => { - initPluginsOnce(); + initInplacePluginsOnce(); const rawFileLink = elFileView.getAttribute('data-raw-file-link')!; const mimeType = elFileView.getAttribute('data-mime-type') || ''; // not used yet - // TODO: we should also provide the prefetched file head bytes to let the plugin decide whether to render or not - const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType); + const plugin = findInplaceRenderPlugin(basename(rawFileLink), mimeType); if (!plugin) return; const renderContainer = elFileView.querySelector('.file-view-render-container'); - showRenderRawFileButton(elFileView, renderContainer); - // maybe in the future multiple plugins can render the same file, so we should not assume only one plugin will render it if (renderContainer) await renderRawFileToContainer(renderContainer, rawFileLink, mimeType); }); } diff --git a/web_src/js/markup/content.ts b/web_src/js/markup/content.ts index 63510458f9b..77ba0eaed4f 100644 --- a/web_src/js/markup/content.ts +++ b/web_src/js/markup/content.ts @@ -3,13 +3,14 @@ import {initMarkupCodeMath} from './math.ts'; import {initMarkupCodeCopy} from './codecopy.ts'; import {initMarkupRenderAsciicast} from './asciicast.ts'; import {initMarkupTasklist} from './tasklist.ts'; -import {registerGlobalSelectorFunc} from '../modules/observer.ts'; -import {initMarkupRenderIframe} from './render-iframe.ts'; +import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; +import {initExternalRenderIframe} from './render-iframe.ts'; import {initMarkupRefIssue} from './refissue.ts'; import {toggleElemClass} from '../utils/dom.ts'; // code that runs for all markup content export function initMarkupContent(): void { + registerGlobalInitFunc('initExternalRenderIframe', initExternalRenderIframe); registerGlobalSelectorFunc('.markup', (el: HTMLElement) => { if (el.matches('.truncated-markup')) { // when the rendered markup is truncated (e.g.: user's home activity feed) @@ -25,7 +26,6 @@ export function initMarkupContent(): void { initMarkupCodeMermaid(el); initMarkupCodeMath(el); initMarkupRenderAsciicast(el); - initMarkupRenderIframe(el); initMarkupRefIssue(el); }); } diff --git a/web_src/js/markup/render-iframe.ts b/web_src/js/markup/render-iframe.ts index 09493df7802..2b1b06e5c0b 100644 --- a/web_src/js/markup/render-iframe.ts +++ b/web_src/js/markup/render-iframe.ts @@ -1,5 +1,6 @@ -import {generateElemId, queryElemChildren} from '../utils/dom.ts'; +import {generateElemId} from '../utils/dom.ts'; import {isDarkTheme} from '../utils.ts'; +import {GET} from '../modules/fetch.ts'; function safeRenderIframeLink(link: any): string | null { try { @@ -41,7 +42,7 @@ function getRealBackgroundColor(el: HTMLElement) { return ''; } -async function loadRenderIframeContent(iframe: HTMLIFrameElement) { +export async function initExternalRenderIframe(iframe: HTMLIFrameElement) { const iframeSrcUrl = iframe.getAttribute('data-src')!; if (!iframe.id) iframe.id = generateElemId('gitea-iframe-'); @@ -62,9 +63,10 @@ async function loadRenderIframeContent(iframe: HTMLIFrameElement) { u.searchParams.set('gitea-is-dark-theme', String(isDarkTheme())); u.searchParams.set('gitea-iframe-id', iframe.id); u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe)); - iframe.src = u.href; -} -export function initMarkupRenderIframe(el: HTMLElement) { - queryElemChildren(el, 'iframe.external-render-iframe', loadRenderIframeContent); + // 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(); } diff --git a/web_src/js/render/plugin.ts b/web_src/js/render/plugin.ts index 234be4118f4..368c73dea36 100644 --- a/web_src/js/render/plugin.ts +++ b/web_src/js/render/plugin.ts @@ -1,10 +1,21 @@ -export type FileRenderPlugin = { - // unique plugin name +// there are 2 kinds of plugins: +// * "inplace" plugins: render file content in-place, e.g. PDF viewer +// * "frontend" plugins: render file content in a separate iframe by a huge frontend library (need to protect from XSS risks) +// TODO: render plugin enhancements, not needed at the moment, leave the problems to the future when the problems actually come: +// 1. provide the prefetched file head bytes to let the plugin decide whether to render or not +// 2. multiple plugins can render the same file, so we should not assume only one plugin will render it + +export type InplaceRenderPlugin = { name: string; - - // test if plugin can handle a specified file canHandle: (filename: string, mimeType: string) => boolean; - - // render file content render: (container: HTMLElement, fileUrl: string, options?: any) => Promise; }; + +export type FrontendRenderOptions = { + container: HTMLElement; + treePath: string; + contentString(): string; + contentBytes(): Uint8Array; +}; + +export type FrontendRenderFunc = (opts: FrontendRenderOptions) => Promise; diff --git a/web_src/js/render/plugins/3d-viewer.ts b/web_src/js/render/plugins/3d-viewer.ts deleted file mode 100644 index f997790af69..00000000000 --- a/web_src/js/render/plugins/3d-viewer.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type {FileRenderPlugin} from '../plugin.ts'; -import {extname} from '../../utils.ts'; - -// support common 3D model file formats, use online-3d-viewer library for rendering - -/* a simple text STL file example: -solid SimpleTriangle - facet normal 0 0 1 - outer loop - vertex 0 0 0 - vertex 1 0 0 - vertex 0 1 0 - endloop - endfacet -endsolid SimpleTriangle -*/ - -export function newRenderPlugin3DViewer(): FileRenderPlugin { - // Some extensions are text-based formats: - // .3mf .amf .brep: XML - // .fbx: XML or BINARY - // .dae .gltf: JSON - // .ifc, .igs, .iges, .stp, .step are: TEXT - // .stl .ply: TEXT or BINARY - // .obj .off .wrl: TEXT - // So we need to be able to render when the file is recognized as plaintext file by backend. - // - // It needs more logic to make it overall right (render a text 3D model automatically): - // we need to distinguish the ambiguous filename extensions. - // For example: "*.obj, *.off, *.step" might be or not be a 3D model file. - // So when it is a text file, we can't assume that "we only render it by 3D plugin", - // otherwise the end users would be impossible to view its real content when the file is not a 3D model. - const SUPPORTED_EXTENSIONS = [ - '.3dm', '.3ds', '.3mf', '.amf', '.bim', '.brep', - '.dae', '.fbx', '.fcstd', '.glb', '.gltf', - '.ifc', '.igs', '.iges', '.stp', '.step', - '.stl', '.obj', '.off', '.ply', '.wrl', - ]; - - return { - name: '3d-model-viewer', - - canHandle(filename: string, _mimeType: string): boolean { - const ext = extname(filename).toLowerCase(); - return SUPPORTED_EXTENSIONS.includes(ext); - }, - - async render(container: HTMLElement, fileUrl: string): Promise { - // TODO: height and/or max-height? - const OV = await import('online-3d-viewer'); - const viewer = new OV.EmbeddedViewer(container, { - backgroundColor: new OV.RGBAColor(59, 68, 76, 0), - defaultColor: new OV.RGBColor(65, 131, 196), - edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1), - }); - viewer.LoadModelFromUrlList([fileUrl]); - }, - }; -} diff --git a/web_src/js/render/plugins/frontend-openapi-swagger.ts b/web_src/js/render/plugins/frontend-openapi-swagger.ts new file mode 100644 index 00000000000..cc8d3451f24 --- /dev/null +++ b/web_src/js/render/plugins/frontend-openapi-swagger.ts @@ -0,0 +1,17 @@ +import type {FrontendRenderFunc} from '../plugin.ts'; +import {initSwaggerUI} from '../swagger.ts'; + +// HINT: SWAGGER-CSS-IMPORT: this import is also necessary when swagger is used as a frontend external render +// It must be on top-level, doesn't work in a function +// Static import doesn't work (it needs to use manifest.json to manually add the CSS file) +await import('../../../css/swagger.css'); + +export const frontendRender: FrontendRenderFunc = async (opts): Promise => { + try { + await initSwaggerUI(opts.container, {specText: opts.contentString()}); + return true; + } catch (error) { + console.error(error); + return false; + } +}; diff --git a/web_src/js/render/plugins/frontend-viewer-3d.ts b/web_src/js/render/plugins/frontend-viewer-3d.ts new file mode 100644 index 00000000000..f7d1c4d0541 --- /dev/null +++ b/web_src/js/render/plugins/frontend-viewer-3d.ts @@ -0,0 +1,36 @@ +import type {FrontendRenderFunc} from '../plugin.ts'; +import {basename} from '../../utils.ts'; +import * as OV from 'online-3d-viewer'; +import {colord} from 'colord'; + +/* a simple text STL file example: +solid SimpleTriangle + facet normal 0 0 1 + outer loop + vertex 0 0 0 + vertex 1 0 0 + vertex 0 1 0 + endloop + endfacet +endsolid SimpleTriangle +*/ + +export const frontendRender: FrontendRenderFunc = async (opts): Promise => { + try { + opts.container.style.height = `${window.innerHeight}px`; + const bgColor = colord(getComputedStyle(document.body).backgroundColor).toRgb(); + const primaryColor = colord(getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()).toRgb(); + const viewer = new OV.EmbeddedViewer(opts.container, { + backgroundColor: new OV.RGBAColor(bgColor.r, bgColor.g, bgColor.b, 255), + defaultColor: new OV.RGBColor(primaryColor.r, primaryColor.g, primaryColor.b), + edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1), + }); + const blob = new Blob([opts.contentBytes()]); + const file = new File([blob], basename(opts.treePath)); + viewer.LoadModelFromFileList([file]); + return true; + } catch (error) { + console.error(error); + return false; + } +}; diff --git a/web_src/js/render/plugins/pdf-viewer.ts b/web_src/js/render/plugins/inplace-pdf-viewer.ts similarity index 69% rename from web_src/js/render/plugins/pdf-viewer.ts rename to web_src/js/render/plugins/inplace-pdf-viewer.ts index c7040e96ef1..7447f38ec4e 100644 --- a/web_src/js/render/plugins/pdf-viewer.ts +++ b/web_src/js/render/plugins/inplace-pdf-viewer.ts @@ -1,6 +1,6 @@ -import type {FileRenderPlugin} from '../plugin.ts'; +import type {InplaceRenderPlugin} from '../plugin.ts'; -export function newRenderPluginPdfViewer(): FileRenderPlugin { +export function newInplacePluginPdfViewer(): InplaceRenderPlugin { return { name: 'pdf-viewer', @@ -11,6 +11,7 @@ export function newRenderPluginPdfViewer(): FileRenderPlugin { async render(container: HTMLElement, fileUrl: string): Promise { const PDFObject = await import('pdfobject'); // TODO: the PDFObject library does not support dynamic height adjustment, + // TODO: it seems that this render must be an inplace render, because the URL must be accessible from the current context container.style.height = `${window.innerHeight - 100}px`; if (!PDFObject.default.embed(fileUrl, container)) { throw new Error('Unable to render the PDF file'); diff --git a/web_src/js/render/swagger.ts b/web_src/js/render/swagger.ts new file mode 100644 index 00000000000..27e678f34bf --- /dev/null +++ b/web_src/js/render/swagger.ts @@ -0,0 +1,54 @@ +// AVOID importing other unneeded main site JS modules to prevent unnecessary code and dependencies and chunks. +// This module is used by both the Gitea API page and the frontend external render. +// It doesn't need any code from main site's modules (at the moment). + +import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js'; +import {load as loadYaml} from 'js-yaml'; + +function syncDarkModeClass(): void { + // if the viewer is embedded in an iframe (external render), use the parent's theme (passed via query param) + // otherwise, if it is for Gitea's API, it is a standalone page, use the site's theme (detected from theme CSS variable) + const url = new URL(window.location.href); + const giteaIsDarkTheme = url.searchParams.get('gitea-is-dark-theme') ?? + window.getComputedStyle(document.documentElement).getPropertyValue('--is-dark-theme').trim(); + const isDark = giteaIsDarkTheme ? giteaIsDarkTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.classList.toggle('dark-mode', isDark); +} + +export async function initSwaggerUI(container: HTMLElement, opts: {specText: string}): Promise { + // swagger-ui has built-in dark mode triggered by html.dark-mode class + syncDarkModeClass(); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkModeClass); + + let spec: any; + const specText = opts.specText.trim(); + if (specText.startsWith('{')) { + spec = JSON.parse(specText); + } else { + spec = loadYaml(specText); + } + + // Make the page's protocol be at the top of the schemes list + const proto = window.location.protocol.slice(0, -1); + if (spec?.schemes) { + spec.schemes.sort((a: string, b: string) => { + if (a === proto) return -1; + if (b === proto) return 1; + return 0; + }); + } + + SwaggerUI({ + spec, + domNode: container, + deepLinking: window.location.protocol !== 'about:', // pushState fails inside about:srcdoc iframes + docExpansion: 'none', + defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete + presets: [ + SwaggerUI.presets.apis, + ], + plugins: [ + SwaggerUI.plugins.DownloadUrl, + ], + }); +} diff --git a/web_src/js/swagger.ts b/web_src/js/swagger.ts index b2f6a61030a..f7a852098a2 100644 --- a/web_src/js/swagger.ts +++ b/web_src/js/swagger.ts @@ -1,70 +1,14 @@ -// AVOID importing other unneeded main site JS modules to prevent unnecessary code and dependencies and chunks. -// -// Swagger JS is standalone because it is also used by external render like "File View -> OpenAPI render", -// and it doesn't need any code from main site's modules (at the moment). -// -// In the future, if there are common utilities needed by both main site and standalone Swagger, -// we can merge this standalone module into "index.ts", do pay attention to the following problems: -// * HINT: SWAGGER-OPENAPI-VIEWER: there are different places rendering the swagger UI. -// * Handle CSS styles carefully for different cases (standalone page, embedded in iframe) -// * Take care of the JS code introduced by "index.ts" and "iife.ts", there might be global variable dependency and event listeners. - +// FIXME: INCORRECT-VITE-MANIFEST-PARSER: it just happens to work for current dependencies +// If this module depends on another one and that one imports "swagger.css", then {{AssetURI "css/swagger.css"}} won't work import '../css/swagger.css'; -import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js'; -import 'swagger-ui-dist/swagger-ui.css'; -import {load as loadYaml} from 'js-yaml'; +import {initSwaggerUI} from './render/swagger.ts'; -function syncDarkModeClass(): void { - // if the viewer is embedded in an iframe (external render), use the parent's theme (passed via query param) - // otherwise, if it is for Gitea's API, it is a standalone page, use the site's theme (detected from theme CSS variable) - const url = new URL(window.location.href); - const giteaIsDarkTheme = url.searchParams.get('gitea-is-dark-theme') ?? - window.getComputedStyle(document.documentElement).getPropertyValue('--is-dark-theme').trim(); - const isDark = giteaIsDarkTheme ? giteaIsDarkTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches; - document.documentElement.classList.toggle('dark-mode', isDark); -} - -async function initSwaggerUI() { - // swagger-ui has built-in dark mode triggered by html.dark-mode class - syncDarkModeClass(); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkModeClass); - - const elSwaggerUi = document.querySelector('#swagger-ui')!; +async function initGiteaAPIViewer() { + const elSwaggerUi = document.querySelector('#swagger-ui')!; const url = elSwaggerUi.getAttribute('data-source')!; - let spec: any; - if (url) { - const res = await fetch(url); // eslint-disable-line no-restricted-globals - spec = await res.json(); - } else { - const elSpecContent = elSwaggerUi.querySelector('.swagger-spec-content')!; - const filename = elSpecContent.getAttribute('data-spec-filename'); - const isJson = filename?.toLowerCase().endsWith('.json'); - spec = isJson ? JSON.parse(elSpecContent.value) : loadYaml(elSpecContent.value); - } - - // Make the page's protocol be at the top of the schemes list - const proto = window.location.protocol.slice(0, -1); - if (spec?.schemes) { - spec.schemes.sort((a: string, b: string) => { - if (a === proto) return -1; - if (b === proto) return 1; - return 0; - }); - } - - SwaggerUI({ - spec, - dom_id: '#swagger-ui', - deepLinking: true, - docExpansion: 'none', - defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete - presets: [ - SwaggerUI.presets.apis, - ], - plugins: [ - SwaggerUI.plugins.DownloadUrl, - ], - }); + const res = await fetch(url); // eslint-disable-line no-restricted-globals + // HINT: SWAGGER-CSS-IMPORT: this is used in the standalone page which already has the related CSS imported by `` + await initSwaggerUI(elSwaggerUi, {specText: await res.text()}); } -initSwaggerUI(); +initGiteaAPIViewer(); From cf3f8e807a4e3cb8d42ab3afa11d552c13b94540 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 18 Apr 2026 16:01:58 +0800 Subject: [PATCH 043/126] Avoid top-level await (#37272) --- .../css/{swagger.css => swagger-render.css} | 28 ------------------ web_src/css/swagger-standalone.css | 29 +++++++++++++++++++ .../plugins/frontend-openapi-swagger.ts | 8 +++-- web_src/js/swagger.ts | 2 +- 4 files changed, 35 insertions(+), 32 deletions(-) rename web_src/css/{swagger.css => swagger-render.css} (52%) create mode 100644 web_src/css/swagger-standalone.css diff --git a/web_src/css/swagger.css b/web_src/css/swagger-render.css similarity index 52% rename from web_src/css/swagger.css rename to web_src/css/swagger-render.css index c20eda7948d..1def667e505 100644 --- a/web_src/css/swagger.css +++ b/web_src/css/swagger-render.css @@ -1,9 +1,5 @@ @import "../../node_modules/swagger-ui-dist/swagger-ui.css"; -body { - margin: 0; -} - html, html body, html .swagger-ui, @@ -15,27 +11,3 @@ html .swagger-ui .scheme-container { html.dark-mode .swagger-ui table.headers td { color: var(--color-text) !important; } - -.swagger-back-link { - color: var(--color-primary); - text-decoration: none; - position: absolute; - top: 1rem; - right: 1.5rem; - display: flex; - align-items: center; -} - -.swagger-back-link:hover { - text-decoration: underline; -} - -.swagger-back-link svg { - color: inherit; - fill: currentcolor; - margin-right: 0.5rem; -} - -.swagger-spec-content { - display: none; -} diff --git a/web_src/css/swagger-standalone.css b/web_src/css/swagger-standalone.css new file mode 100644 index 00000000000..ae36ab49cf5 --- /dev/null +++ b/web_src/css/swagger-standalone.css @@ -0,0 +1,29 @@ +@import "swagger-render.css"; + +body { + margin: 0; +} + +.swagger-back-link { + color: var(--color-primary); + text-decoration: none; + position: absolute; + top: 1rem; + right: 1.5rem; + display: flex; + align-items: center; +} + +.swagger-back-link:hover { + text-decoration: underline; +} + +.swagger-back-link svg { + color: inherit; + fill: currentcolor; + margin-right: 0.5rem; +} + +.swagger-spec-content { + display: none; +} diff --git a/web_src/js/render/plugins/frontend-openapi-swagger.ts b/web_src/js/render/plugins/frontend-openapi-swagger.ts index cc8d3451f24..99410fd496a 100644 --- a/web_src/js/render/plugins/frontend-openapi-swagger.ts +++ b/web_src/js/render/plugins/frontend-openapi-swagger.ts @@ -2,12 +2,14 @@ import type {FrontendRenderFunc} from '../plugin.ts'; import {initSwaggerUI} from '../swagger.ts'; // HINT: SWAGGER-CSS-IMPORT: this import is also necessary when swagger is used as a frontend external render -// It must be on top-level, doesn't work in a function -// Static import doesn't work (it needs to use manifest.json to manually add the CSS file) -await import('../../../css/swagger.css'); +// But it can't share the same CSS file with the standalone page: it triggers our Vite manifest parser's bug +// Although single top-level "await import(css)" can work, it requires es2022. +// Otherwise, single function-level "await import(css)" can't work due to Vite's dependency analysis and bundling. +import '../../../css/swagger-render.css'; export const frontendRender: FrontendRenderFunc = async (opts): Promise => { try { + await import('../../../css/swagger-render.css'); await initSwaggerUI(opts.container, {specText: opts.contentString()}); return true; } catch (error) { diff --git a/web_src/js/swagger.ts b/web_src/js/swagger.ts index f7a852098a2..ee0fd289367 100644 --- a/web_src/js/swagger.ts +++ b/web_src/js/swagger.ts @@ -1,6 +1,6 @@ // FIXME: INCORRECT-VITE-MANIFEST-PARSER: it just happens to work for current dependencies // If this module depends on another one and that one imports "swagger.css", then {{AssetURI "css/swagger.css"}} won't work -import '../css/swagger.css'; +import '../css/swagger-standalone.css'; import {initSwaggerUI} from './render/swagger.ts'; async function initGiteaAPIViewer() { From 98202110beb7dd3ac239953e866914381cb69ec6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 18 Apr 2026 10:49:40 +0200 Subject: [PATCH 044/126] Upgrade go-git to v5.18.0 (#37268) Fixes GHSA-3xc5-wrhm-f963 (credential exposure on HTTP redirects). --- This PR was written with the help of Claude Opus 4.6 Co-authored-by: Claude (Opus 4.6) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d87bf6840c2..d1aac0db900 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/go-co-op/gocron/v2 v2.20.0 github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-git/go-billy/v5 v5.8.0 - github.com/go-git/go-git/v5 v5.17.2 + github.com/go-git/go-git/v5 v5.18.0 github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-sql-driver/mysql v1.9.3 diff --git a/go.sum b/go.sum index ee60fd36e43..547c61d826c 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= -github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= From 0824610e3975bdd355766d8359e498fb2944bea4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 18 Apr 2026 20:55:01 +0200 Subject: [PATCH 045/126] Remove `SubmitEvent` polyfill (#37276) Remove this obsolete polyfill as per https://github.com/go-gitea/gitea/pull/37270#issuecomment-4273399551. Co-authored-by: Claude (Opus 4.7) --- web_src/js/features/common-fetch-action.ts | 8 ++++---- web_src/js/features/repo-diff.ts | 6 +++--- web_src/js/index.ts | 2 -- web_src/js/utils/dom.ts | 22 ---------------------- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index f3decf53b2a..24f4ad2e02f 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -1,6 +1,6 @@ import {GET, request} from '../modules/fetch.ts'; import {hideToastsAll, showErrorToast} from '../modules/toast.ts'; -import {addDelegatedEventListener, createElementFromHTML, submitEventSubmitter} from '../utils/dom.ts'; +import {addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts'; import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {registerGlobalSelectorFunc} from '../modules/observer.ts'; @@ -146,7 +146,7 @@ async function performActionRequest(el: HTMLElement, opt: FetchActionOpts) { } type SubmitFormFetchActionOpts = { - formSubmitter?: HTMLElement; + formSubmitter?: HTMLElement | null; formData?: FormData; }; @@ -396,10 +396,10 @@ export function initGlobalFetchAction() { // * it has "-header" and "-content" variants to set the header and content of the "confirm modal" // * it can refer an existing modal element by "#the-modal-id" - addDelegatedEventListener(document, 'submit', '.form-fetch-action', async (el: HTMLFormElement, e) => { + addDelegatedEventListener(document, 'submit', '.form-fetch-action', async (el, e) => { // "fetch-action" will use the form's data to send the request e.preventDefault(); - await submitFormFetchAction(el, {formSubmitter: submitEventSubmitter(e)}); + await submitFormFetchAction(el, {formSubmitter: e.submitter}); }); addDelegatedEventListener(document, 'click', '.link-action', async (el, e) => { diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 08e26cfc6d8..500c86fec6a 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -5,7 +5,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts'; import {initViewedCheckboxListenerFor, initExpandAndCollapseFilesButton} from './pull-view-file.ts'; import {initImageDiff} from './imagediff.ts'; import {showErrorToast} from '../modules/toast.ts'; -import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts'; +import {queryElemSiblings, hideElem, showElem, animateOnce, addDelegatedEventListener, createElementFromHTML, queryElems} from '../utils/dom.ts'; import {POST, GET} from '../modules/fetch.ts'; import {createTippy} from '../modules/tippy.ts'; import {invertFileFolding} from './file-fold.ts'; @@ -41,8 +41,8 @@ function initRepoDiffConversationForm() { const formData = new FormData(form); // if the form is submitted by a button, append the button's name and value to the form data - const submitter = submitEventSubmitter(e); - const isSubmittedByButton = (submitter?.nodeName === 'BUTTON') || (submitter?.nodeName === 'INPUT' && submitter.type === 'submit'); + const submitter = e.submitter; + const isSubmittedByButton = submitter instanceof HTMLButtonElement || (submitter instanceof HTMLInputElement && submitter.type === 'submit'); if (isSubmittedByButton && submitter.name) { formData.append(submitter.name, submitter.value); } diff --git a/web_src/js/index.ts b/web_src/js/index.ts index ba40bc9b9e4..c65972e3377 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -45,7 +45,6 @@ import {initCaptcha} from './features/captcha.ts'; import {initRepositoryActionView} from './features/repo-actions.ts'; import {initGlobalTooltips} from './modules/tippy.ts'; import {initGiteaFomantic} from './modules/fomantic.ts'; -import {initSubmitEventPolyfill} from './utils/dom.ts'; import {initRepoIssueList} from './features/repo-issue-list.ts'; import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts'; import {initRepoContributors} from './features/contributors.ts'; @@ -69,7 +68,6 @@ import {initDevtest} from './modules/devtest.ts'; const initStartTime = performance.now(); const initPerformanceTracer = callInitFunctions([ - initSubmitEventPolyfill, initGiteaFomantic, initGlobalComponent, diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 6833a196c3b..e0c6f351939 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -257,28 +257,6 @@ export function loadElem(el: LoadableElement, src: string) { }); } -// some browsers like PaleMoon don't have "SubmitEvent" support, so polyfill it by a tricky method: use the last clicked button as submitter -// it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)" -const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined'; - -export function submitEventSubmitter(e: any) { - e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself - return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter; -} - -function submitEventPolyfillListener(e: Event) { - const form = (e.target as HTMLElement).closest('form'); - if (!form) return; - form._submitter = (e.target as HTMLElement).closest('button:not([type]), button[type="submit"], input[type="submit"]'); -} - -export function initSubmitEventPolyfill() { - if (!needSubmitEventPolyfill) return; - console.warn(`This browser doesn't have "SubmitEvent" support, use a tricky method to polyfill`); - document.body.addEventListener('click', submitEventPolyfillListener); - document.body.addEventListener('focus', submitEventPolyfillListener); -} - export function isElemVisible(el: HTMLElement): boolean { // Check if an element is visible, equivalent to jQuery's `:visible` pseudo. // This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem" From af31b9d433b43ce7300de347ad8ea6314886766e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 19 Apr 2026 03:32:49 +0800 Subject: [PATCH 046/126] Refactor LDAP tests (#37274) Not really fix #37263, just make things better, and easy to catch more clues if it would fail again. --- modules/testlogger/testlogger.go | 14 +- routers/web/admin/auths.go | 2 +- services/forms/auth_form.go | 1 - tests/integration/auth_ldap_test.go | 295 ++++++++++++-------------- tests/integration/html_helper.go | 4 +- tests/integration/integration_test.go | 2 +- 6 files changed, 151 insertions(+), 167 deletions(-) diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 39232a3eed3..217121f604b 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -90,8 +90,8 @@ func (w *testLoggerWriterCloser) Reset() { w.Unlock() } -// Printf takes a format and args and prints the string to os.Stdout -func Printf(format string, args ...any) { +// stdoutPrintf takes a format and args and prints the string to os.Stdout +func stdoutPrintf(format string, args ...any) { if !log.CanColorStdout { for i := range args { if c, ok := args[i].(*log.ColoredValue); ok { @@ -118,20 +118,20 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { deferHasRun := false t.Cleanup(func() { if !deferHasRun { - Printf("!!! %s defer function hasn't been run but Cleanup is called, usually caused by panic", t.Name()) + stdoutPrintf("!!! %s defer function hasn't been run but Cleanup is called, usually caused by panic\n", t.Name()) } }) - Printf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) + stdoutPrintf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) WriterCloser.pushT(t) timeoutChecker := time.AfterFunc(TestTimeout, func() { - Printf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, getRuntimeStackAll()) + stdoutPrintf("!!! %s ... timeout: %v ... stacktrace:\n%s\n\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestTimeout, getRuntimeStackAll()) }) return func() { deferHasRun = true flushStart := time.Now() slowFlushChecker := time.AfterFunc(TestSlowFlush, func() { - Printf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush) + stdoutPrintf("+++ %s ... still flushing after %v ...\n", log.NewColoredValue(t.Name(), log.Bold, log.FgRed), TestSlowFlush) }) if err := queue.GetManager().FlushAll(t.Context(), -1); err != nil { // if panic occurs, then the t.Context() is also cancelled ahead, so here it shows "context canceled" error. @@ -143,7 +143,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { runDuration := time.Since(runStart) flushDuration := time.Since(flushStart) if runDuration > TestSlowRun { - Printf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration) + stdoutPrintf("+++ %s is a slow test (run: %v, flush: %v)\n", log.NewColoredValue(t.Name(), log.Bold, log.FgYellow), runDuration, flushDuration) } WriterCloser.popT() } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index cc02ce99996..c718eb34e05 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -440,7 +440,7 @@ func EditAuthSourcePost(ctx *context.Context) { log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID) ctx.Flash.Success(ctx.Tr("admin.auths.update_success")) - ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(form.ID, 10)) + ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(source.ID, 10)) } // DeleteAuthSource response for deleting an auth source diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index ad2243be348..651618cfad4 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -14,7 +14,6 @@ import ( // AuthenticationForm form for authentication type AuthenticationForm struct { - ID int64 Type int `binding:"Range(2,7)"` Name string `binding:"Required;MaxSize(30)"` TwoFactorPolicy string diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index fd740ce8e58..ab4d120b9cb 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -4,8 +4,10 @@ package integration import ( + "fmt" "net/http" "os" + "strconv" "strings" "testing" @@ -14,7 +16,6 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -46,12 +47,29 @@ type ldapTestEnv struct { serverPort string } -func prepareLdapTestEnv(t *testing.T) *ldapTestEnv { - if os.Getenv("TEST_LDAP") != "1" { - t.Skip() - return nil - } +func TestAuthLDAP(t *testing.T) { + // To test it locally: + // $ docker run --rm gitea/test-openldap:latest -p 389:389 + // $ TEST_LDAP=1 TEST_LDAP_HOST=localhost make "test-sqlite#TestAuthLDAP" + defer tests.PrepareTestEnv(t)() + t.Run("PreventInvalidGroupTeamMap", testLDAPPreventInvalidGroupTeamMap) + t.Run("AuthChange", testLDAPAuthChange) + t.Run("EmailSignin", testLDAPEmailSignin) + hasRealServer, _ := strconv.ParseBool(os.Getenv("TEST_LDAP")) + if hasRealServer { + t.Run("UserSignin", testLDAPUserSignin) + t.Run("UserSyncWithAttributeUsername", testLDAPUserSyncWithAttributeUsername) + t.Run("UserSyncWithoutAttributeUsername", testLDAPUserSyncWithoutAttributeUsername) + t.Run("UserSyncSSHKeys", testLDAPUserSyncSSHKeys) + t.Run("UserSyncWithGroupFilter", testLDAPUserSyncWithGroupFilter) + + t.Run("GroupTeamSyncAddMember", testLDAPGroupTeamSyncAddMember) + t.Run("GroupTeamSyncRemoveMember", testLDAPGroupTeamSyncRemoveMember) + } +} + +func prepareLdapTestServerEnv() *ldapTestEnv { gitLDAPUsers := []ldapUser{ { UserName: "professor", @@ -117,23 +135,8 @@ func prepareLdapTestEnv(t *testing.T) *ldapTestEnv { } } -type ldapAuthOptions struct { - attributeUID optional.Option[string] // defaults to "uid" - attributeSSHPublicKey string - groupFilter string - groupTeamMap string - groupTeamMapRemoval string -} - -func (te *ldapTestEnv) buildAuthSourcePayload(opts ...ldapAuthOptions) map[string]string { - opt := util.OptionalArg(opts) - // Modify user filter to test group filter explicitly - userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))" - if opt.groupFilter != "" { - userFilter = "(&(objectClass=inetOrgPerson)(uid=%s))" - } - - return map[string]string{ +func (te *ldapTestEnv) buildAuthSourcePayload(m map[string]string) map[string]string { + ret := map[string]string{ "type": "2", "name": "ldap", "host": te.serverHost, @@ -141,107 +144,119 @@ func (te *ldapTestEnv) buildAuthSourcePayload(opts ...ldapAuthOptions) map[strin "bind_dn": "uid=gitea,ou=service,dc=planetexpress,dc=com", "bind_password": "password", "user_base": "ou=people,dc=planetexpress,dc=com", - "filter": userFilter, + "filter": "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))", "admin_filter": "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)", "restricted_filter": "(uid=leela)", - "attribute_username": util.Iif(opt.attributeUID.Has(), opt.attributeUID.Value(), "uid"), + "attribute_username": "uid", "attribute_name": "givenName", "attribute_surname": "sn", "attribute_mail": "mail", - "attribute_ssh_public_key": opt.attributeSSHPublicKey, + "attribute_ssh_public_key": "", "is_sync_enabled": "on", "is_active": "on", "groups_enabled": "on", "group_dn": "ou=people,dc=planetexpress,dc=com", "group_member_uid": "member", - "group_filter": opt.groupFilter, - "group_team_map": opt.groupTeamMap, - "group_team_map_removal": opt.groupTeamMapRemoval, + "group_filter": "", + "group_team_map": "", + "group_team_map_removal": "", "user_uid": "DN", } + for k, v := range m { + if _, ok := ret[k]; !ok { + panic("invalid key: " + k) + } + ret[k] = v + } + return ret } -func (te *ldapTestEnv) addAuthSource(t *testing.T, opts ...ldapAuthOptions) { +func (te *ldapTestEnv) setupAuthSource(t *testing.T, params map[string]string) { session := loginUser(t, "user1") - req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", te.buildAuthSourcePayload(opts...)) - session.MakeRequest(t, req, http.StatusSeeOther) + existing := &auth_model.Source{Name: params["name"]} + if ok, _ := db.GetEngine(t.Context()).Get(existing); ok { + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/auths/%d", existing.ID), params) + session.MakeRequest(t, req, http.StatusSeeOther) + } else { + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", params) + session.MakeRequest(t, req, http.StatusSeeOther) + } } -func TestLDAPUserSignin(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t) +func testLDAPUserSignin(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(nil)) - u := te.gitLDAPUsers[0] - - session := loginUserWithPassword(t, u.UserName, u.Password) - req := NewRequest(t, "GET", "/user/settings") - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - - assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) - assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) - assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text()) + t.Run("Success", func(t *testing.T) { + u := te.gitLDAPUsers[0] + session := loginUserWithPassword(t, u.UserName, u.Password) + req := NewRequest(t, "GET", "/user/settings") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) + assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) + assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text()) + }) + t.Run("Failed", func(t *testing.T) { + u := te.otherLDAPUsers[0] + testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) + }) } -func TestLDAPAuthChange(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t) +func testLDAPAuthChange(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(nil)) session := loginUser(t, "user1") req := NewRequest(t, "GET", "/-/admin/auths") resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - href, exists := doc.Find("table.table td a").Attr("href") - if !exists { - assert.True(t, exists, "No authentication source found") + respStr := resp.Body.String() + doc := NewHTMLParser(t, strings.NewReader(respStr)) + hrefAuthSource, exists := doc.Find("table.table td a").Attr("href") + if !assert.True(t, exists, "No authentication source found") { + t.Logf("response: %s", respStr) return } - req = NewRequest(t, "GET", href) + req = NewRequest(t, "GET", hrefAuthSource) resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) host, _ := doc.Find(`input[name="host"]`).Attr("value") assert.Equal(t, te.serverHost, host) - binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") - assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) + bindDN, _ := doc.Find(`input[name="bind_dn"]`).Attr("value") + assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", bindDN) - req = NewRequestWithValues(t, "POST", href, te.buildAuthSourcePayload(ldapAuthOptions{groupTeamMapRemoval: "off"})) + req = NewRequestWithValues(t, "POST", hrefAuthSource, te.buildAuthSourcePayload(map[string]string{"group_team_map_removal": "off"})) session.MakeRequest(t, req, http.StatusSeeOther) - req = NewRequest(t, "GET", href) + req = NewRequest(t, "GET", hrefAuthSource) resp = session.MakeRequest(t, req, http.StatusOK) doc = NewHTMLParser(t, resp.Body) host, _ = doc.Find(`input[name="host"]`).Attr("value") assert.Equal(t, te.serverHost, host) - binddn, _ = doc.Find(`input[name="bind_dn"]`).Attr("value") - assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn) + bindDN, _ = doc.Find(`input[name="bind_dn"]`).Attr("value") + assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", bindDN) } -func TestLDAPUserSync(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } +func testLDAPUserSyncWithAttributeUsername(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(map[string]string{"attribute_username": "uid"})) + + // reset user and email table + _, _ = db.GetEngine(t.Context()).Where("`name` != 'user1'").Delete(&user_model.User{}) + _ = db.TruncateBeans(t.Context(), &user_model.EmailAddress{}) + unittest.AssertCount(t, &user_model.User{}, 1) - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t) err := auth.SyncExternalUsers(t.Context(), true) - assert.NoError(t, err) + require.NoError(t, err) // Check if users exists for _, gitLDAPUser := range te.gitLDAPUsers { dbUser, err := user_model.GetUserByName(t.Context(), gitLDAPUser.UserName) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, gitLDAPUser.UserName, dbUser.Name) assert.Equal(t, gitLDAPUser.Email, dbUser.Email) assert.Equal(t, gitLDAPUser.IsAdmin, dbUser.IsAdmin) @@ -255,23 +270,21 @@ func TestLDAPUserSync(t *testing.T) { } } -func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } +func testLDAPUserSyncWithoutAttributeUsername(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + authParams := te.buildAuthSourcePayload(map[string]string{"attribute_username": ""}) + te.setupAuthSource(t, authParams) - defer tests.PrepareTestEnv(t)() - - session := loginUser(t, "user1") - payload := te.buildAuthSourcePayload() - payload["attribute_username"] = "" - req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload) - session.MakeRequest(t, req, http.StatusSeeOther) + // reset user and email table + _, _ = db.GetEngine(t.Context()).Where("`name` != 'user1'").Delete(&user_model.User{}) + _ = db.TruncateBeans(t.Context(), &user_model.EmailAddress{}) + unittest.AssertCount(t, &user_model.User{}, 1) + adminSession := loginUser(t, "user1") for _, u := range te.gitLDAPUsers { req := NewRequest(t, "GET", "/-/admin/users?q="+u.UserName) - resp := session.MakeRequest(t, req, http.StatusOK) + resp := adminSession.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -289,9 +302,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { require.NoError(t, auth.SyncExternalUsers(t.Context(), true)) - authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{ - Name: payload["name"], - }) + authSource := unittest.AssertExistsAndLoadBean(t, &auth_model.Source{Name: authParams["name"]}) unittest.AssertCount(t, &user_model.User{ LoginType: auth_model.LDAP, LoginSource: authSource.ID, @@ -305,14 +316,13 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { } } -func TestLDAPUserSyncWithGroupFilter(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t, ldapAuthOptions{groupFilter: "(cn=git)"}) +func testLDAPUserSyncWithGroupFilter(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(map[string]string{ + "filter": "(&(objectClass=inetOrgPerson)(uid=%s))", + "group_filter": "(cn=git)", + })) // Assert a user not a member of the LDAP group "cn=git" cannot login // This test may look like TestLDAPUserSigninFailed but it is not. @@ -365,64 +375,43 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) { } } -func TestLDAPUserSigninFailed(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t) - - u := te.otherLDAPUsers[0] - testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect")) -} - -func TestLDAPUserSSHKeySync(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - - defer tests.PrepareTestEnv(t)() - te.addAuthSource(t, ldapAuthOptions{attributeSSHPublicKey: "sshPublicKey"}) +func testLDAPUserSyncSSHKeys(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(map[string]string{"attribute_ssh_public_key": "sshPublicKey"})) require.NoError(t, auth.SyncExternalUsers(t.Context(), true)) // Check if users has SSH keys synced + count := 0 for _, u := range te.gitLDAPUsers { if len(u.SSHKeys) == 0 { continue } - session := loginUserWithPassword(t, u.UserName, u.Password) + count++ + session := loginUserWithPassword(t, u.UserName, u.Password) req := NewRequest(t, "GET", "/user/settings/keys") resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) divs := htmlDoc.doc.Find("#keys-ssh .flex-item .flex-item-body:not(:last-child)") - syncedKeys := make([]string, divs.Length()) for i := 0; i < divs.Length(); i++ { syncedKeys[i] = strings.TrimSpace(divs.Eq(i).Text()) } - assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName) } + assert.NotZero(t, count) } -func TestLDAPGroupTeamSyncAddMember(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - +func testLDAPGroupTeamSyncAddMember(t *testing.T) { defer tests.PrepareTestEnv(t)() - te.addAuthSource(t, ldapAuthOptions{ - groupTeamMap: `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`, - groupTeamMapRemoval: "on", - }) + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(map[string]string{ + "group_team_map": `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`, + "group_team_map_removal": "on", + })) org, err := organization.GetOrgByName(t.Context(), "org26") assert.NoError(t, err) team, err := organization.GetTeam(t.Context(), org.ID, "team11") @@ -461,16 +450,14 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) { } } -func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } +func testLDAPGroupTeamSyncRemoveMember(t *testing.T) { defer tests.PrepareTestEnv(t)() - te.addAuthSource(t, ldapAuthOptions{ - groupTeamMap: `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`, - groupTeamMapRemoval: "on", - }) + te := prepareLdapTestServerEnv() + te.setupAuthSource(t, te.buildAuthSourcePayload(map[string]string{ + "group_team_map": `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`, + "group_team_map_removal": "on", + })) + org, err := organization.GetOrgByName(t.Context(), "org26") assert.NoError(t, err) team, err := organization.GetTeam(t.Context(), org.ID, "team11") @@ -499,20 +486,18 @@ func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) { assert.False(t, isMember, "User membership should have been removed from team") } -func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) { - te := prepareLdapTestEnv(t) - if te == nil { - return - } - defer tests.PrepareTestEnv(t)() +func testLDAPPreventInvalidGroupTeamMap(t *testing.T) { + defer tests.PrintCurrentTest(t)() + te := prepareLdapTestServerEnv() session := loginUser(t, "user1") - payload := te.buildAuthSourcePayload(ldapAuthOptions{groupTeamMap: `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, groupTeamMapRemoval: "off"}) + payload := te.buildAuthSourcePayload(map[string]string{"group_team_map": `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "group_team_map_removal": "off"}) req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload) session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok } -func TestLDAPEmailSignin(t *testing.T) { +func testLDAPEmailSignin(t *testing.T) { + defer tests.PrintCurrentTest(t)() te := ldapTestEnv{ gitLDAPUsers: []ldapUser{ { @@ -549,7 +534,7 @@ func TestLDAPEmailSignin(t *testing.T) { return result })() defer tests.PrepareTestEnv(t)() - te.addAuthSource(t) + te.setupAuthSource(t, te.buildAuthSourcePayload(nil)) u := te.gitLDAPUsers[0] diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index b1ae4f40f2f..c2045453e65 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -4,7 +4,7 @@ package integration import ( - "bytes" + "io" "testing" "github.com/PuerkitoBio/goquery" @@ -17,7 +17,7 @@ type HTMLDoc struct { } // NewHTMLParser parse html file -func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc { +func NewHTMLParser(t testing.TB, body io.Reader) *HTMLDoc { t.Helper() doc, err := goquery.NewDocumentFromReader(body) assert.NoError(t, err) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index d60f66e785d..33be7d89cb6 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -404,7 +404,7 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) { if err != nil { return // probably a non-HTML response } - errMsg := htmlDoc.Find(".ui.negative.message").Text() + errMsg := htmlDoc.Find(".ui.negative.message:not(.tw-hidden)").Text() if len(errMsg) > 0 { t.Log("A flash error message was found:", errMsg) } From f247d7d4e5a38148f02b9a414a18da6fe79d338c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 18 Apr 2026 22:21:21 +0200 Subject: [PATCH 047/126] Enhance GetActionWorkflow to support fallback references (#37189) If a workflow is not in default branch the hooks could not be detected Fixes #37169 Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Giteabot --- services/actions/notifier.go | 7 +- services/convert/action_test.go | 109 ++++++++++++++++++++++++++++++++ services/convert/convert.go | 34 ++++++++-- services/webhook/notifier.go | 7 +- 4 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 services/convert/action_test.go diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 19d6be94207..5f7ee6fcea0 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" @@ -20,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" notify_service "code.gitea.io/gitea/services/notify" @@ -805,7 +807,10 @@ func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *rep } defer gitRepo.Close() - convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) + if err != nil && errors.Is(err, util.ErrNotExist) { + convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + } if err != nil { log.Error("GetActionWorkflow: %v", err) return diff --git a/services/convert/action_test.go b/services/convert/action_test.go new file mode 100644 index 00000000000..7080fc2f146 --- /dev/null +++ b/services/convert/action_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "fmt" + "strings" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// buildWorkflowTestRepo creates a temporary git repository for testing GetActionWorkflow. +// The default branch "main" has no workflow files; "feature" and "release-v1" each add their own workflow file. +func buildWorkflowTestRepo(t *testing.T) string { + t.Helper() + ctx := t.Context() + tmpDir := t.TempDir() + + _, _, err := gitcmd.NewCommand("init").WithDir(tmpDir).RunStdString(ctx) + require.NoError(t, err) + + readme := "readme" + featureWF := "on: [push]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo test\n" + releaseWF := "on: [push]\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - run: echo release\n" + + // Build a git fast-import stream: + // :4 = initial commit on main (README.md only) + // :5 = feature branch commit (adds feature workflow) + // :6 = release commit from :4 (adds release workflow, tagged release-v1, not on main) + var sb strings.Builder + fmt.Fprintf(&sb, "blob\nmark :1\ndata %d\n%s\n", len(readme), readme) + fmt.Fprintf(&sb, "blob\nmark :2\ndata %d\n%s\n", len(featureWF), featureWF) + fmt.Fprintf(&sb, "blob\nmark :3\ndata %d\n%s\n", len(releaseWF), releaseWF) + fmt.Fprintf(&sb, "commit refs/heads/main\nmark :4\nauthor Test 1000000000 +0000\ncommitter Test 1000000000 +0000\ndata 14\ninitial commit\nM 100644 :1 README.md\n\n") + fmt.Fprintf(&sb, "commit refs/heads/feature\nmark :5\nauthor Test 1000000001 +0000\ncommitter Test 1000000001 +0000\ndata 12\nadd workflow\nfrom :4\nM 100644 :2 .gitea/workflows/my-workflow.yml\n\n") + fmt.Fprintf(&sb, "reset refs/pull/42/merge\nfrom :5\n\n") + fmt.Fprintf(&sb, "commit refs/heads/main\nmark :6\nauthor Test 1000000002 +0000\ncommitter Test 1000000002 +0000\ndata 16\nrelease workflow\nfrom :4\nM 100644 :3 .gitea/workflows/my-workflow.yml\n\n") + fmt.Fprintf(&sb, "reset refs/tags/release-v1\nfrom :6\n\n") + fmt.Fprintf(&sb, "reset refs/heads/main\nfrom :4\n\n") + fmt.Fprintf(&sb, "done\n") + + _, _, err = gitcmd.NewCommand("fast-import").WithDir(tmpDir).WithStdinBytes([]byte(sb.String())).RunStdString(ctx) + require.NoError(t, err) + + return tmpDir +} + +func TestGetActionWorkflow_FallbackRef(t *testing.T) { + ctx := t.Context() + + repoDir := buildWorkflowTestRepo(t) + + gitRepo, err := git.OpenRepository(ctx, repoDir) + require.NoError(t, err) + defer gitRepo.Close() + + repo := &repo_model.Repository{ + DefaultBranch: "main", + OwnerName: "test-owner", + Name: "test-repo", + Units: []*repo_model.RepoUnit{ + { + Type: unit.TypeActions, + Config: &repo_model.ActionsConfig{}, + }, + }, + } + + t.Run("returns error when workflow only on non-default branch", func(t *testing.T) { + _, err := GetActionWorkflow(ctx, gitRepo, repo, "my-workflow.yml") + require.Error(t, err) + assert.ErrorIs(t, err, util.ErrNotExist) + }) + + t.Run("returns workflow when found via ref", func(t *testing.T) { + wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/heads/feature")) + require.NoError(t, err) + assert.Equal(t, "my-workflow.yml", wf.ID) + }) + + t.Run("returns workflow when found via pull ref", func(t *testing.T) { + wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/pull/42/merge")) + require.NoError(t, err) + assert.Equal(t, "my-workflow.yml", wf.ID) + assert.Contains(t, wf.HTMLURL, "/src/commit/") + }) + + t.Run("returns workflow with tag link when found via tag ref", func(t *testing.T) { + wf, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "my-workflow.yml", git.RefName("refs/tags/release-v1")) + require.NoError(t, err) + assert.Equal(t, "my-workflow.yml", wf.ID) + assert.Contains(t, wf.HTMLURL, "/src/tag/release-v1/") + }) + + t.Run("returns error when workflow missing from ref", func(t *testing.T) { + _, err := GetActionWorkflowByRef(ctx, gitRepo, repo, "nonexistent.yml", git.RefName("refs/heads/feature")) + require.Error(t, err) + assert.ErrorIs(t, err, util.ErrNotExist) + }) +} diff --git a/services/convert/convert.go b/services/convert/convert.go index 71d2ecb3333..f7a207622be 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -387,12 +387,15 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task }, nil } -func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branchName, folder string, entry *git.TreeEntry) *api.ActionWorkflow { +func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow { cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name())) - workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(branchName), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) + workflowRepoURL := fmt.Sprintf("%s/src/commit/%s/%s/%s", repo.HTMLURL(ctx), commit.ID.String(), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) + if refWebLinkPath := refName.RefWebLinkPath(); refWebLinkPath != "" { + workflowRepoURL = fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(ctx), refWebLinkPath, util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) + } badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch)) // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow @@ -457,7 +460,7 @@ func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *rep workflows := make([]*api.ActionWorkflow, len(entries)) for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, repo.DefaultBranch, folder, entry) + workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), folder, entry) } return workflows, nil @@ -469,14 +472,35 @@ func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_ return nil, err } - folder, entries, err := actions.ListWorkflows(defaultBranchCommit) + return getActionWorkflowFromCommit(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), workflowID) +} + +func GetActionWorkflowByRef(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string, ref git.RefName) (*api.ActionWorkflow, error) { + if ref == "" { + return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) + } + + refCommitID, err := gitrepo.GetRefCommitID(ref.String()) + if err != nil { + return nil, err + } + refCommit, err := gitrepo.GetCommit(refCommitID) + if err != nil { + return nil, err + } + + return getActionWorkflowFromCommit(ctx, repo, refCommit, ref, workflowID) +} + +func getActionWorkflowFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, workflowID string) (*api.ActionWorkflow, error) { + folder, entries, err := actions.ListWorkflows(commit) if err != nil { return nil, err } for _, entry := range entries { if entry.Name() == workflowID { - return getActionWorkflowEntry(ctx, repo, defaultBranchCommit, repo.DefaultBranch, folder, entry), nil + return getActionWorkflowEntry(ctx, repo, commit, refName, folder, entry), nil } } diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 0a5661009e5..2b301d4d583 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -5,6 +5,7 @@ package webhook import ( "context" + "errors" actions_model "code.gitea.io/gitea/models/actions" git_model "code.gitea.io/gitea/models/git" @@ -22,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" notify_service "code.gitea.io/gitea/services/notify" @@ -1032,7 +1034,10 @@ func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_ } defer gitRepo.Close() - convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + convertedWorkflow, err := convert.GetActionWorkflowByRef(ctx, gitRepo, repo, run.WorkflowID, git.RefName(run.Ref)) + if err != nil && errors.Is(err, util.ErrNotExist) { + convertedWorkflow, err = convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + } if err != nil { log.Error("GetActionWorkflow: %v", err) return From ea6280da75d0f4281a5e2ca61a806f6d97752b3e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 18 Apr 2026 13:39:25 -0700 Subject: [PATCH 048/126] release notes for 1.26.0 (#37282) Frontend from #37266 --- CHANGELOG.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbdf6d9f78..05c56dc84fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). -## [1.26.0-rc0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0-rc0) - 2026-04-07 +## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17 * BREAKING * Correct swagger annotations for enums, status codes, and notification state (#37030) @@ -30,7 +30,8 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Add summary to action runs view (#36883) * Add user badges (#36752) * Add configurable permissions for Actions automatic tokens (#36173) - * Add per-runner “Disable/Pause” (#36776) + * Add per-runner "Disable/Pause" (#36776) + * Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786) * PERFORMANCE * WorkflowDispatch API optionally return runid (#36706) * Add render cache for SVG icons (#36863) @@ -41,6 +42,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Refactor cat-file batch operations and support `--batch-command` approach (#35775) * Use merge tree to detect conflicts when possible (#36400) * ENHANCEMENTS + * Implement logout redirection for reverse proxy auth setups (#36085) (#37171) * Adds option to force update new branch in contents routes (#35592) * Add viewer controller for mermaid (zoom, drag) (#36557) * Add code editor setting dropdowns (#36534) @@ -49,7 +51,6 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Allow configuring default PR base branch (fixes #36412) (#36425) * Add support for RPM Errata (updateinfo.xml) (#37125) * Require additional user confirmation for making repo private (#36959) - * Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786) * Add `actions.WORKFLOW_DIRS` setting (#36619) * Avoid opening new tab when downloading actions logs (#36740) * Implements OIDC RP-Initiated Logout (#36724) @@ -67,7 +68,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Refactor storage content-type handling of ServeDirectURL (#36804) * Use "Enable Gravatar" but not "Disable" (#36771) * Use case-insensitive matching for Git error "Not a valid object name" (#36728) - * Add “Copy Source” to markup comment menu (#36726) + * Add "Copy Source" to markup comment menu (#36726) * Change image transparency grid to CSS (#36711) * Add "Run" prefix for unnamed action steps (#36624) * Persist actions log time display settings in `localStorage` (#36623) @@ -139,6 +140,20 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Expose content_version for optimistic locking on issue and PR edits (#37035) * Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) * BUGFIXES + * Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273) + * Fix CODEOWNERS absolute path matching. (#37244) (#37264) + * Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261) + * Fix user ssh key exporting and tests (#37256) (#37258) + * Fix team member avatar size and add tooltip (#37253) + * Fix commit title rendering in action run and blame (#37243) (#37251) + * Fix corrupted JSON caused by goccy library (#37214) (#37220) + * Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216) + * Fix incorrect concurrency check (#37205) (#37215) + * Fix handle missing base branch in PR commits API (#37193) (#37203) + * Fix encoding for Matrix Webhooks (#37190) (#37201) + * Fix handle fork-only commits in compare API (#37185) (#37199) + * Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37178) + * Report structurally invalid workflows to users (#37116) (#37164) * Fix API not persisting pull request unit config when has_pull_requests is not set (#36718) * Rename CSS variables and improve colorblind themes (#36353) * Hide `add-matcher` and `remove-matcher` from actions job logs (#36520) @@ -232,6 +247,9 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Only turn links to current instance into hash links (#36237) * Fix typos in code comments: doesnt, dont, wont (#36890) * REFACTOR + * Clean up and improve non-gitea js error filter (#37148) (#37155) + * Always show owner/repo name in compare page dropdowns (#37172) (#37200) + * Remove dead CSS rules (#37173) (#37177) * Replace Monaco with CodeMirror (#36764) * Replace CSRF cookie with `CrossOriginProtection` (#36183) * Replace index with id in actions routes (#36842) @@ -289,6 +307,9 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Add e2e reaction test, improve accessibility, enable parallel testing (#37081) * Increase e2e test timeouts on CI to fix flaky tests (#37053) * BUILD + * Upgrade go-git to v5.18.0 (#37269) + * Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158) + * Bump min go version to 1.26.2 (#37139) (#37143) * Convert locale files from ini to json format (#35489) * Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180) * Port away from `flake-utils` (#35675) From 0bc2a2836f52d06eb7aa4d730a4e88605c464df9 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 19 Apr 2026 01:01:55 +0000 Subject: [PATCH 049/126] [skip ci] Updated translations via Crowdin --- options/locale/locale_ga-IE.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json index 85a69220727..cee97810b65 100644 --- a/options/locale/locale_ga-IE.json +++ b/options/locale/locale_ga-IE.json @@ -269,7 +269,7 @@ "install.lfs_path": "Cosán Fréamh Git LFS", "install.lfs_path_helper": "Stórálfar comhaid a rianóidh Git LFS san eolaire seo. Fág folamh le díchumasú.", "install.run_user": "Rith mar Ainm Úsáideora", - "install.run_user_helper": "An ainm úsáideora an chórais oibriúcháin a ritheann Gitea mar. Tabhair faoi deara go gcaithfidh rochtain a bheith ag an úsáideoir seo ar fhréamhchosán an taisclainne.", + "install.run_user_helper": "Ainm úsáideora an chórais oibriúcháin a ritheann Gitea mar, ní mór rochtain scríofa a bheith aige ar na cosáin sonraí. Braitear an luach seo go huathoibríoch agus ní féidir é a athrú anseo. Chun úsáideoir difriúil a úsáid, atosú Gitea faoin gcuntas sin.", "install.domain": "Fearann ​​Freastalaí", "install.domain_helper": "Seoladh fearainn nó óstach don fhreastalaí.", "install.ssh_port": "Port Freastalaí SSH", @@ -316,7 +316,6 @@ "install.invalid_db_table": "Tá an tábla bunachar sonraí \"%s\" neamhbhailí: %v", "install.invalid_repo_path": "Tá cosán fréimhe an stór neamhbhailí:%v", "install.invalid_app_data_path": "Tá cosán sonraí an aip neamhbhailí:%v", - "install.run_user_not_match": "Ní hé an t-ainm úsáideora 'rith mar' an t-ainm úsáideora reatha: %s -> %s", "install.internal_token_failed": "Theip ar chomhartha inmheánach a ghiniúint:%v", "install.secret_key_failed": "Theip ar an eochair rúnda a ghiniúint:%v", "install.save_config_failed": "Theip ar chumraíocht a shábháil:%v", @@ -2827,7 +2826,7 @@ "org.teams.manage_team_member_prompt": "Déantar baill a bhainistiú trí fhoirne. Cuir úsáideoirí le foireann chun cuireadh a thabhairt dóibh chuig an eagraíocht seo.", "org.teams.update_settings": "Nuashonrú Socruithe", "org.teams.delete_team": "Scrios Foireann", - "org.teams.add_team_member": "Cuir Comhalta Foirne leis", + "org.teams.add_team_member": "Cuir ball foirne leis", "org.teams.invite_team_member": "Tabhair cuireadh chuig %s", "org.teams.invite_team_member.list": "Cuirí ar Feitheamh", "org.teams.delete_team_title": "Scrios Foireann", @@ -3181,6 +3180,8 @@ "admin.auths.oauth2_required_claim_name_helper": "Socraigh an t-ainm seo chun logáil isteach ón bhfoinse seo a shrianadh d'úsáideoirí a bhfuil éileamh acu leis an ainm seo", "admin.auths.oauth2_required_claim_value": "Luach Éilimh Riachtanach", "admin.auths.oauth2_required_claim_value_helper": "Socraigh an luach seo chun logáil isteach ón bhfoinse seo a shrianadh chuig úsáideoirí a bhfuil éileamh acu leis an ainm agus an luach seo", + "admin.auths.open_id_connect_external_id_claim": "Ainm Éilimh Aitheantais Sheachtraigh (Roghnach)", + "admin.auths.open_id_connect_external_id_claim_helper": "Ainm an éilimh le húsáid mar aitheantas seachtrach an úsáideora. Is é \"sub\" an rogha réamhshocraithe. I gcás Azure AD / Entra ID, socraigh é seo go \"oid\" chun leanúnachas a choinneáil agus aistriú á dhéanamh ón soláthraí Azure AD V2. Tabhair faoi deara: éilíonn an t-éileamh \"oid\" go gcuirfí an raon feidhme \"próifíl\" san áireamh sa réimse Scóipe thuas.", "admin.auths.oauth2_group_claim_name": "Ainm éileamh ag soláthar ainmneacha grúpa don fhoinse seo (Roghnach)", "admin.auths.oauth2_full_name_claim_name": "Ainm Iomlán Éilimh Ainm. (Roghnach — má shocraítear é, déanfar ainm iomlán an úsáideora a shioncrónú leis an éileamh seo i gcónaí)", "admin.auths.oauth2_ssh_public_key_claim_name": "Ainm Éilimh Eochrach Phoiblí SSH", From 16bdae53c812247a122976b62c3e6106f19be91b Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sun, 19 Apr 2026 09:37:50 +0200 Subject: [PATCH 050/126] Workflow Artifact Info Hover (#37100) Add expiry metadata to action artifacts in the run view and show it on hover. --------- Signed-off-by: Nicolas Co-authored-by: silverwind Co-authored-by: Claude (Opus 4.6) Co-authored-by: wxiaoguang --- models/actions/artifact.go | 3 +- options/locale/locale_en-US.json | 1 + routers/web/devtest/mock_actions.go | 31 ++++++++++------- routers/web/repo/actions/view.go | 14 ++++---- templates/devtest/relative-time.tmpl | 1 + templates/repo/actions/view_component.tmpl | 1 + web_src/css/modules/divider.css | 8 +++++ .../js/components/ActionRunArtifacts.test.ts | 34 +++++++++++++++++++ web_src/js/components/ActionRunArtifacts.ts | 25 ++++++++++++++ web_src/js/components/RepoActionView.vue | 24 ++++++++----- web_src/js/features/repo-actions.ts | 1 + web_src/js/modules/gitea-actions.ts | 5 ++- web_src/js/modules/tippy.ts | 7 +++- web_src/js/utils.test.ts | 13 ++++++- web_src/js/utils.ts | 11 ++++++ web_src/js/utils/testhelper.ts | 8 +++++ 16 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 web_src/js/components/ActionRunArtifacts.test.ts create mode 100644 web_src/js/components/ActionRunArtifacts.ts diff --git a/models/actions/artifact.go b/models/actions/artifact.go index d61afb2aed4..ffadc79661a 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -183,6 +183,7 @@ type ActionArtifactMeta struct { ArtifactName string FileSize int64 Status ArtifactStatus + ExpiredUnix timeutil.TimeStamp } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run @@ -191,7 +192,7 @@ func ListUploadedArtifactsMeta(ctx context.Context, repoID, runID int64) ([]*Act return arts, db.GetEngine(ctx).Table("action_artifact"). Where("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). - Select("artifact_name, sum(file_size) as file_size, max(status) as status"). + Select("artifact_name, sum(file_size) as file_size, max(status) as status, max(expired_unix) as expired_unix"). Find(&arts) } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 343a672dc01..8efafd5c4b7 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -122,6 +122,7 @@ "unpin": "Unpin", "artifacts": "Artifacts", "expired": "Expired", + "artifact_expires_at": "Expires at %s", "confirm_delete_artifact": "Are you sure you want to delete the artifact '%s'?", "archived": "Archived", "concept_system_global": "Global", diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 0fb2a358243..fe12dc3079c 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -67,6 +67,9 @@ func MockActionsView(ctx *context.Context) { func MockActionsRunsJobs(ctx *context.Context) { runID := ctx.PathParamInt64("run") + alignTime := func(v, unit int64) int64 { + return (v + unit) / unit * unit + } resp := &actions.ViewResponse{} resp.State.Run.RepoID = 12345 resp.State.Run.TitleHTML = `mock run title link` @@ -96,24 +99,28 @@ func MockActionsRunsJobs(ctx *context.Context) { }, } resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-a", - Size: 100 * 1024, - Status: "expired", + Name: "artifact-a", + Size: 100 * 1024, + Status: "expired", + ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-b", - Size: 1024 * 1024, - Status: "completed", + Name: "artifact-b", + Size: 1024 * 1024, + Status: "completed", + ExpiresUnix: alignTime(time.Now().Add(24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", - Size: 100 * 1024, - Status: "expired", + Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", + Size: 100 * 1024, + Status: "expired", + ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600), }) resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ - Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", - Size: 1024 * 1024, - Status: "completed", + Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong", + Size: 1024 * 1024, + Status: "completed", + ExpiresUnix: 0, }) resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{ diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index f92df685fda..fb4dfa9603d 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -248,9 +248,10 @@ type ViewRequest struct { } type ArtifactsViewItem struct { - Name string `json:"name"` - Size int64 `json:"size"` - Status string `json:"status"` + Name string `json:"name"` + Size int64 `json:"size"` + Status string `json:"status"` + ExpiresUnix int64 `json:"expiresUnix"` } type ViewResponse struct { @@ -344,9 +345,10 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifact } for _, art := range artifacts { artifactsViewItems = append(artifactsViewItems, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, - Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), + Name: art.ArtifactName, + Size: art.FileSize, + Status: util.Iif(art.Status == actions_model.ArtifactStatusExpired, "expired", "completed"), + ExpiresUnix: int64(art.ExpiredUnix), }) } return artifactsViewItems, nil diff --git a/templates/devtest/relative-time.tmpl b/templates/devtest/relative-time.tmpl index 041ce49f09f..f4c664e26f3 100644 --- a/templates/devtest/relative-time.tmpl +++ b/templates/devtest/relative-time.tmpl @@ -38,6 +38,7 @@
numeric:
weekday:
with time:
+
minutes:

Threshold

diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 405e9cfb4b1..2cc70e499ad 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -28,6 +28,7 @@ data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" data-locale-artifact-expired="{{ctx.Locale.Tr "expired"}}" + data-locale-artifact-expires-at="{{ctx.Locale.Tr "artifact_expires_at"}}" data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css index a60b7d52cbe..32d03885d35 100644 --- a/web_src/css/modules/divider.css +++ b/web_src/css/modules/divider.css @@ -36,3 +36,11 @@ h4.divider { .divider.divider-text::after { margin-left: .75em; } + +.inline-divider { + display: inline-block; + border-left: 1px solid var(--color-secondary); + overflow: hidden; + width: 1px; + margin: 0 var(--gap-inline); +} diff --git a/web_src/js/components/ActionRunArtifacts.test.ts b/web_src/js/components/ActionRunArtifacts.test.ts new file mode 100644 index 00000000000..358510e8cd7 --- /dev/null +++ b/web_src/js/components/ActionRunArtifacts.test.ts @@ -0,0 +1,34 @@ +import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts'; +import {normalizeTestHtml} from '../utils/testhelper.ts'; + +describe('buildArtifactTooltipHtml', () => { + test('active artifact', () => { + const result = buildArtifactTooltipHtml({ + name: 'artifact.zip', + size: 1024 * 1024, + status: 'completed', + expiresUnix: Date.UTC(2026, 2, 20, 12, 0, 0) / 1000, + }, 'Expires at %s (extra)'); + + expect(normalizeTestHtml(result)).toBe(normalizeTestHtml(` +Expires at + + 2026-03-20T12:00:00.000Z + + (extra) + , + 1.0 MiB + +`)); + }); + + test('no expiry', () => { + const result = buildArtifactTooltipHtml({ + name: 'artifact.zip', + size: 512, + status: 'completed', + expiresUnix: 0, + }, 'Expires at %s'); + expect(normalizeTestHtml(result)).toBe(`512 B`); + }); +}); diff --git a/web_src/js/components/ActionRunArtifacts.ts b/web_src/js/components/ActionRunArtifacts.ts new file mode 100644 index 00000000000..ca8f5991620 --- /dev/null +++ b/web_src/js/components/ActionRunArtifacts.ts @@ -0,0 +1,25 @@ +import {html} from '../utils/html.ts'; +import {formatBytes} from '../utils.ts'; +import type {ActionsArtifact} from '../modules/gitea-actions.ts'; + +export function buildArtifactTooltipHtml(artifact: ActionsArtifact, expiresAtLocale: string): string { + const sizeText = formatBytes(artifact.size); + if (artifact.expiresUnix <= 0) { + return html`${sizeText}`; // use the same layout as below + } + + // split so the element can be interleaved, e.g. "Expires at %s" -> ["Expires at ", ""] + const [prefix, suffix = ''] = expiresAtLocale.split('%s'); + const datetime = new Date(artifact.expiresUnix * 1000).toISOString(); + return html` + + ${prefix} + + ${datetime} + + ${suffix} + , + ${sizeText} + + `; +} diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index ee8b4880029..dbb5426ca78 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -5,7 +5,8 @@ import {toRefs} from 'vue'; import {POST, DELETE} from '../modules/fetch.ts'; import ActionRunSummaryView from './ActionRunSummaryView.vue'; import ActionRunJobView from './ActionRunJobView.vue'; -import {createActionRunViewStore} from "./ActionRunView.ts"; +import {createActionRunViewStore} from './ActionRunView.ts'; +import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts'; defineOptions({ name: 'RepoActionView', @@ -20,7 +21,7 @@ const props = defineProps<{ const locale = props.locale; const store = createActionRunViewStore(props.actionsUrl, props.runId); -const {currentRun: run , runArtifacts: artifacts} = toRefs(store.viewData); +const {currentRun: run, runArtifacts: artifacts} = toRefs(store.viewData); function cancelRun() { POST(`${run.value.link}/cancel`); @@ -120,18 +121,24 @@ async function deleteArtifact(name: string) {
  • - + {{ artifact.name }} - {{ locale.artifactExpired }} + {{ locale.artifactExpired }}
@@ -251,6 +258,7 @@ async function deleteArtifact(name: string) { .left-list-header { font-size: 13px; + font-weight: var(--font-weight-semibold); color: var(--color-text-light-2); } diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts index a3984e40cda..d8b13804ba5 100644 --- a/web_src/js/features/repo-actions.ts +++ b/web_src/js/features/repo-actions.ts @@ -33,6 +33,7 @@ export function initRepositoryActionView() { artifactsTitle: el.getAttribute('data-locale-artifacts-title'), areYouSure: el.getAttribute('data-locale-are-you-sure'), artifactExpired: el.getAttribute('data-locale-artifact-expired'), + artifactExpiresAt: el.getAttribute('data-locale-artifact-expires-at'), confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), diff --git a/web_src/js/modules/gitea-actions.ts b/web_src/js/modules/gitea-actions.ts index 96b31e4c949..bf7550a0329 100644 --- a/web_src/js/modules/gitea-actions.ts +++ b/web_src/js/modules/gitea-actions.ts @@ -1,5 +1,6 @@ // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" export type ActionsRunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; +export type ActionsArtifactStatus = 'expired' | 'completed'; export type ActionsRun = { repoId: number, @@ -49,5 +50,7 @@ export type ActionsJob = { export type ActionsArtifact = { name: string; - status: string; + size: number; + status: ActionsArtifactStatus; + expiresUnix: number; }; diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index 22eb875c976..c2ca9ab51b0 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -2,6 +2,7 @@ import tippy, {followCursor} from 'tippy.js'; import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; import type {Content, Instance, Placement, Props} from 'tippy.js'; import {html} from '../utils/html.ts'; +import {stripTags} from '../utils.ts'; type TippyOpts = { role?: string, @@ -85,6 +86,7 @@ function attachTooltip(target: Element, content: Content | null = null): Instanc role: 'tooltip', theme: 'tooltip', hideOnClick, + allowHTML: target.getAttribute('data-tooltip-render') === 'html', placement: target.getAttribute('data-tooltip-placement') as Placement || 'top-start', followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false, ...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}), @@ -127,7 +129,10 @@ function attachLazyTooltip(el: HTMLElement): void { if (!el.hasAttribute('aria-label')) { const content = el.getAttribute('data-tooltip-content'); if (content) { - el.setAttribute('aria-label', content); + const isHtml = el.getAttribute('data-tooltip-render') === 'html'; + let ariaLabelValue = content; + if (isHtml) ariaLabelValue = stripTags(content).replace(/\s+/g, ' ').trim(); + el.setAttribute('aria-label', ariaLabelValue); } } } diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index dfc498693e7..507334c0b41 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -1,5 +1,5 @@ import { - dirname, basename, extname, isObject, stripTags, parseIssueHref, + dirname, basename, extname, formatBytes, isObject, stripTags, parseIssueHref, translateMonth, translateDay, blobToDataURI, toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseRepoOwnerPathInfo, urlQueryEscape, @@ -122,6 +122,17 @@ test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a')); }); +test('formatBytes', () => { + expect(formatBytes(-1)).toBe('0 B'); + expect(formatBytes(0)).toBe('0 B'); + expect(formatBytes(512)).toBe('512 B'); + expect(formatBytes(1024)).toBe('1.0 KiB'); + expect(formatBytes(1536)).toBe('1.5 KiB'); + expect(formatBytes(10 * 1024)).toBe('10 KiB'); + expect(formatBytes(1024 * 1024)).toBe('1.0 MiB'); + expect(formatBytes(1024 * 1024 * 1024)).toBe('1.0 GiB'); +}); + test('file detection', () => { for (const name of ['a.avif', 'a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) { expect(isImageFile({name})).toBeTruthy(); diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index e812a7b978b..d802dd8e084 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -203,6 +203,17 @@ export function isVideoFile({name, type}: {name?: string, type?: string}): boole return Boolean(/\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/')); } +const byteUnits = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + +export function formatBytes(num: number, precision = 2): string { + if (!Number.isFinite(num) || num < 0) return `0 ${byteUnits[0]}`; + if (num < 1024) return `${num} ${byteUnits[0]}`; + const exp = Math.min(Math.floor(Math.log2(num) / 10), byteUnits.length - 1); + const value = num / (1024 ** exp); + const digits = Math.max(0, precision - 1 - Math.floor(Math.log10(value))); + return `${value.toFixed(digits)} ${byteUnits[exp]}`; +} + export function toggleFullScreen(fullScreenEl: HTMLElement, isFullScreen: boolean, sourceParentSelector?: string): void { // hide other elements const headerEl = document.querySelector('#navbar')!; diff --git a/web_src/js/utils/testhelper.ts b/web_src/js/utils/testhelper.ts index 9541ecdefb2..8da77d8134e 100644 --- a/web_src/js/utils/testhelper.ts +++ b/web_src/js/utils/testhelper.ts @@ -20,3 +20,11 @@ export function dedent(str: string) { return str.replace(new RegExp(`^[ \\t]{${minIndent}}`, 'gm'), '').trim(); } + +export function normalizeTestHtml(s: string) { + const lines = s.replace(/>\s+\n<').trim().split('\n'); + for (let i = 0; i < lines.length; i++) { + lines[i] = lines[i].trim(); + } + return lines.join('\n'); +} From c98134033a77df04f480062988ebc74af7b363df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:20:49 +0200 Subject: [PATCH 051/126] Update Nix flake (#37284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated changes by the [update-flake-lock](https://github.com/DeterminateSystems/update-flake-lock) GitHub Action. ``` Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/4c1018d' (2026-04-09) → 'github:nixos/nixpkgs/4bd9165' (2026-04-14) ``` ### Running GitHub Actions on this PR GitHub Actions will not run workflows on pull requests which are opened by a GitHub Action. **To run GitHub Actions workflows on this PR, close and re-open this pull request.** Co-authored-by: github-actions[bot] Co-authored-by: Nicolas --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 8ec14d28526..2130399c1dd 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1775710090, - "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4c1018dae018162ec878d42fec712642d214fdfa", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { From 30be22f30f0dde56c073a4a2a6722f86de79f5b7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:57:48 +0800 Subject: [PATCH 052/126] Refactor frontend `tw-justify-between` layouts to `flex-left-right` (#37291) This PR standardizes left/right two-child frontend layouts on `flex-left-right` and removes ad-hoc `tw-justify-between` combinations. The goal is consistent wrapping + spacing behavior under narrow widths with less utility-class churn. Also: remove useless "flex-center-wrap", slightly improve some templates (no visual change, tested) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <2114189+wxiaoguang@users.noreply.github.com> Co-authored-by: wxiaoguang --- templates/org/team/sidebar.tmpl | 2 +- templates/org/team/teams.tmpl | 4 ++-- templates/projects/list.tmpl | 2 +- templates/repo/actions/list.tmpl | 2 +- templates/repo/actions/workflow_dispatch.tmpl | 2 +- templates/repo/blame.tmpl | 2 +- templates/repo/branch/list.tmpl | 2 +- templates/repo/commits_table.tmpl | 2 +- templates/repo/diff/box.tmpl | 2 +- templates/repo/editor/edit.tmpl | 2 +- templates/repo/editor/patch.tmpl | 2 +- templates/repo/issue/filter_actions.tmpl | 3 +-- templates/repo/issue/sidebar/due_date.tmpl | 2 +- templates/repo/issue/sidebar/issue_dependencies.tmpl | 10 +++++----- templates/repo/issue/view_content/conversation.tmpl | 2 +- templates/repo/pulls/status.tmpl | 2 +- templates/repo/release/list.tmpl | 2 +- templates/repo/settings/githook_edit.tmpl | 2 +- templates/repo/settings/webhook/history.tmpl | 2 +- templates/repo/view_file.tmpl | 4 ++-- templates/repo/wiki/new.tmpl | 2 +- templates/repo/wiki/pages.tmpl | 2 +- templates/repo/wiki/revision.tmpl | 2 +- templates/shared/searchbottom.tmpl | 2 +- templates/user/notification/notification_div.tmpl | 2 +- .../notification/notification_subscriptions.tmpl | 6 ++---- web_src/css/base.css | 12 ++---------- web_src/js/components/RepoCodeFrequency.vue | 2 +- web_src/js/components/RepoContributors.vue | 2 +- web_src/js/components/RepoRecentCommits.vue | 2 +- web_src/js/features/repo-issue-content.ts | 2 +- 31 files changed, 39 insertions(+), 50 deletions(-) diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 645c94d4162..1487c280dee 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -1,7 +1,7 @@

{{.Team.Name}} -
+
{{if .Team.IsMember ctx $.SignedUser.ID}} diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 489590cd4df..8bdefa5d43e 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -11,7 +11,7 @@ {{end}} {{end}}
-

+

{{template "repo/file_info" .}}
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 5ae33935758..40aefe5b113 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -70,7 +70,7 @@

{{end}} -

+

{{ctx.Locale.Tr "repo.branches"}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 8f6e6e01692..56a4867ff4b 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,4 +1,4 @@ -

+

{{if or .PageIsCommits (gt .CommitCount 0)}} {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index ffba4cf1521..ccb9e80f28b 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -211,7 +211,7 @@ {{if .Diff.IsIncomplete}}
-

+

{{ctx.Locale.Tr "repo.diff.too_many_files"}} {{ctx.Locale.Tr "repo.diff.show_more"}}

diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index bb258d333d1..74d6dcb07f8 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -18,7 +18,7 @@ {{if not .NotEditableReason}}
-
+
-
+
diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 8e2410393d8..4c9366a645b 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -29,7 +29,7 @@
{{end}} {{$previousExclusiveScope = $exclusiveScope}} -
+
{{if SliceUtils.Contains $.SelLabelIDs .ID}}{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}{{end}} {{ctx.RenderUtils.RenderLabel .}} {{template "repo/issue/labels/label_archived" .}}
@@ -125,4 +125,3 @@
{{end}}
- diff --git a/templates/repo/issue/sidebar/due_date.tmpl b/templates/repo/issue/sidebar/due_date.tmpl index 0e3d57eb728..b312e8a889e 100644 --- a/templates/repo/issue/sidebar/due_date.tmpl +++ b/templates/repo/issue/sidebar/due_date.tmpl @@ -2,7 +2,7 @@ {{ctx.Locale.Tr "repo.issues.due_date"}}
{{if .Issue.DeadlineUnix}} -
+
{{svg "octicon-calendar"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}}
diff --git a/templates/repo/issue/sidebar/issue_dependencies.tmpl b/templates/repo/issue/sidebar/issue_dependencies.tmpl index 0eb7f26c706..f1555dfa397 100644 --- a/templates/repo/issue/sidebar/issue_dependencies.tmpl +++ b/templates/repo/issue/sidebar/issue_dependencies.tmpl @@ -20,7 +20,7 @@
{{range .BlockingDependencies}} -
+
{{end}} {{if .BlockingDependenciesNotPermitted}} -
+
{{ctx.Locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}
{{end}} @@ -54,7 +54,7 @@
{{range .BlockedByDependencies}} -
+
#{{.Issue.Index}} {{.Issue.Title | ctx.RenderUtils.RenderEmoji}} @@ -76,7 +76,7 @@ {{end}} {{if $.CanCreateIssueDependencies}} {{range .BlockedByDependenciesNotPermitted}} -
+
{{svg "octicon-lock" 16}} @@ -100,7 +100,7 @@
{{end}} {{else if .BlockedByDependenciesNotPermitted}} -
+
{{ctx.Locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}
{{end}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index 333d120fde7..df383f46b87 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -11,7 +11,7 @@ The variables in "ctx.Data" are different in each case, making this template fra {{$hasReview := and $comment.Review}} {{$isReviewPending := and $hasReview (eq $comment.Review.Type 0)}}
-
+
{{if and $statusCheckData $statusCheckData.RequireApprovalRunCount}} -
+
{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals" $statusCheckData.RequireApprovalRunCount}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 90ad32bcf05..1009d224372 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -30,7 +30,7 @@ {{end}}
-
+

{{if $.PageIsSingleTag}}{{$release.Title}}{{else}}{{$release.Title}}{{end}} {{template "repo/commit_statuses" dict "Status" $info.CommitStatus "Statuses" $info.CommitStatuses "AdditionalClasses" "tw-flex"}} diff --git a/templates/repo/settings/githook_edit.tmpl b/templates/repo/settings/githook_edit.tmpl index 8c07ed0fb45..b5374032983 100644 --- a/templates/repo/settings/githook_edit.tmpl +++ b/templates/repo/settings/githook_edit.tmpl @@ -1,7 +1,7 @@ {{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings edit githook")}}
-

+

{{.Hook.Name}}
{{template "repo/editor/options" dict "CodeEditorConfig" $.CodeEditorConfig}} diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index 72e204ebd37..797ce05a85b 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -17,7 +17,7 @@
{{range .History}}
-
+
{{if .IsSucceed}} {{svg "octicon-check"}} diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 59243bf6840..9f936afb8e0 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -13,7 +13,7 @@ {{end}} {{if not .ReadmeInList}} -
+
{{template "repo/latest_commit" .}} {{if .LatestCommit}} {{if .LatestCommit.Committer}} @@ -25,7 +25,7 @@
{{end}} -

+

{{if .ReadmeInList}} {{svg "octicon-book" 16 "tw-mr-2"}} diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index 82c4e2f8ac0..c09b8759ddc 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -3,7 +3,7 @@ {{template "repo/header" .}}
{{template "base/alert" .}} -
+
{{ctx.Locale.Tr "repo.wiki.new_page"}} {{if .PageIsWikiEdit}} {{ctx.Locale.Tr "repo.wiki.new_page_button"}} diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl index 120c1cda323..efa97103cc4 100644 --- a/templates/repo/wiki/pages.tmpl +++ b/templates/repo/wiki/pages.tmpl @@ -2,7 +2,7 @@
{{template "repo/header" .}}
-

+

{{ctx.Locale.Tr "repo.wiki.pages"}} {{if and .CanWriteWiki (not .Repository.IsMirror)}} diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index 108e3789378..c59047a6404 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -3,7 +3,7 @@ {{template "repo/header" .}} {{$title := .title}}
-
+
{{svg "octicon-home"}}
diff --git a/templates/shared/searchbottom.tmpl b/templates/shared/searchbottom.tmpl index 4e0bd9570ba..b6a5f1b955c 100644 --- a/templates/shared/searchbottom.tmpl +++ b/templates/shared/searchbottom.tmpl @@ -1,5 +1,5 @@ {{if or .result.Language (not .result.UpdatedUnix.IsZero)}} -
+
{{if .result.Language}} {{.result.Language}} diff --git a/templates/user/notification/notification_div.tmpl b/templates/user/notification/notification_div.tmpl index 8a28f8dc620..a724ab725dd 100644 --- a/templates/user/notification/notification_div.tmpl +++ b/templates/user/notification/notification_div.tmpl @@ -3,7 +3,7 @@ {{$statusUnread := 1}}{{$statusRead := 2}}{{$statusPinned := 3}} {{$notificationUnreadCount := call .PageGlobalData.GetNotificationUnreadCount}} {{$pageTypeIsRead := eq $.PageType "read"}} -
+
{{if eq .Status 1}} -
+
-
- -
{{if not .Issues}} diff --git a/web_src/css/base.css b/web_src/css/base.css index 49ce6565c78..08033e4a6f0 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -873,7 +873,7 @@ table th[data-sortt-desc] .svg { gap: var(--gap-block); } -/* TODO: use this to replace all existing "flex + justify-between" (there are quite a lot) */ +/* this is useful to make a left-right (e.g.: title .... operations) layout with default gap, and it wrap for small widths */ .flex-left-right { display: flex; flex-wrap: wrap; @@ -883,15 +883,6 @@ table th[data-sortt-desc] .svg { min-width: 0; } -/* TODO: use this to replace all existing "flex + wrap" and (there are quite a lot of) */ -.flex-center-wrap { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--gap-block); - min-width: 0; -} - .ui.list.flex-items-block > .item, .ui.vertical.menu.flex-items-block > .item, .ui.form .field > label.flex-text-block, /* override fomantic "block" style */ @@ -903,6 +894,7 @@ table th[data-sortt-desc] .svg { min-width: 0; } +.flex-left-right > .ui.button, .flex-text-block > .ui.button, .flex-text-inline > .ui.button { margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */ diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index 6cba3c51091..97ce07cc350 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -144,7 +144,7 @@ const options: ChartOptions<'line'> = {