mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-08 23:02:10 +00:00
Compare commits
26 Commits
renovate/n
...
v1.26.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b9d1e31aa | ||
|
|
eb43da41f5 | ||
|
|
1412009d0a | ||
|
|
26a618ac1a | ||
|
|
145898b358 | ||
|
|
b191cf7e77 | ||
|
|
4adee80f58 | ||
|
|
4de12baf9b | ||
|
|
5d852d2d0a | ||
|
|
2aca966c5f | ||
|
|
3b253e06a3 | ||
|
|
df0ad4e8c1 | ||
|
|
68f5e40e46 | ||
|
|
d1be5c3612 | ||
|
|
8687faaf3a | ||
|
|
789a3d3a4d | ||
|
|
b37e098ff0 | ||
|
|
f9b808a8d2 | ||
|
|
0112ec9b34 | ||
|
|
7a7376dfc8 | ||
|
|
fc5e0ec877 | ||
|
|
d0a39bc3a4 | ||
|
|
4eca71d6d4 | ||
|
|
a2283a0c03 | ||
|
|
3e6b9e5312 | ||
|
|
1ad9e996be |
400
CHANGELOG.md
400
CHANGELOG.md
@@ -4,6 +4,406 @@ 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](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)
|
||||
* 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)
|
||||
* 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)
|
||||
* 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
|
||||
* 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)
|
||||
* 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)
|
||||
* 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
|
||||
* 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)
|
||||
* 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
|
||||
* 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)
|
||||
* 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
|
||||
* 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)
|
||||
* 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -926,6 +926,7 @@ export default defineConfig([
|
||||
{
|
||||
...playwright.configs['flat/recommended'],
|
||||
files: ['tests/e2e/**/*.test.ts'],
|
||||
languageOptions: {globals: {...globals.nodeBuiltin, ...globals.browser}},
|
||||
rules: {
|
||||
...playwright.configs['flat/recommended'].rules,
|
||||
'playwright/expect-expect': [0],
|
||||
|
||||
8
go.mod
8
go.mod
@@ -1,6 +1,6 @@
|
||||
module code.gitea.io/gitea
|
||||
|
||||
go 1.26.1
|
||||
go 1.26.2
|
||||
|
||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||
// But some CAs use negative serial number, just relax the check. related:
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
code.gitea.io/sdk/gitea v0.24.1
|
||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||
connectrpc.com/connect v1.19.1
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
|
||||
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a
|
||||
gitea.com/go-chi/cache v0.2.1
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
|
||||
@@ -52,12 +52,11 @@ require (
|
||||
github.com/go-co-op/gocron/v2 v2.19.1
|
||||
github.com/go-enry/go-enry/v2 v2.9.5
|
||||
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
|
||||
github.com/go-webauthn/webauthn v0.16.1
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
@@ -196,6 +195,7 @@ require (
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -18,8 +18,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
|
||||
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
||||
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
|
||||
gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ=
|
||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
|
||||
@@ -304,8 +304,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=
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@@ -64,7 +65,12 @@ func (key *PublicKey) AfterLoad() {
|
||||
|
||||
// OmitEmail returns content of public key without email address.
|
||||
func (key *PublicKey) OmitEmail() string {
|
||||
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
||||
fields := strings.Split(key.Content, " ") // format: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... comment
|
||||
if len(fields) < 2 {
|
||||
setting.PanicInDevOrTesting("invalid public key %d content: %s", key.ID, key.Content)
|
||||
return "" // not a valid public key, it shouldn't really happen, the value is managed internally
|
||||
}
|
||||
return strings.Join(fields[:2], " ")
|
||||
}
|
||||
|
||||
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
||||
|
||||
@@ -877,7 +877,12 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
|
||||
|
||||
warnings := make([]string, 0)
|
||||
|
||||
expr := fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))
|
||||
// Strip leading "!" for negative rules, then strip leading "/" since
|
||||
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
|
||||
// and the regex is already anchored with ^...$, so the "/" is redundant.
|
||||
pattern := strings.TrimPrefix(tokens[0], "!")
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
expr := fmt.Sprintf("^%s$", pattern)
|
||||
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
||||
|
||||
@@ -17,16 +17,43 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_LoadAttributes(t *testing.T) {
|
||||
func TestPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("LoadAttributes", testPullRequestLoadAttributes)
|
||||
t.Run("LoadIssue", testPullRequestLoadIssue)
|
||||
t.Run("LoadBaseRepo", testPullRequestLoadBaseRepo)
|
||||
t.Run("LoadHeadRepo", testPullRequestLoadHeadRepo)
|
||||
t.Run("PullRequestsNewest", testPullRequestsNewest)
|
||||
t.Run("PullRequestsOldest", testPullRequestsOldest)
|
||||
t.Run("GetUnmergedPullRequest", testGetUnmergedPullRequest)
|
||||
t.Run("HasUnmergedPullRequestsByHeadInfo", testHasUnmergedPullRequestsByHeadInfo)
|
||||
t.Run("GetUnmergedPullRequestsByHeadInfo", testGetUnmergedPullRequestsByHeadInfo)
|
||||
t.Run("GetUnmergedPullRequestsByBaseInfo", testGetUnmergedPullRequestsByBaseInfo)
|
||||
t.Run("GetPullRequestByIndex", testGetPullRequestByIndex)
|
||||
t.Run("GetPullRequestByID", testGetPullRequestByID)
|
||||
t.Run("GetPullRequestByIssueID", testGetPullRequestByIssueID)
|
||||
t.Run("PullRequest_UpdateCols", testPullRequestUpdateCols)
|
||||
t.Run("PullRequest_IsWorkInProgress", testPullRequestIsWorkInProgress)
|
||||
t.Run("PullRequest_GetWorkInProgressPrefixWorkInProgress", testPullRequestGetWorkInProgressPrefixWorkInProgress)
|
||||
t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects)
|
||||
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
|
||||
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
|
||||
t.Run("GetApprovers", testGetApprovers)
|
||||
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
|
||||
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
|
||||
t.Run("PullRequestsClosedRecentSortType", testPullRequestsClosedRecentSortType)
|
||||
t.Run("LoadRequestedReviewers", testLoadRequestedReviewers)
|
||||
}
|
||||
|
||||
func testPullRequestLoadAttributes(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadAttributes(t.Context()))
|
||||
assert.NotNil(t, pr.Merger)
|
||||
assert.Equal(t, pr.MergerID, pr.Merger.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadIssue(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadIssue(t.Context()))
|
||||
assert.NotNil(t, pr.Issue)
|
||||
@@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) {
|
||||
assert.Equal(t, int64(2), pr.Issue.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadBaseRepo(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
assert.NotNil(t, pr.BaseRepo)
|
||||
@@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
||||
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadHeadRepo(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
assert.NotNil(t, pr.HeadRepo)
|
||||
@@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
||||
|
||||
// TODO TestNewPullRequest
|
||||
|
||||
func TestPullRequestsNewest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestsNewest(t *testing.T) {
|
||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
@@ -77,7 +101,7 @@ func TestPullRequestsNewest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
func testPullRequestsClosedRecentSortType(t *testing.T) {
|
||||
// Issue ID | Closed At. | Updated At
|
||||
// 2 | 1707270001 | 1707270001
|
||||
// 3 | 1707271000 | 1707279999
|
||||
@@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
{"recentclose", []int64{11, 3, 2}},
|
||||
}
|
||||
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
|
||||
require.NoError(t, err)
|
||||
_, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
|
||||
@@ -118,9 +141,7 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testLoadRequestedReviewers(t *testing.T) {
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.NoError(t, pull.LoadIssue(t.Context()))
|
||||
issue := pull.Issue
|
||||
@@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.Empty(t, pull.RequestedReviewers)
|
||||
}
|
||||
|
||||
func TestPullRequestsOldest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestsOldest(t *testing.T) {
|
||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
@@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequest(t *testing.T) {
|
||||
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.ID)
|
||||
@@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
@@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
@@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
@@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
assert.Equal(t, "master", pr.BaseBranch)
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIndex(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByIndex(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.BaseRepoID)
|
||||
@@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByID(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.ID)
|
||||
@@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIssueID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByIssueID(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.IssueID)
|
||||
@@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestUpdateCols(t *testing.T) {
|
||||
pr := &issues_model.PullRequest{
|
||||
ID: 1,
|
||||
BaseBranch: "baseBranch",
|
||||
@@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
|
||||
// TODO TestAddTestPullRequestTask
|
||||
|
||||
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestIsWorkInProgress(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
pr.LoadIssue(t.Context())
|
||||
|
||||
@@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
assert.True(t, pr.IsWorkInProgress(t.Context()))
|
||||
}
|
||||
|
||||
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
pr.LoadIssue(t.Context())
|
||||
|
||||
@@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context()))
|
||||
}
|
||||
|
||||
func TestDeleteOrphanedObjects(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testDeleteOrphanedObjects(t *testing.T) {
|
||||
countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) {
|
||||
assert.Equal(t, countBefore, countAfter)
|
||||
}
|
||||
|
||||
func TestParseCodeOwnersLine(t *testing.T) {
|
||||
func testParseCodeOwnersLine(t *testing.T) {
|
||||
type CodeOwnerTest struct {
|
||||
Line string
|
||||
Tokens []string
|
||||
@@ -331,6 +336,8 @@ func TestParseCodeOwnersLine(t *testing.T) {
|
||||
{Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}},
|
||||
{Line: `\#path @org3`, Tokens: []string{`#path`, "@org3"}},
|
||||
{Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@org3"}},
|
||||
{Line: `/docs/.*\\.md @user1`, Tokens: []string{`/docs/.*\.md`, "@user1"}},
|
||||
{Line: `!/assets/.*\\.(bin|exe|msi) @user1`, Tokens: []string{`!/assets/.*\.(bin|exe|msi)`, "@user1"}},
|
||||
}
|
||||
|
||||
for _, g := range given {
|
||||
@@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApprovers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
|
||||
type testCase struct {
|
||||
content string
|
||||
file string
|
||||
expected bool
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
// Absolute path pattern should match (leading "/" stripped)
|
||||
{content: "/README.md @user5\n", file: "README.md", expected: true},
|
||||
// Absolute path pattern in subdirectory
|
||||
{content: "/docs/.* @user5\n", file: "docs/foo.md", expected: true},
|
||||
// Absolute path should not match nested paths it shouldn't
|
||||
{content: "/docs/.* @user5\n", file: "other/docs/foo.md", expected: false},
|
||||
// Relative path still works
|
||||
{content: "README.md @user5\n", file: "README.md", expected: true},
|
||||
// Negated absolute path pattern
|
||||
{content: "!/.* @user5\n", file: "README.md", expected: false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), c.content)
|
||||
require.NotEmpty(t, rules)
|
||||
rule := rules[0]
|
||||
regexpMatched, _ := rule.Rule.MatchString(c.file)
|
||||
ruleMatched := regexpMatched == !rule.Negative
|
||||
assert.Equal(t, c.expected, ruleMatched, "pattern %q against file %q", c.content, c.file)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetApprovers(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
|
||||
// Official reviews are already deduplicated. Allow unofficial reviews
|
||||
// to assert that there are no duplicated approvers.
|
||||
@@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) {
|
||||
assert.Equal(t, expected, approvers)
|
||||
}
|
||||
|
||||
func TestGetPullRequestByMergedCommit(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByMergedCommit(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pr.ID)
|
||||
@@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) {
|
||||
assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
|
||||
}
|
||||
|
||||
func TestMigrate_InsertPullRequests(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testMigrateInsertPullRequests(t *testing.T) {
|
||||
reponame := "repo1"
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -52,13 +52,13 @@ func InsertProperty(ctx context.Context, refType PropertyType, refID int64, name
|
||||
// GetProperties gets all properties
|
||||
func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) {
|
||||
pps := make([]*PackageProperty, 0, 10)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Find(&pps)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).OrderBy("id").Find(&pps)
|
||||
}
|
||||
|
||||
// GetPropertiesByName gets all properties with a specific name
|
||||
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
|
||||
pps := make([]*PackageProperty, 0, 10)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps)
|
||||
return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps)
|
||||
}
|
||||
|
||||
// UpdateProperty updates a property
|
||||
|
||||
@@ -50,8 +50,8 @@ type RepoFileOptions struct {
|
||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
||||
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||
|
||||
CurrentRefPath string // eg: "branch/main"
|
||||
CurrentTreePath string // eg: "path/to/file" in the repo
|
||||
CurrentRefPath string // eg: "branch/main", it is a sub URL path escaped by callers, TODO: rename to CurrentRefSubURL
|
||||
CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping
|
||||
}
|
||||
|
||||
func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext {
|
||||
@@ -70,6 +70,10 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
|
||||
"repo": helper.opts.DeprecatedRepoName,
|
||||
})
|
||||
}
|
||||
// External render's iframe needs this to generate correct links
|
||||
// TODO: maybe need to make it access "CurrentRefPath" directly (but impossible at the moment due to cycle-import)
|
||||
// CurrentRefPath is already path-escaped by callers
|
||||
rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefPath
|
||||
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
|
||||
return rctx
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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: <oid> SP <type> SP <size> LF
|
||||
// then leaving the rest of the stream "<contents> LF" to be read
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
var _ Interface = jsonGoccy{}
|
||||
|
||||
type jsonGoccy struct{}
|
||||
|
||||
func (jsonGoccy) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (jsonGoccy) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
func (jsonGoccy) NewEncoder(writer io.Writer) Encoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
func (jsonGoccy) NewDecoder(reader io.Reader) Decoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return json.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
@@ -6,12 +6,12 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
)
|
||||
|
||||
func getDefaultJSONHandler() Interface {
|
||||
return jsonGoccy{}
|
||||
return jsonV1{}
|
||||
}
|
||||
|
||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||
|
||||
28
modules/markup/external/external.go
vendored
28
modules/markup/external/external.go
vendored
@@ -21,7 +21,33 @@ import (
|
||||
|
||||
// RegisterRenderers registers all supported third part renderers according settings
|
||||
func RegisterRenderers() {
|
||||
markup.RegisterRenderer(&openAPIRenderer{})
|
||||
markup.RegisterRenderer(&frontendRenderer{
|
||||
name: "openapi-swagger",
|
||||
patterns: []string{
|
||||
"openapi.yaml",
|
||||
"openapi.yml",
|
||||
"openapi.json",
|
||||
"swagger.yaml",
|
||||
"swagger.yml",
|
||||
"swagger.json",
|
||||
},
|
||||
})
|
||||
|
||||
markup.RegisterRenderer(&frontendRenderer{
|
||||
name: "viewer-3d",
|
||||
patterns: []string{
|
||||
// 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: "*.amf, *.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.
|
||||
"*.3dm", "*.3ds", "*.3mf", "*.amf", "*.bim", "*.brep",
|
||||
"*.dae", "*.fbx", "*.fcstd", "*.glb", "*.gltf",
|
||||
"*.ifc", "*.igs", "*.iges", "*.stp", "*.step",
|
||||
"*.stl", "*.obj", "*.off", "*.ply", "*.wrl",
|
||||
},
|
||||
})
|
||||
|
||||
for _, renderer := range setting.ExternalMarkupRenderers {
|
||||
markup.RegisterRenderer(&Renderer{renderer})
|
||||
}
|
||||
|
||||
95
modules/markup/external/frontend.go
vendored
Normal file
95
modules/markup/external/frontend.go
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package external
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type frontendRenderer struct {
|
||||
name string
|
||||
patterns []string
|
||||
}
|
||||
|
||||
var (
|
||||
_ markup.PostProcessRenderer = (*frontendRenderer)(nil)
|
||||
_ markup.ExternalRenderer = (*frontendRenderer)(nil)
|
||||
)
|
||||
|
||||
func (p *frontendRenderer) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *frontendRenderer) NeedPostProcess() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *frontendRenderer) FileNamePatterns() []string {
|
||||
// TODO: the file extensions are ambiguous, even if the file name matches, it doesn't mean that the file is a 3D model
|
||||
// There are some approaches to make it more accurate, but they are all complicated:
|
||||
// A. Make backend know everything (detect a file is a 3D model or not)
|
||||
// B. Let frontend renders to try render one by one
|
||||
//
|
||||
// If there would be more frontend renders in the future, we need to implement the "frontend" approach:
|
||||
// 1. Make backend or parent window collect the supported extensions of frontend renders (done: backend external render framework)
|
||||
// 2. If the current file matches any extension, start the general iframe embedded render (done: this renderer)
|
||||
// 3. The iframe window calls the frontend renders one by one (done: frontend external render)
|
||||
// 4. Report the render result to parent by postMessage (TODO: when needed)
|
||||
return p.patterns
|
||||
}
|
||||
|
||||
func (p *frontendRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *frontendRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
|
||||
ret.SanitizerDisabled = true
|
||||
ret.DisplayInIframe = true
|
||||
ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.RenderOptions.StandalonePageOptions == nil {
|
||||
opts := p.GetExternalRendererOptions()
|
||||
return markup.RenderIFrame(ctx, &opts, output)
|
||||
}
|
||||
|
||||
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentEncoding, contentString := "text", util.UnsafeBytesToString(content)
|
||||
if !utf8.Valid(content) {
|
||||
contentEncoding = "base64"
|
||||
contentString = base64.StdEncoding.EncodeToString(content)
|
||||
}
|
||||
|
||||
_, err = htmlutil.HTMLPrintf(output,
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- external-render-helper will be injected here by the markup render -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="frontend-render-viewer" data-frontend-renders="%s" data-file-tree-path="%s"></div>
|
||||
<textarea id="frontend-render-data" data-content-encoding="%s" hidden>%s</textarea>
|
||||
<script nonce type="module" src="%s"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
p.name, ctx.RenderOptions.RelativePath,
|
||||
contentEncoding, contentString,
|
||||
public.AssetURI("js/external-render-frontend.js"))
|
||||
return err
|
||||
}
|
||||
84
modules/markup/external/openapi.go
vendored
84
modules/markup/external/openapi.go
vendored
@@ -1,84 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package external
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type openAPIRenderer struct{}
|
||||
|
||||
var (
|
||||
_ markup.PostProcessRenderer = (*openAPIRenderer)(nil)
|
||||
_ markup.ExternalRenderer = (*openAPIRenderer)(nil)
|
||||
)
|
||||
|
||||
func (p *openAPIRenderer) Name() string {
|
||||
return "openapi"
|
||||
}
|
||||
|
||||
func (p *openAPIRenderer) NeedPostProcess() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *openAPIRenderer) FileNamePatterns() []string {
|
||||
return []string{
|
||||
"openapi.yaml",
|
||||
"openapi.yml",
|
||||
"openapi.json",
|
||||
"swagger.yaml",
|
||||
"swagger.yml",
|
||||
"swagger.json",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *openAPIRenderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *openAPIRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
|
||||
ret.SanitizerDisabled = true
|
||||
ret.DisplayInIframe = true
|
||||
ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads"
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
|
||||
if ctx.RenderOptions.StandalonePageOptions == nil {
|
||||
opts := p.GetExternalRendererOptions()
|
||||
return markup.RenderIFrame(ctx, &opts, output)
|
||||
}
|
||||
|
||||
content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// HINT: SWAGGER-OPENAPI-VIEWER: another place "templates/swagger/openapi-viewer.tmpl"
|
||||
_, err = io.WriteString(output, fmt.Sprintf(
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="%s">
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
|
||||
<script type="module" src="%s"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
public.AssetURI("css/swagger.css"),
|
||||
html.EscapeString(ctx.RenderOptions.RelativePath),
|
||||
html.EscapeString(util.UnsafeBytesToString(content)),
|
||||
public.AssetURI("js/swagger.js"),
|
||||
))
|
||||
return err
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package markup
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
@@ -43,7 +44,8 @@ type WebThemeInterface interface {
|
||||
}
|
||||
|
||||
type StandalonePageOptions struct {
|
||||
CurrentWebTheme WebThemeInterface
|
||||
CurrentWebTheme WebThemeInterface
|
||||
RenderQueryString string
|
||||
}
|
||||
|
||||
type RenderOptions struct {
|
||||
@@ -206,17 +208,23 @@ func RenderString(ctx *RenderContext, content string) (string, error) {
|
||||
}
|
||||
|
||||
func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.Writer) error {
|
||||
ownerName, repoName := ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"]
|
||||
refSubURL := ctx.RenderOptions.Metas["RefTypeNameSubURL"]
|
||||
if ownerName == "" || repoName == "" || refSubURL == "" {
|
||||
setting.PanicInDevOrTesting("RenderIFrame requires user, repo and RefTypeNameSubURL metas")
|
||||
return errors.New("RenderIFrame requires user, repo and RefTypeNameSubURL metas")
|
||||
}
|
||||
src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL,
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]),
|
||||
url.PathEscape(ownerName),
|
||||
url.PathEscape(repoName),
|
||||
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
|
||||
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
|
||||
)
|
||||
var extraAttrs template.HTML
|
||||
if opts.ContentSandbox != "" {
|
||||
extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, opts.ContentSandbox)
|
||||
}
|
||||
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" class="external-render-iframe"%s></iframe>`, src, extraAttrs)
|
||||
_, err := htmlutil.HTMLPrintf(output, `<iframe data-src="%s" data-global-init="initExternalRenderIframe" class="external-render-iframe"%s></iframe>`, src, extraAttrs)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -228,7 +236,7 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
|
||||
func GetExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok {
|
||||
return externalRender.GetExternalRendererOptions(), true
|
||||
}
|
||||
@@ -237,7 +245,7 @@ func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions,
|
||||
|
||||
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var extraHeadHTML template.HTML
|
||||
if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
|
||||
if extOpts, ok := GetExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
|
||||
if ctx.RenderOptions.StandalonePageOptions == nil {
|
||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||
@@ -248,7 +256,12 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
|
||||
extraLinkHref := ctx.RenderOptions.StandalonePageOptions.CurrentWebTheme.PublicAssetURI()
|
||||
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
|
||||
// DO NOT use "type=module", the script must run as early as possible, to set up the environment in the iframe
|
||||
extraHeadHTML = htmlutil.HTMLFormat(`<script crossorigin src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraLinkHref)
|
||||
extraHeadHTML = htmlutil.HTMLFormat(
|
||||
`<script nonce crossorigin src="%s" id="gitea-external-render-helper" data-render-query-string="%s"></script>`+
|
||||
`<link rel="stylesheet" href="%s">`,
|
||||
extraScriptSrc, ctx.RenderOptions.StandalonePageOptions.RenderQueryString,
|
||||
extraLinkHref,
|
||||
)
|
||||
}
|
||||
|
||||
ctx.usedByRender = true
|
||||
|
||||
@@ -24,8 +24,8 @@ func TestRenderIFrame(t *testing.T) {
|
||||
|
||||
// the value is read from config RENDER_CONTENT_SANDBOX, empty means "disabled"
|
||||
ret := render(ctx, ExternalRendererOptions{ContentSandbox: ""})
|
||||
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" class="external-render-iframe"></iframe>`, ret)
|
||||
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe"></iframe>`, ret)
|
||||
|
||||
ret = render(ctx, ExternalRendererOptions{ContentSandbox: "allow"})
|
||||
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" class="external-render-iframe" sandbox="allow"></iframe>`, ret)
|
||||
assert.Equal(t, `<iframe data-src="/test-owner/test-repo/render/src/branch/master/tree-path" data-global-init="initExternalRenderIframe" class="external-render-iframe" sandbox="allow"></iframe>`, ret)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ type Metadata struct {
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
LicenseURL string `json:"license_url,omitempty"`
|
||||
Author Person `json:"author"`
|
||||
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
||||
}
|
||||
@@ -67,7 +68,8 @@ type SoftwareSourceCode struct {
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
CodeRepository string `json:"codeRepository,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author Person `json:"author"`
|
||||
LicenseURL string `json:"licenseURL,omitempty"`
|
||||
Author *Person `json:"author,omitempty"`
|
||||
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
|
||||
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
|
||||
}
|
||||
@@ -181,26 +183,31 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
|
||||
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Metadata.Description = ssc.Description
|
||||
p.Metadata.Keywords = ssc.Keywords
|
||||
p.Metadata.License = ssc.License
|
||||
author := Person{
|
||||
Name: ssc.Author.Name,
|
||||
GivenName: ssc.Author.GivenName,
|
||||
MiddleName: ssc.Author.MiddleName,
|
||||
FamilyName: ssc.Author.FamilyName,
|
||||
p.Metadata.LicenseURL = ssc.LicenseURL
|
||||
if ssc.Author != nil {
|
||||
author := Person{
|
||||
Name: ssc.Author.Name,
|
||||
GivenName: ssc.Author.GivenName,
|
||||
MiddleName: ssc.Author.MiddleName,
|
||||
FamilyName: ssc.Author.FamilyName,
|
||||
}
|
||||
// If Name is not provided, generate it from individual name components
|
||||
if author.Name == "" {
|
||||
author.Name = author.String()
|
||||
}
|
||||
p.Metadata.Author = author
|
||||
}
|
||||
// If Name is not provided, generate it from individual name components
|
||||
if author.Name == "" {
|
||||
author.Name = author.String()
|
||||
}
|
||||
p.Metadata.Author = author
|
||||
|
||||
p.Metadata.RepositoryURL = ssc.CodeRepository
|
||||
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
||||
p.Metadata.RepositoryURL = ""
|
||||
}
|
||||
if !validation.IsValidURL(p.Metadata.LicenseURL) {
|
||||
p.Metadata.LicenseURL = ""
|
||||
}
|
||||
|
||||
p.RepositoryURLs = ssc.RepositoryURLs
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package swift
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -18,36 +19,24 @@ const (
|
||||
packageVersion = "1.0.1"
|
||||
packageDescription = "Package Description"
|
||||
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
||||
packageLicenseURL = "https://opensource.org/license/mit"
|
||||
packageAuthor = "KN4CK3R"
|
||||
packageLicense = "MIT"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
createArchive := func(files map[string][]byte) *bytes.Reader {
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
for filename, content := range files {
|
||||
w, _ := zw.Create(filename)
|
||||
w.Write(content)
|
||||
}
|
||||
zw.Close()
|
||||
return bytes.NewReader(buf.Bytes())
|
||||
}
|
||||
|
||||
t.Run("MissingManifestFile", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{"dummy.txt": {}})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
data := test.WriteZipArchive(map[string]string{"dummy.txt": ""})
|
||||
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrMissingManifestFile)
|
||||
})
|
||||
|
||||
t.Run("ManifestFileTooLarge", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": make([]byte, maxManifestFileSize+1),
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": strings.Repeat("a", maxManifestFileSize+1),
|
||||
})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
|
||||
})
|
||||
@@ -56,12 +45,12 @@ func TestParsePackage(t *testing.T) {
|
||||
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte(content1),
|
||||
"Package@swift-5.5.swift": []byte(content2),
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": content1,
|
||||
"Package@swift-5.5.swift": content2,
|
||||
})
|
||||
|
||||
p, err := ParsePackage(data, data.Size(), nil)
|
||||
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("WithMetadata", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||
})
|
||||
|
||||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
@@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.Equal(t, packageDescription, p.Metadata.Description)
|
||||
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
|
||||
assert.Equal(t, packageLicense, p.Metadata.License)
|
||||
assert.Equal(t, packageLicenseURL, p.Metadata.LicenseURL)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author.Name)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
|
||||
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
||||
@@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("WithExplicitNameField", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||
})
|
||||
|
||||
authorName := "John Doe"
|
||||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
@@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) {
|
||||
assert.Equal(t, "Doe", p.Metadata.Author.FamilyName)
|
||||
})
|
||||
|
||||
t.Run("WithEmptyJSONMetadata", func(t *testing.T) {
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||
})
|
||||
|
||||
p, err := ParsePackage(
|
||||
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||
strings.NewReader(`{}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, p.Metadata)
|
||||
assert.Empty(t, p.Metadata.Author.Name)
|
||||
assert.Empty(t, p.RepositoryURLs)
|
||||
})
|
||||
|
||||
t.Run("NameFieldGeneration", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
||||
data := test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||
})
|
||||
|
||||
// Test with only individual name components - Name should be auto-generated
|
||||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
|
||||
@@ -56,6 +56,8 @@ func parseManifest(data []byte) (map[string]string, map[string]string) {
|
||||
paths[key] = entry.File
|
||||
names[entry.File] = entry.Name
|
||||
// Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css"
|
||||
// FIXME: INCORRECT-VITE-MANIFEST-PARSER: the logic is wrong, Vite manifest doesn't work this way
|
||||
// It just happens to be correct for the current modules dependencies
|
||||
for _, css := range entry.CSS {
|
||||
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
|
||||
paths[cssKey] = css
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package test
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
@@ -97,6 +98,17 @@ func WriteTarArchive(files map[string]string) *bytes.Buffer {
|
||||
return WriteTarCompression(func(w io.Writer) io.WriteCloser { return util.NopCloser{Writer: w} }, files)
|
||||
}
|
||||
|
||||
func WriteZipArchive(files map[string]string) *bytes.Buffer {
|
||||
buf := &bytes.Buffer{}
|
||||
zw := zip.NewWriter(buf)
|
||||
for name, content := range files {
|
||||
w, _ := zw.Create(name)
|
||||
_, _ = w.Write([]byte(content))
|
||||
}
|
||||
_ = zw.Close()
|
||||
return buf
|
||||
}
|
||||
|
||||
func WriteTarCompression[F func(io.Writer) io.WriteCloser | func(io.Writer) (io.WriteCloser, error)](compression F, files map[string]string) *bytes.Buffer {
|
||||
buf := &bytes.Buffer{}
|
||||
var cw io.WriteCloser
|
||||
|
||||
@@ -5,12 +5,14 @@ package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
@@ -31,8 +33,23 @@ const (
|
||||
ErrInvalidBadgeSlug = "InvalidBadgeSlug"
|
||||
)
|
||||
|
||||
type jsonProvider struct{}
|
||||
|
||||
func (j jsonProvider) Marshal(v any) ([]byte, error) { return json.Marshal(v) }
|
||||
|
||||
func (j jsonProvider) Unmarshal(data []byte, v any) error { return json.Unmarshal(data, v) }
|
||||
|
||||
func (j jsonProvider) NewDecoder(reader io.Reader) binding.JSONDecoder {
|
||||
return json.NewDecoder(reader)
|
||||
}
|
||||
|
||||
func (j jsonProvider) NewEncoder(writer io.Writer) binding.JSONEncoder {
|
||||
return json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
// AddBindingRules adds additional binding rules
|
||||
func AddBindingRules() {
|
||||
binding.JSONProvider = jsonProvider{}
|
||||
addGitRefNameBindingRule()
|
||||
addValidURLListBindingRule()
|
||||
addValidURLBindingRule()
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "2.1.0",
|
||||
"postcss": "8.5.8",
|
||||
"rollup-plugin-license": "3.7.0",
|
||||
"rolldown-license-plugin": "2.2.0",
|
||||
"sortablejs": "1.15.7",
|
||||
"swagger-ui-dist": "5.32.1",
|
||||
"tailwindcss": "3.4.19",
|
||||
@@ -73,8 +73,7 @@
|
||||
"vite-string-plugin": "2.0.2",
|
||||
"vue": "3.5.31",
|
||||
"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",
|
||||
|
||||
369
pnpm-lock.yaml
generated
369
pnpm-lock.yaml
generated
@@ -185,9 +185,9 @@ importers:
|
||||
postcss:
|
||||
specifier: 8.5.8
|
||||
version: 8.5.8
|
||||
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
|
||||
@@ -230,9 +230,6 @@ importers:
|
||||
vue-chartjs:
|
||||
specifier: 5.3.3
|
||||
version: 5.3.3(chart.js@4.5.1)(vue@3.5.31(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
|
||||
@@ -1235,144 +1232,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==}
|
||||
|
||||
@@ -1892,10 +1751,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==}
|
||||
|
||||
@@ -1916,10 +1771,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==}
|
||||
|
||||
@@ -2113,9 +1964,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==}
|
||||
|
||||
@@ -3385,9 +3233,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==}
|
||||
|
||||
@@ -3478,10 +3323,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'}
|
||||
@@ -3706,22 +3547,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.12:
|
||||
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||
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==}
|
||||
|
||||
@@ -3805,27 +3638,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'}
|
||||
@@ -4256,10 +4068,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}
|
||||
@@ -5205,81 +5013,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': {}
|
||||
@@ -5925,8 +5658,6 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
ansi_up@6.0.6: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
@@ -5942,8 +5673,6 @@ snapshots:
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
array-find-index@1.0.2: {}
|
||||
|
||||
asciinema-player@3.15.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
@@ -6112,8 +5841,6 @@ snapshots:
|
||||
|
||||
comment-parser@1.4.6: {}
|
||||
|
||||
commenting@1.1.0: {}
|
||||
|
||||
compare-versions@6.1.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
@@ -7556,8 +7283,6 @@ snapshots:
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.3
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
moo@0.5.3: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -7627,8 +7352,6 @@ snapshots:
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
package-name-regex@2.0.6: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -7815,6 +7538,8 @@ snapshots:
|
||||
|
||||
robust-predicates@3.0.3: {}
|
||||
|
||||
rolldown-license-plugin@2.2.0: {}
|
||||
|
||||
rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.122.0
|
||||
@@ -7839,51 +7564,6 @@ snapshots:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
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.17.23
|
||||
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
|
||||
@@ -7958,33 +7638,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
|
||||
@@ -8453,12 +8106,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
|
||||
|
||||
@@ -198,6 +198,23 @@ func PackageVersionMetadata(ctx *context.Context) {
|
||||
}
|
||||
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
repositoryURLs := make([]string, 0, len(pd.VersionProperties))
|
||||
for _, property := range pd.VersionProperties {
|
||||
if property.Name == swift_module.PropertyRepositoryURL {
|
||||
repositoryURLs = append(repositoryURLs, property.Value)
|
||||
}
|
||||
}
|
||||
|
||||
var author *swift_module.Person
|
||||
if metadata.Author.Name != "" || metadata.Author.GivenName != "" || metadata.Author.MiddleName != "" || metadata.Author.FamilyName != "" {
|
||||
author = &swift_module.Person{
|
||||
Type: "Person",
|
||||
Name: metadata.Author.Name,
|
||||
GivenName: metadata.Author.GivenName,
|
||||
MiddleName: metadata.Author.MiddleName,
|
||||
FamilyName: metadata.Author.FamilyName,
|
||||
}
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &headers{})
|
||||
|
||||
@@ -220,18 +237,14 @@ func PackageVersionMetadata(ctx *context.Context) {
|
||||
Keywords: metadata.Keywords,
|
||||
CodeRepository: metadata.RepositoryURL,
|
||||
License: metadata.License,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
Author: author,
|
||||
ProgrammingLanguage: swift_module.ProgrammingLanguage{
|
||||
Type: "ComputerLanguage",
|
||||
Name: "Swift",
|
||||
URL: "https://swift.org",
|
||||
},
|
||||
Author: swift_module.Person{
|
||||
Type: "Person",
|
||||
Name: metadata.Author.String(),
|
||||
GivenName: metadata.Author.GivenName,
|
||||
MiddleName: metadata.Author.MiddleName,
|
||||
FamilyName: metadata.Author.FamilyName,
|
||||
},
|
||||
RepositoryURLs: repositoryURLs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -1429,7 +1430,11 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
||||
} else {
|
||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
|
||||
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ 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) {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
redirect := req.FormValue("redirect")
|
||||
if req.Method != http.MethodPost || !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) {
|
||||
http.Error(resp, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resp.Header().Add("Location", redirect)
|
||||
// no OpenRedirect, the "redirect" is validated by "IsCurrentGiteaSiteURL" above
|
||||
resp.Header().Set("Location", redirect)
|
||||
resp.WriteHeader(http.StatusSeeOther)
|
||||
}
|
||||
|
||||
48
routers/common/redirect_test.go
Normal file
48
routers/common/redirect_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFetchRedirectDelegate(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://gitea/")()
|
||||
|
||||
cases := []struct {
|
||||
method string
|
||||
input string
|
||||
status int
|
||||
}{
|
||||
{method: "POST", input: "/foo?k=v", status: http.StatusSeeOther},
|
||||
{method: "GET", input: "/foo?k=v", status: http.StatusBadRequest},
|
||||
{method: "POST", input: `\/foo?k=v`, status: http.StatusBadRequest},
|
||||
{method: "POST", input: `\\/foo?k=v`, status: http.StatusBadRequest},
|
||||
{method: "POST", input: "https://gitea/xxx", status: http.StatusSeeOther},
|
||||
{method: "POST", input: "https://other/xxx", status: http.StatusBadRequest},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.method+" "+c.input, func(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(c.method, "/?redirect="+url.QueryEscape(c.input), nil)
|
||||
FetchRedirectDelegate(resp, req)
|
||||
assert.Equal(t, c.status, resp.Code)
|
||||
if c.status == http.StatusSeeOther {
|
||||
assert.Equal(t, c.input, resp.Header().Get("Location"))
|
||||
} else {
|
||||
assert.Empty(t, resp.Header().Get("Location"))
|
||||
assert.Equal(t, "Bad Request", strings.TrimSpace(resp.Body.String()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 + "/"
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -43,7 +43,8 @@ func RenderFile(ctx *context.Context) {
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||
}).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{
|
||||
CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(),
|
||||
CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(),
|
||||
RenderQueryString: ctx.Req.URL.RawQuery,
|
||||
})
|
||||
renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader)
|
||||
if err != nil {
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
routers/web/repo/setting/webhook_test.go
Normal file
15
routers/web/repo/setting/webhook_test.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -21,12 +21,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/git/attribute"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"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 {
|
||||
@@ -78,14 +77,17 @@ func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Re
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
||||
|
||||
var err error
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader)
|
||||
if err != nil {
|
||||
ctx.ServerError("Render", err)
|
||||
return true
|
||||
}
|
||||
|
||||
opts, ok := markup.GetExternalRendererOptions(renderer)
|
||||
usingIframe := ok && opts.DisplayInIframe
|
||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
||||
ctx.Data["RenderAsMarkup"] = util.Iif(usingIframe, "markup-iframe", "markup-inplace")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -184,8 +186,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) {
|
||||
@@ -238,8 +239,6 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
||||
case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize:
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
case handleFileViewRenderMarkup(ctx, buf, contentReader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsMarkup"] = true
|
||||
case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsDisplayingSource"] = true
|
||||
|
||||
@@ -195,16 +195,16 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
}).WithRelativePath(readmeFullPath)
|
||||
renderer := rctx.DetectMarkupRenderer(buf)
|
||||
if renderer != nil {
|
||||
ctx.Data["IsMarkup"] = true
|
||||
ctx.Data["RenderAsMarkup"] = "markup-inplace"
|
||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
|
||||
if err != nil {
|
||||
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
||||
delete(ctx.Data, "IsMarkup")
|
||||
delete(ctx.Data, "RenderAsMarkup")
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Data["IsMarkup"] != true {
|
||||
if ctx.Data["RenderAsMarkup"] == nil {
|
||||
ctx.Data["IsPlainText"] = true
|
||||
content, err := io.ReadAll(rd)
|
||||
if err != nil {
|
||||
|
||||
@@ -655,6 +655,9 @@ func ShowSSHKeys(ctx *context.Context) {
|
||||
// "authorized_keys" file format: "#" followed by comment line per key
|
||||
buf.WriteString("# Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified.\n")
|
||||
for i := range keys {
|
||||
if keys[i].Type == asymkey_model.KeyTypePrincipal {
|
||||
continue // SSH principal keys are not for signing or authentication
|
||||
}
|
||||
buf.WriteString(keys[i].OmitEmail())
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
@@ -151,21 +151,28 @@ func findBlockedRunByConcurrency(ctx context.Context, repoID int64, concurrencyG
|
||||
func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) {
|
||||
checkedConcurrencyGroup := make(container.Set[string])
|
||||
|
||||
// check run (workflow-level) concurrency
|
||||
if run.ConcurrencyGroup != "" {
|
||||
concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, run.ConcurrencyGroup)
|
||||
collect := func(concurrencyGroup string) error {
|
||||
concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, concurrencyGroup)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err)
|
||||
return fmt.Errorf("find blocked run by concurrency: %w", err)
|
||||
}
|
||||
if concurrentRun != nil && !concurrentRun.NeedApproval {
|
||||
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return err
|
||||
}
|
||||
jobs = append(jobs, js...)
|
||||
updatedJobs = append(updatedJobs, ujs...)
|
||||
}
|
||||
checkedConcurrencyGroup.Add(run.ConcurrencyGroup)
|
||||
checkedConcurrencyGroup.Add(concurrencyGroup)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check run (workflow-level) concurrency
|
||||
if run.ConcurrencyGroup != "" {
|
||||
if err := collect(run.ConcurrencyGroup); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// check job concurrency
|
||||
@@ -177,22 +184,12 @@ func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (job
|
||||
if !job.Status.IsDone() {
|
||||
continue
|
||||
}
|
||||
if job.ConcurrencyGroup == "" && checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) {
|
||||
if job.ConcurrencyGroup == "" || checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) {
|
||||
continue
|
||||
}
|
||||
concurrentRun, err := findBlockedRunByConcurrency(ctx, job.RepoID, job.ConcurrencyGroup)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("find blocked run by concurrency: %w", err)
|
||||
if err := collect(job.ConcurrencyGroup); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if concurrentRun != nil && !concurrentRun.NeedApproval {
|
||||
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
jobs = append(jobs, js...)
|
||||
updatedJobs = append(updatedJobs, ujs...)
|
||||
}
|
||||
checkedConcurrencyGroup.Add(job.ConcurrencyGroup)
|
||||
}
|
||||
return jobs, updatedJobs, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -134,3 +136,68 @@ jobs:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck verifies that when a run's
|
||||
// ConcurrencyGroup has already been checked at the run level, the same group is not
|
||||
// re-checked for individual jobs.
|
||||
func Test_checkRunConcurrency_NoDuplicateConcurrencyGroupCheck(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
// Run A: the triggering run with a concurrency group.
|
||||
runA := &actions_model.ActionRun{
|
||||
RepoID: 4,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: "test.yml",
|
||||
Index: 9901,
|
||||
Ref: "refs/heads/main",
|
||||
Status: actions_model.StatusRunning,
|
||||
ConcurrencyGroup: "test-cg",
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, runA))
|
||||
|
||||
// A done job for run A with the same ConcurrencyGroup.
|
||||
// This triggers the job-level concurrency check in checkRunConcurrency.
|
||||
jobADone := &actions_model.ActionRunJob{
|
||||
RunID: runA.ID,
|
||||
RepoID: 4,
|
||||
OwnerID: 1,
|
||||
JobID: "job1",
|
||||
Name: "job1",
|
||||
Status: actions_model.StatusSuccess,
|
||||
ConcurrencyGroup: "test-cg",
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, jobADone))
|
||||
|
||||
// Blocked run B competing for the same concurrency group.
|
||||
runB := &actions_model.ActionRun{
|
||||
RepoID: 4,
|
||||
OwnerID: 1,
|
||||
TriggerUserID: 1,
|
||||
WorkflowID: "test.yml",
|
||||
Index: 9902,
|
||||
Ref: "refs/heads/main",
|
||||
Status: actions_model.StatusBlocked,
|
||||
ConcurrencyGroup: "test-cg",
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, runB))
|
||||
|
||||
// A blocked job belonging to run B (no job-level concurrency group).
|
||||
jobBBlocked := &actions_model.ActionRunJob{
|
||||
RunID: runB.ID,
|
||||
RepoID: 4,
|
||||
OwnerID: 1,
|
||||
JobID: "job1",
|
||||
Name: "job1",
|
||||
Status: actions_model.StatusBlocked,
|
||||
}
|
||||
assert.NoError(t, db.Insert(ctx, jobBBlocked))
|
||||
|
||||
jobs, _, err := checkRunConcurrency(ctx, runA)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if assert.Len(t, jobs, 1) {
|
||||
assert.Equal(t, jobBBlocked.ID, jobs[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/webhook"
|
||||
@@ -523,16 +525,49 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
|
||||
type MergePullRequestForm struct {
|
||||
// required: true
|
||||
// enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"]
|
||||
Do string `binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
|
||||
MergeTitleField string
|
||||
MergeMessageField string
|
||||
MergeCommitID string // only used for manually-merged
|
||||
Do string `json:"do" binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
|
||||
MergeTitleField string `json:"merge_title_field,omitempty"`
|
||||
MergeMessageField string `json:"merge_message_field,omitempty"`
|
||||
MergeCommitID string `json:"merge_commit_id,omitempty"` // only used for manually-merged
|
||||
HeadCommitID string `json:"head_commit_id,omitempty"`
|
||||
ForceMerge bool `json:"force_merge,omitempty"`
|
||||
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
||||
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
|
||||
}
|
||||
|
||||
func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
|
||||
// This is for backward compatibility, to support both field names like "do" and "Do",
|
||||
// because old code doesn't have "json" tag for these fields
|
||||
type aux struct {
|
||||
Do1 string `json:"do"`
|
||||
Do2 string `json:"Do"`
|
||||
MergeTitleField1 string `json:"merge_title_field"`
|
||||
MergeTitleField2 string `json:"MergeTitleField"`
|
||||
MergeMessageField1 string `json:"merge_message_field"`
|
||||
MergeMessageField2 string `json:"MergeMessageField"`
|
||||
MergeCommitID1 string `json:"merge_commit_id"`
|
||||
MergeCommitID2 string `json:"MergeCommitID"`
|
||||
|
||||
HeadCommitID string `json:"head_commit_id"`
|
||||
ForceMerge bool `json:"force_merge"`
|
||||
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed"`
|
||||
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge"`
|
||||
}
|
||||
var a aux
|
||||
if err := json.Unmarshal(b, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Do = util.IfZero(a.Do1, a.Do2)
|
||||
f.MergeTitleField = util.IfZero(a.MergeTitleField1, a.MergeTitleField2)
|
||||
f.MergeMessageField = util.IfZero(a.MergeMessageField1, a.MergeMessageField2)
|
||||
f.MergeCommitID = util.IfZero(a.MergeCommitID1, a.MergeCommitID2)
|
||||
f.HeadCommitID = a.HeadCommitID
|
||||
f.ForceMerge = a.ForceMerge
|
||||
f.MergeWhenChecksSucceed = a.MergeWhenChecksSucceed
|
||||
f.DeleteBranchAfterMerge = a.DeleteBranchAfterMerge
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
|
||||
@@ -6,7 +6,10 @@ package forms
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
||||
@@ -37,3 +40,48 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
||||
assert.Equal(t, v.expected, v.form.HasEmptyContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePullRequestForm(t *testing.T) {
|
||||
expected := &MergePullRequestForm{
|
||||
Do: "merge",
|
||||
MergeTitleField: "title",
|
||||
MergeMessageField: "message",
|
||||
MergeCommitID: "merge-id",
|
||||
HeadCommitID: "head-id",
|
||||
ForceMerge: true,
|
||||
MergeWhenChecksSucceed: true,
|
||||
DeleteBranchAfterMerge: new(true),
|
||||
}
|
||||
|
||||
t.Run("NewFields", func(t *testing.T) {
|
||||
input := `{
|
||||
"do": "merge",
|
||||
"merge_title_field": "title",
|
||||
"merge_message_field": "message",
|
||||
"merge_commit_id": "merge-id",
|
||||
"head_commit_id": "head-id",
|
||||
"force_merge": true,
|
||||
"merge_when_checks_succeed": true,
|
||||
"delete_branch_after_merge": true
|
||||
}`
|
||||
var m *MergePullRequestForm
|
||||
require.NoError(t, json.Unmarshal([]byte(input), &m))
|
||||
assert.Equal(t, expected, m)
|
||||
})
|
||||
|
||||
t.Run("OldFields", func(t *testing.T) {
|
||||
input := `{
|
||||
"Do": "merge",
|
||||
"MergeTitleField": "title",
|
||||
"MergeMessageField": "message",
|
||||
"MergeCommitID": "merge-id",
|
||||
"head_commit_id": "head-id",
|
||||
"force_merge": true,
|
||||
"merge_when_checks_succeed": true,
|
||||
"delete_branch_after_merge": true
|
||||
}`
|
||||
var m *MergePullRequestForm
|
||||
require.NoError(t, json.Unmarshal([]byte(input), &m))
|
||||
assert.Equal(t, expected, m)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,6 +93,9 @@ export default {
|
||||
return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
|
||||
})),
|
||||
},
|
||||
extend: {
|
||||
zIndex: {'1': '1'},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
plugin(({addUtilities}) => {
|
||||
|
||||
109
templates/devtest/form-fields.tmpl
Normal file
109
templates/devtest/form-fields.tmpl
Normal file
@@ -0,0 +1,109 @@
|
||||
{{template "devtest/devtest-header"}}
|
||||
<div class="page-content devtest ui container">
|
||||
<form class="ui form left-right-form">
|
||||
<h4 class="ui dividing header">Input</h4>
|
||||
<div class="inline field">
|
||||
<label>Normal</label>
|
||||
<input type="text" value="value">
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>Readonly</label>
|
||||
<input type="text" value="value" readonly>
|
||||
</div>
|
||||
<div class="inline disabled field">
|
||||
<label>Disabled</label>
|
||||
<input type="text" value="value" disabled>
|
||||
</div>
|
||||
<div class="inline field error">
|
||||
<label>Error</label>
|
||||
<input type="text" value="value">
|
||||
</div>
|
||||
|
||||
<h4 class="ui dividing header">Textarea</h4>
|
||||
<div class="inline field">
|
||||
<label>Normal</label>
|
||||
<textarea rows="2">value</textarea>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>Readonly</label>
|
||||
<textarea rows="2" readonly>value</textarea>
|
||||
</div>
|
||||
<div class="inline disabled field">
|
||||
<label>Disabled</label>
|
||||
<textarea rows="2" disabled>value</textarea>
|
||||
</div>
|
||||
<div class="inline field error">
|
||||
<label>Error</label>
|
||||
<textarea rows="2">value</textarea>
|
||||
</div>
|
||||
|
||||
<h4 class="ui dividing header">Dropdown</h4>
|
||||
<div class="inline field">
|
||||
<label>Normal</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" value="a">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text">Option A</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="a">Option A</div>
|
||||
<div class="item" data-value="b">Option B</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<label>Readonly</label>
|
||||
<div class="ui selection dropdown" readonly>
|
||||
<input type="hidden" value="a">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text">Option A</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="a">Option A</div>
|
||||
<div class="item" data-value="b">Option B</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline disabled field">
|
||||
<label>Disabled</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" value="a">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text">Option A</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="a">Option A</div>
|
||||
<div class="item" data-value="b">Option B</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field error">
|
||||
<label>Error</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" value="a">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text">Option A</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="a">Option A</div>
|
||||
<div class="item" data-value="b">Option B</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="ui dividing header">Required</h4>
|
||||
<div class="inline required field">
|
||||
<label>Normal</label>
|
||||
<input type="text" value="value">
|
||||
</div>
|
||||
<div class="inline required field">
|
||||
<label>Readonly</label>
|
||||
<input type="text" value="value" readonly>
|
||||
</div>
|
||||
<div class="inline required disabled field">
|
||||
<label>Disabled</label>
|
||||
<input type="text" value="value" disabled>
|
||||
</div>
|
||||
<div class="inline required field error">
|
||||
<label>Error</label>
|
||||
<input type="text" value="value">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{template "devtest/devtest-footer"}}
|
||||
@@ -117,7 +117,7 @@
|
||||
<input id="lfs_root_path" name="lfs_root_path" value="{{.lfs_root_path}}">
|
||||
<span class="help">{{ctx.Locale.Tr "install.lfs_path_helper"}}</span>
|
||||
</div>
|
||||
<div class="inline required field {{if .Err_RunUser}}error{{end}}">
|
||||
<div class="inline field">
|
||||
<label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label>
|
||||
<input id="run_user" name="run_user" value="{{.run_user}}" readonly>
|
||||
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
{{$isMember := .IsOrganizationMember}}
|
||||
{{range .Members}}
|
||||
{{if or $isMember (call $.IsPublicMember .ID)}}
|
||||
<a href="{{.HomeLink}}" title="{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
|
||||
{{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
<div class="ui right">
|
||||
{{if .Team.IsMember ctx $.SignedUser.ID}}
|
||||
<form>
|
||||
<button class="ui red tiny button delete-button" data-modal-id="leave-team-sidebar"
|
||||
<button class="ui red mini compact button delete-button" data-modal-id="leave-team-sidebar"
|
||||
data-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
|
||||
data-name="{{.Team.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
||||
</form>
|
||||
{{else if .IsOrganizationOwner}}
|
||||
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join">
|
||||
<input type="hidden" name="page" value="team">
|
||||
<button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
|
||||
<button type="submit" class="ui primary mini compact button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
<div class="ui top attached header">
|
||||
<a class="tw-text-text" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
|
||||
<div class="ui right">
|
||||
<a class="ui primary tiny button" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}">{{ctx.Locale.Tr "view"}}</a>
|
||||
<a class="ui primary mini compact button" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}">{{ctx.Locale.Tr "view"}}</a>
|
||||
{{if .IsMember ctx $.SignedUser.ID}}
|
||||
<form>
|
||||
<button class="ui red tiny button delete-button" data-modal-id="leave-team"
|
||||
<button class="ui red mini compact button delete-button" data-modal-id="leave-team"
|
||||
data-url="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
|
||||
data-name="{{.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
||||
</form>
|
||||
{{else if $.IsOrganizationOwner}}
|
||||
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/join">
|
||||
<button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
|
||||
<button type="submit" class="ui primary mini compact button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached segment members">
|
||||
{{range .Members}}
|
||||
{{template "shared/user/avatarlink" dict "user" .}}
|
||||
{{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui bottom attached header">
|
||||
|
||||
@@ -25,49 +25,49 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container-main">
|
||||
<div class="ui top attached header tw-flex tw-items-center tw-justify-between">
|
||||
<span class="tw-text-base tw-font-semibold">{{ctx.Locale.TrN .Page.Paginater.Total "actions.runs.workflow_run_count_1" "actions.runs.workflow_run_count_n" .Page.Paginater.Total}}</span>
|
||||
<div class="ui secondary filter menu tw-flex tw-items-center tw-m-0">
|
||||
<!-- Actor -->
|
||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search"}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
|
||||
</div>
|
||||
<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
|
||||
{{ctx.Locale.Tr "actions.runs.actors_no_select"}}
|
||||
</a>
|
||||
{{range .Actors}}
|
||||
<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
|
||||
{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
|
||||
<div class="ui top attached header flex-text-block tw-flex-wrap tw-justify-between">
|
||||
<strong>{{ctx.Locale.TrN .Page.Paginater.Total "actions.runs.workflow_run_count_1" "actions.runs.workflow_run_count_n" .Page.Paginater.Total}}</strong>
|
||||
<div class="ui secondary filter menu flex-text-block tw-m-0">
|
||||
<!-- Actor -->
|
||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search"}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
|
||||
</div>
|
||||
<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
|
||||
{{ctx.Locale.Tr "actions.runs.actors_no_select"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="ui dropdown jump item">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.runs.status"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search"}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}">
|
||||
{{range .Actors}}
|
||||
<a class="item{{if eq .ID $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{.ID}}&status={{$.CurStatus}}">
|
||||
{{ctx.AvatarUtils.Avatar . 20}} {{.GetDisplayName}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="ui dropdown jump item">
|
||||
<span class="text">{{ctx.Locale.Tr "actions.runs.status"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search"}}</i>
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}">
|
||||
</div>
|
||||
<a class="item{{if not $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
|
||||
{{ctx.Locale.Tr "actions.runs.status_no_select"}}
|
||||
</a>
|
||||
{{range .StatusInfoList}}
|
||||
<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
|
||||
{{.DisplayedStatus}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="item{{if not $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status=0">
|
||||
{{ctx.Locale.Tr "actions.runs.status_no_select"}}
|
||||
</a>
|
||||
{{range .StatusInfoList}}
|
||||
<a class="item{{if eq .Status $.CurStatus}} active{{end}}" href="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{.Status}}">
|
||||
{{.DisplayedStatus}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .AllowDisableOrEnableWorkflow}}
|
||||
{{if .AllowDisableOrEnableWorkflow}}
|
||||
<button class="ui jump dropdown btn interact-bg tw-p-2">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
@@ -76,7 +76,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
{{template "repo/actions/status" (dict "status" $run.Status.String)}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}">
|
||||
{{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
|
||||
</a>
|
||||
<span class="flex-item-title" title="{{$run.Title}}">
|
||||
{{if $run.Title}}
|
||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $run.Title $run.Link $.Repository}}
|
||||
{{else}}
|
||||
<a href="{{$run.Link}}">{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}</a>
|
||||
{{end}}
|
||||
</span>
|
||||
<div class="flex-item-body">
|
||||
<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<div class="ui blue info attached message tw-relative tw-z-10 tw-flex tw-justify-between tw-items-center">
|
||||
<span class="ui text middle">{{ctx.Locale.Tr "actions.workflow.has_workflow_dispatch"}}</span>
|
||||
<button class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
|
||||
{{/* "z-index" is used to maintain continuous attached styling and keep the colored border-bottom visible (pre-existing fomantic issue with negative margins) */}}
|
||||
<div class="ui blue info attached message flex-text-block tw-flex-wrap tw-justify-between tw-z-1">
|
||||
<span>{{ctx.Locale.Tr "actions.workflow.has_workflow_dispatch"}}</span>
|
||||
<div class="flex-text-block tw-bg-box-body tw-rounded">{{/*make the button have correct hovered color */}}
|
||||
<button class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="runWorkflowDispatchModal" class="ui tiny modal">
|
||||
<div class="content">
|
||||
|
||||
@@ -45,10 +45,8 @@
|
||||
<div class="blame-avatar">
|
||||
{{$row.Avatar}}
|
||||
</div>
|
||||
<div class="blame-message">
|
||||
<a class="suppressed tw-text-text" href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
|
||||
{{$row.CommitMessage}}
|
||||
</a>
|
||||
<div class="blame-message muted-links" title="{{$row.CommitMessage}}">
|
||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $row.CommitMessage $row.CommitURL $.Repository}}
|
||||
</div>
|
||||
<div class="blame-time not-mobile">
|
||||
{{$row.CommitSince}}
|
||||
|
||||
@@ -13,22 +13,16 @@
|
||||
{{ctx.Locale.Tr "action.compare_commits_general"}}
|
||||
{{end}}
|
||||
</h2>
|
||||
{{$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 -}}
|
||||
|
||||
<div class="ui segment choose branch">
|
||||
<a class="tw-mr-2" href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$compareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{ctx.Locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
{{template "repo/code/upstream_diverging_info" .}}
|
||||
{{end}}
|
||||
{{template "repo/view_list" .}}
|
||||
{{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
|
||||
{{if and .ReadmeExist (or .RenderAsMarkup .IsPlainText)}}
|
||||
{{template "repo/view_file" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -40,7 +40,9 @@
|
||||
{{if .IsRepresentableAsText}}
|
||||
<a href="?display=source" class="ui mini basic button file-view-toggle-source {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
|
||||
{{end}}
|
||||
{{if .HasSourceRenderedToggle}}
|
||||
<a href="?display=rendered" class="ui mini basic button file-view-toggle-rendered {{if not .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if not .ReadmeInList}}
|
||||
<div class="ui buttons tw-mr-1">
|
||||
@@ -90,15 +92,15 @@
|
||||
</h4>
|
||||
|
||||
<div class="ui bottom attached table unstackable segment">
|
||||
{{if not .IsMarkup}}
|
||||
{{if not .RenderAsMarkup}}
|
||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||
{{end}}
|
||||
<div class="file-view {{if .IsMarkup}}markup {{.MarkupType}}{{else if .IsPlainText}}plain-text{{else if .IsDisplayingSource}}code-view{{end}}">
|
||||
<div class="file-view {{if eq .RenderAsMarkup "markup-inplace"}}markup {{.MarkupType}}{{else if .IsPlainText}}plain-text{{else if .IsDisplayingSource}}code-view{{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}}
|
||||
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<a class="avatar-with-link" {{if gt .user.ID 0}}href="{{.user.HomeLink}}"{{end}}>{{ctx.AvatarUtils.Avatar .user (or .size 20)}}</a>
|
||||
<a class="avatar-with-link" {{if .tooltip}}data-tooltip-content="{{.user.Name}}{{if .user.FullName}} ({{.user.FullName}}){{end}}"{{end}} {{if gt .user.ID 0}}href="{{.user.HomeLink}}"{{end}}>{{ctx.AvatarUtils.Avatar .user (or .size 20)}}</a>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Gitea API</title>
|
||||
{{/* HINT: SWAGGER-OPENAPI-VIEWER: another place is "modules/markup/external/openapi.go" */}}
|
||||
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
|
||||
{{/* HINT: SWAGGER-CSS-IMPORT: import swagger styles ahead to avoid UI flicker (e.g.: the swagger-back-link element) */}}
|
||||
<link rel="stylesheet" href="{{AssetURI "css/swagger.css"}}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
36
templates/swagger/v1_json.tmpl
generated
36
templates/swagger/v1_json.tmpl
generated
@@ -26771,10 +26771,14 @@
|
||||
"description": "MergePullRequestForm form for merging Pull Request",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Do"
|
||||
"do"
|
||||
],
|
||||
"properties": {
|
||||
"Do": {
|
||||
"delete_branch_after_merge": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "DeleteBranchAfterMerge"
|
||||
},
|
||||
"do": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"merge",
|
||||
@@ -26783,20 +26787,8 @@
|
||||
"squash",
|
||||
"fast-forward-only",
|
||||
"manually-merged"
|
||||
]
|
||||
},
|
||||
"MergeCommitID": {
|
||||
"type": "string"
|
||||
},
|
||||
"MergeMessageField": {
|
||||
"type": "string"
|
||||
},
|
||||
"MergeTitleField": {
|
||||
"type": "string"
|
||||
},
|
||||
"delete_branch_after_merge": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "DeleteBranchAfterMerge"
|
||||
],
|
||||
"x-go-name": "Do"
|
||||
},
|
||||
"force_merge": {
|
||||
"type": "boolean",
|
||||
@@ -26806,6 +26798,18 @@
|
||||
"type": "string",
|
||||
"x-go-name": "HeadCommitID"
|
||||
},
|
||||
"merge_commit_id": {
|
||||
"type": "string",
|
||||
"x-go-name": "MergeCommitID"
|
||||
},
|
||||
"merge_message_field": {
|
||||
"type": "string",
|
||||
"x-go-name": "MergeMessageField"
|
||||
},
|
||||
"merge_title_field": {
|
||||
"type": "string",
|
||||
"x-go-name": "MergeTitleField"
|
||||
},
|
||||
"merge_when_checks_succeed": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "MergeWhenChecksSucceed"
|
||||
|
||||
@@ -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 <API> & "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);
|
||||
|
||||
69
tests/e2e/file-view-render.test.ts
Normal file
69
tests/e2e/file-view-render.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
9
tests/e2e/licenses.test.ts
Normal file
9
tests/e2e/licenses.test.ts
Normal file
@@ -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/');
|
||||
});
|
||||
@@ -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('/');
|
||||
|
||||
@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiNewReaction)
|
||||
|
||||
// Add existing reaction
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "rocket",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Blocked user can't react to comment
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiNewReaction)
|
||||
|
||||
// Add existing reaction
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Get end result of reaction list of issue #1
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@@ -35,9 +36,26 @@ func TestPackageSwift(t *testing.T) {
|
||||
packageID := packageScope + "." + packageName
|
||||
packageVersion := "1.0.3"
|
||||
packageVersion2 := "1.0.4"
|
||||
packageVersion3 := "1.0.5"
|
||||
packageAuthor := "KN4CK3R"
|
||||
packageDescription := "Gitea Test Package"
|
||||
packageRepositoryURL := "https://gitea.io/gitea/gitea"
|
||||
packageCodeRepositoryURL := "https://gitea.io/gitea/gitea" // this one is not used as a property, it is meta
|
||||
packageLicenseURL := "https://opensource.org/license/mit"
|
||||
packageRepositoryURL1 := "https://gitea.io/gitea/repo"
|
||||
packageRepositoryURLs := []string{packageRepositoryURL1, "https://gitea.io/gitea/repo.git", "ssh://git@gitea.io/gitea/repo.git"}
|
||||
makePackageMetadataJSON := func(ver string) string {
|
||||
tmpl := `{
|
||||
"name":"` + packageName + `",
|
||||
"version":"%s",
|
||||
"description":"` + packageDescription + `",
|
||||
"codeRepository":"` + packageCodeRepositoryURL + `",
|
||||
"licenseURL":"` + packageLicenseURL + `",
|
||||
"author":{"givenName":"` + packageAuthor + `"},
|
||||
"repositoryURLs":["` + strings.Join(packageRepositoryURLs, `","`) + `"]
|
||||
}`
|
||||
return fmt.Sprintf(tmpl, ver)
|
||||
}
|
||||
|
||||
contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||
contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||
|
||||
@@ -146,7 +164,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
makePackageMetadataJSON(packageVersion),
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
|
||||
@@ -164,8 +182,8 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
assert.Len(t, pd.VersionProperties, 3)
|
||||
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
|
||||
assert.NoError(t, err)
|
||||
@@ -234,7 +252,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
makePackageMetadataJSON(packageVersion2),
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
|
||||
@@ -252,8 +270,8 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
assert.Len(t, pd.VersionProperties, 3)
|
||||
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
|
||||
assert.NoError(t, err)
|
||||
@@ -354,8 +372,11 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Equal(t, packageVersion, result.Metadata.Version)
|
||||
assert.Equal(t, packageDescription, result.Metadata.Description)
|
||||
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
|
||||
assert.Equal(t, packageLicenseURL, result.Metadata.LicenseURL)
|
||||
require.NotNil(t, result.Metadata.Author)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.Name)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
|
||||
assert.ElementsMatch(t, packageRepositoryURLs, result.Metadata.RepositoryURLs)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
|
||||
AddBasicAuth(user.Name)
|
||||
@@ -364,6 +385,41 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("UploadEmptyJSONMetadata", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)
|
||||
var body bytes.Buffer
|
||||
mpw := multipart.NewWriter(&body)
|
||||
|
||||
part, err := mpw.CreateFormFile("source-archive", "source-archive.zip")
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(part, test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mpw.WriteField("metadata", "{}"))
|
||||
require.NoError(t, mpw.Close())
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", uploadURL, &body).
|
||||
SetHeader("Content-Type", mpw.FormDataContentType()).
|
||||
SetHeader("Accept", swift_router.AcceptJSON).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)).
|
||||
AddBasicAuth(user.Name).
|
||||
SetHeader("Accept", swift_router.AcceptJSON)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
result := DecodeJSON(t, resp, &swift_router.PackageVersionMetadataResponse{})
|
||||
|
||||
assert.Nil(t, result.Metadata.Author)
|
||||
assert.Empty(t, result.Metadata.RepositoryURLs)
|
||||
assert.Empty(t, result.Metadata.CodeRepository)
|
||||
assert.Empty(t, result.Metadata.LicenseURL)
|
||||
})
|
||||
|
||||
t.Run("DownloadManifest", func(t *testing.T) {
|
||||
manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
|
||||
|
||||
@@ -421,7 +477,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL).
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL1).
|
||||
SetHeader("Accept", swift_router.AcceptJSON)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestAPIPrivateServ(t *testing.T) {
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Add reading deploy key
|
||||
deployKey, err := asymkey_model.AddDeployKey(ctx, 19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
|
||||
deployKey, err := asymkey_model.AddDeployKey(ctx, 19 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Can pull from repo we're a deploy key for
|
||||
@@ -106,17 +106,17 @@ func TestAPIPrivateServ(t *testing.T) {
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Cannot pull from a private repo we're not associated with
|
||||
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
|
||||
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
|
||||
assert.Error(t, extra.Error)
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Cannot pull from a public repo we're not associated with
|
||||
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
|
||||
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
|
||||
assert.Error(t, extra.Error)
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Add writing deploy key
|
||||
deployKey, err = asymkey_model.AddDeployKey(ctx, 20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
|
||||
deployKey, err = asymkey_model.AddDeployKey(ctx, 20 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Cannot push to a private repo with reading key
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>" tag, but it indeed is the sanitizer's job
|
||||
assert.Equal(t, `<script crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><div><any attr="val"><script></script></any></div>`, respSub.Body.String())
|
||||
assert.Equal(t,
|
||||
`<script nonce crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`" id="gitea-external-render-helper" data-render-query-string=""></script>`+
|
||||
`<link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`">`+
|
||||
`<div><any attr="val"><script></script></any></div>`,
|
||||
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, `<script crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`"></script><link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`"><script>foo("raw")</script>`, respSub.Body.String())
|
||||
assert.Equal(t,
|
||||
`<script nonce crossorigin src="`+public.AssetURI("js/external-render-helper.js")+`" id="gitea-external-render-helper" data-render-query-string="a=1%2f2"></script>`+
|
||||
`<link rel="stylesheet" href="`+public.AssetURI("css/theme-gitea-auto.css")+`">`+
|
||||
`<script>foo("raw")</script>`,
|
||||
respSub.Body.String(),
|
||||
)
|
||||
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"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/models/unittest"
|
||||
@@ -22,9 +24,20 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestViewUser(t *testing.T) {
|
||||
func TestUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("ViewUser", testViewUser)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ViewLimitedAndPrivateUserAndRename", testViewLimitedAndPrivateUserAndRename)
|
||||
t.Run("ExportUserGPGKeys", testExportUserGPGKeys)
|
||||
t.Run("GetUserRss", testGetUserRss)
|
||||
t.Run("ListStopWatches", testUserListStopWatches)
|
||||
t.Run("LocationMapLink", testUserLocationMapLink)
|
||||
t.Run("RenameUsername", testRenameUsername)
|
||||
}
|
||||
|
||||
func testViewUser(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -32,12 +45,28 @@ func TestViewUser(t *testing.T) {
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, `# Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified.
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDWVj0fQ5N8wNc0LVNA41wDLYJ89ZIbejrPfg/avyj3u/ZohAKsQclxG4Ju0VirduBFF9EOiuxoiFBRr3xRpqzpsZtnMPkWVWb+akZwBFAx8p+jKdy4QXR/SZqbVobrGwip2UjSrri1CtBxpJikojRIZfCnDaMOyd9Jp6KkujvniFzUWdLmCPxUE9zhTaPu0JsEP7MW0m6yx7ZUhHyfss+NtqmFTaDO+QlMR7L2QkDliN2Jl3Xa3PhuWnKJfWhdAq1Cw4oraKUOmIgXLkuiuxVQ6mD3AiFupkmfqdHq6h+uHHmyQqv3gU+/sD8GbGAhf6ftqhTsXjnv1Aj4R8NoDf9BS6KRkzkeun5UisSzgtfQzjOMEiJtmrep2ZQrMGahrXa+q4VKr0aKJfm+KlLfwm/JztfsBcqQWNcTURiCFqz+fgZw0Ey/de0eyMzldYTdXXNRYCKjs9bvBK+6SSXRM7AhftfQ0ZuoW5+gtinPrnmoOaSCEJbAiEiTO/BzOHgowiM=
|
||||
`, resp.Body.String())
|
||||
|
||||
_ = db.TruncateBeans(t.Context(), &asymkey_model.PublicKey{})
|
||||
_ = db.Insert(t.Context(), &asymkey_model.PublicKey{
|
||||
OwnerID: 2,
|
||||
Name: "key-1",
|
||||
Content: "ssh-rsa AAAA",
|
||||
Type: asymkey_model.KeyTypeUser,
|
||||
}, &asymkey_model.PublicKey{
|
||||
OwnerID: 2,
|
||||
Name: "key-2",
|
||||
Content: "principal",
|
||||
Type: asymkey_model.KeyTypePrincipal,
|
||||
})
|
||||
req = NewRequest(t, "GET", "/user2.keys")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, `# Gitea isn't a key server. The keys are exported as the user uploaded and might not have been fully verified.
|
||||
ssh-rsa AAAA
|
||||
`, resp.Body.String())
|
||||
}
|
||||
|
||||
func TestRenameUsername(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testRenameUsername(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "newUsername",
|
||||
@@ -50,9 +79,7 @@ func TestRenameUsername(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||
}
|
||||
|
||||
func TestViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
// user 22 is a limited visibility org
|
||||
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||
req := NewRequest(t, "GET", "/"+org22.Name)
|
||||
@@ -119,9 +146,7 @@ func TestViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusTemporaryRedirect) // login user2 can visit private visibility user via old name
|
||||
}
|
||||
|
||||
func TestRenameInvalidUsername(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testRenameInvalidUsername(t *testing.T) {
|
||||
invalidUsernames := []string{
|
||||
"%2f*",
|
||||
"%2f.",
|
||||
@@ -166,9 +191,7 @@ func TestRenameInvalidUsername(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenameReservedUsername(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
reservedUsernames := []string{
|
||||
// ".", "..", ".well-known", // The names are not only reserved but also invalid
|
||||
"api",
|
||||
@@ -198,8 +221,7 @@ func TestRenameReservedUsername(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportUserGPGKeys(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
func testExportUserGPGKeys(t *testing.T) {
|
||||
testExportUserGPGKeys := func(t *testing.T, user, expected string) {
|
||||
session := loginUser(t, user)
|
||||
t.Logf("Testing username %s export gpg keys", user)
|
||||
@@ -284,9 +306,7 @@ GrE0MHOxUbc9tbtyk0F1SuzREUBH
|
||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
|
||||
func TestGetUserRss(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testGetUserRss(t *testing.T) {
|
||||
user34 := "the_34-user.with.all.allowedChars"
|
||||
req := NewRequestf(t, "GET", "/%s.rss", user34)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
@@ -306,9 +326,7 @@ func TestGetUserRss(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestListStopWatches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserListStopWatches(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -329,8 +347,7 @@ func TestListStopWatches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserLocationMapLink(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
func testUserLocationMapLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
@@ -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
|
||||
@@ -158,9 +153,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}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -171,6 +172,7 @@ function reducedSourcemapPlugin(): Plugin {
|
||||
'js/index.',
|
||||
'js/iife.',
|
||||
'js/swagger.',
|
||||
'js/external-render-frontend.',
|
||||
'js/external-render-helper.',
|
||||
'js/eventsource.sharedworker.',
|
||||
];
|
||||
@@ -257,8 +259,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,
|
||||
@@ -314,33 +318,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<string, string> = {};
|
||||
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');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ap-terminal {
|
||||
/* Related: https://github.com/asciinema/asciinema-player/blob/develop/src/components/Terminal.js : <div class="ap-term" ...>
|
||||
Old PR: Fix UI regression of asciinema player https://github.com/go-gitea/gitea/pull/26159 */
|
||||
.ap-term {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -527,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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -84,15 +84,6 @@
|
||||
border-color: var(--color-warning-border);
|
||||
}
|
||||
|
||||
/* use opaque colors for buttons inside colored messages */
|
||||
.ui.message .ui.button:hover {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.ui.message .ui.button:active {
|
||||
background: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.ui.message > .close.icon {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
29
web_src/css/swagger-standalone.css
Normal file
29
web_src/css/swagger-standalone.css
Normal file
@@ -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;
|
||||
}
|
||||
67
web_src/js/external-render-frontend.ts
Normal file
67
web_src/js/external-render-frontend.ts
Normal file
@@ -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<string, LazyLoadFunc> = {
|
||||
'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<ArrayBuffer> | 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<ArrayBuffer> {
|
||||
return Uint8Array.from(atob(this.rawString), (c) => c.charCodeAt(0));
|
||||
}
|
||||
contentBytes(): Uint8Array<ArrayBuffer> {
|
||||
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<HTMLElement>('#frontend-render-viewer')!;
|
||||
const renderNames = viewerContainer.getAttribute('data-frontend-renders')!.split(' ');
|
||||
const fileTreePath = viewerContainer.getAttribute('data-file-tree-path')!;
|
||||
|
||||
const fileDataElem = document.querySelector<HTMLTextAreaElement>('#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();
|
||||
25
web_src/js/external-render-helper.test.ts
Normal file
25
web_src/js/external-render-helper.test.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import './external-render-helper.ts';
|
||||
|
||||
test('isValidCssColor', async () => {
|
||||
const isValidCssColor = window.testModules.externalRenderHelper!.isValidCssColor;
|
||||
expect(isValidCssColor(null)).toBe(false);
|
||||
expect(isValidCssColor('')).toBe(false);
|
||||
|
||||
expect(isValidCssColor('#123')).toBe(true);
|
||||
expect(isValidCssColor('#1234')).toBe(true);
|
||||
expect(isValidCssColor('#abcabc')).toBe(true);
|
||||
expect(isValidCssColor('#abcabc12')).toBe(true);
|
||||
|
||||
expect(isValidCssColor('rgb(255 255 255)')).toBe(true);
|
||||
expect(isValidCssColor('rgb(0, 255, 255)')).toBe(true);
|
||||
|
||||
// examples from MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/rgb
|
||||
expect(isValidCssColor('rgb(255 255 255 / 50%)')).toBe(true);
|
||||
expect(isValidCssColor('rgb(from #123456 hwb(120deg 10% 20%) calc(g + 40) b / 0.5)')).toBe(true);
|
||||
|
||||
expect(isValidCssColor('#123 ; other')).toBe(false);
|
||||
expect(isValidCssColor('#123 : other')).toBe(false);
|
||||
expect(isValidCssColor('#rgb(0, 255, 255); other')).toBe(false);
|
||||
expect(isValidCssColor('#rgb(0, 255, 255)} other')).toBe(false);
|
||||
expect(isValidCssColor('url(other)')).toBe(false);
|
||||
});
|
||||
@@ -15,27 +15,41 @@ RENDER_COMMAND = `echo '<div style="width: 100%; height: 2000px; border: 10px so
|
||||
|
||||
*/
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
// Check whether the user-provided color value is a valid CSS color format to avoid CSS injection.
|
||||
// Don't extract this function to a common module, because this file is an IIFE module for external render
|
||||
// and should not have any dependency to avoid potential conflicts.
|
||||
function isValidCssColor(s: string | null): boolean {
|
||||
if (!s) return false;
|
||||
// it should only be in format "#hex" or "rgb(...)", because it comes from a computed style's color value
|
||||
const reHex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||
const reRgb = /^rgb\([^{}'";:]+\)$/;
|
||||
return reHex.test(s) || reRgb.test(s);
|
||||
}
|
||||
|
||||
const isDarkTheme = url.searchParams.get('gitea-is-dark-theme') === 'true';
|
||||
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 = 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');
|
||||
if (backgroundColor) {
|
||||
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');
|
||||
style.textContent = `
|
||||
: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<string, any> = {}) => {
|
||||
@@ -75,3 +89,7 @@ if (iframeId) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (window.testModules) {
|
||||
window.testModules.externalRenderHelper = {isValidCssColor};
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@ export function checkAppUrl() {
|
||||
if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
|
||||
return;
|
||||
}
|
||||
showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting.
|
||||
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`, 'warning');
|
||||
showGlobalErrorMessage(`The detected web site URL is "${appUrl}", it's unlikely matching the site config.
|
||||
Mismatched app.ini ROOT_URL or reverse proxy "Host/X-Forwarded-Proto" config might cause wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`, 'warning');
|
||||
}
|
||||
|
||||
export function checkAppUrlScheme() {
|
||||
|
||||
@@ -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<HTMLElement>('.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);
|
||||
});
|
||||
}
|
||||
|
||||
9
web_src/js/globals.d.ts
vendored
9
web_src/js/globals.d.ts
vendored
@@ -68,6 +68,15 @@ interface Window {
|
||||
turnstile: any,
|
||||
hcaptcha: any,
|
||||
|
||||
// Make IIFE private functions can be tested in unit tests, without exposing the IIFE module to global scope.
|
||||
// Otherwise, when using "export" in IIFE code, the compiled JS will inject global "var externalRenderHelper = ..."
|
||||
// which is not expected and may cause conflicts with other modules.
|
||||
testModules: {
|
||||
externalRenderHelper?: {
|
||||
isValidCssColor(s: string | null): boolean,
|
||||
}
|
||||
}
|
||||
|
||||
// do not add more properties here unless it is a must
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = '<div class="page-content"></div>';
|
||||
|
||||
@@ -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})`;
|
||||
|
||||
@@ -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<void>;
|
||||
};
|
||||
|
||||
export type FrontendRenderOptions = {
|
||||
container: HTMLElement;
|
||||
treePath: string;
|
||||
contentString(): string;
|
||||
contentBytes(): Uint8Array<ArrayBuffer>;
|
||||
};
|
||||
|
||||
export type FrontendRenderFunc = (opts: FrontendRenderOptions) => Promise<boolean>;
|
||||
|
||||
@@ -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<void> {
|
||||
// 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]);
|
||||
},
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user