mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 14:03:24 +09:00
Compare commits
26
Commits
ba4db8a2d9
...
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
@@ -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
|
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).
|
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
|
## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/v1.25.5) - 2026-03-10
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
// regexp is based on go-license, excluding README and NOTICE
|
// regexp is based on go-license, excluding README and NOTICE
|
||||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
// 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).*$`)
|
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||||
|
|
||||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||||
|
|||||||
@@ -41,10 +41,10 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; App name that shows in every page title
|
;; 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 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"
|
;; 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.
|
;; 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.
|
;; Name of cookie used to store authentication information.
|
||||||
;COOKIE_REMEMBER_NAME = gitea_incredible
|
;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 header name of user name, email, and full name
|
||||||
;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
|
;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
|
||||||
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
|
;REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
|
||||||
|
|||||||
@@ -926,6 +926,7 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
...playwright.configs['flat/recommended'],
|
...playwright.configs['flat/recommended'],
|
||||||
files: ['tests/e2e/**/*.test.ts'],
|
files: ['tests/e2e/**/*.test.ts'],
|
||||||
|
languageOptions: {globals: {...globals.nodeBuiltin, ...globals.browser}},
|
||||||
rules: {
|
rules: {
|
||||||
...playwright.configs['flat/recommended'].rules,
|
...playwright.configs['flat/recommended'].rules,
|
||||||
'playwright/expect-expect': [0],
|
'playwright/expect-expect': [0],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module code.gitea.io/gitea
|
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."
|
// 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:
|
// 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
|
code.gitea.io/sdk/gitea v0.24.1
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||||
connectrpc.com/connect v1.19.1
|
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/cache v0.2.1
|
||||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||||
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
|
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-co-op/gocron/v2 v2.19.1
|
||||||
github.com/go-enry/go-enry/v2 v2.9.5
|
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-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-ldap/ldap/v3 v3.4.13
|
||||||
github.com/go-redsync/redsync/v4 v4.16.0
|
github.com/go-redsync/redsync/v4 v4.16.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/go-webauthn/webauthn v0.16.1
|
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/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
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-ini/ini v1.67.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/go-webauthn/x v0.2.2 // 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/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
|||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
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 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
|
||||||
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
|
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-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M=
|
||||||
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/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 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||||
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
|
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=
|
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-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 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-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.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||||
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/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
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-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
@@ -64,7 +65,12 @@ func (key *PublicKey) AfterLoad() {
|
|||||||
|
|
||||||
// OmitEmail returns content of public key without email address.
|
// OmitEmail returns content of public key without email address.
|
||||||
func (key *PublicKey) OmitEmail() string {
|
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) {
|
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)
|
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)
|
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
||||||
|
|||||||
+83
-49
@@ -17,16 +17,43 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPullRequest_LoadAttributes(t *testing.T) {
|
func TestPullRequest(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
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})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||||
assert.NoError(t, pr.LoadAttributes(t.Context()))
|
assert.NoError(t, pr.LoadAttributes(t.Context()))
|
||||||
assert.NotNil(t, pr.Merger)
|
assert.NotNil(t, pr.Merger)
|
||||||
assert.Equal(t, pr.MergerID, pr.Merger.ID)
|
assert.Equal(t, pr.MergerID, pr.Merger.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequest_LoadIssue(t *testing.T) {
|
func testPullRequestLoadIssue(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||||
assert.NoError(t, pr.LoadIssue(t.Context()))
|
assert.NoError(t, pr.LoadIssue(t.Context()))
|
||||||
assert.NotNil(t, pr.Issue)
|
assert.NotNil(t, pr.Issue)
|
||||||
@@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) {
|
|||||||
assert.Equal(t, int64(2), pr.Issue.ID)
|
assert.Equal(t, int64(2), pr.Issue.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
func testPullRequestLoadBaseRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||||
assert.NotNil(t, pr.BaseRepo)
|
assert.NotNil(t, pr.BaseRepo)
|
||||||
@@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
|||||||
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
func testPullRequestLoadHeadRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||||
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||||
assert.NotNil(t, pr.HeadRepo)
|
assert.NotNil(t, pr.HeadRepo)
|
||||||
@@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
|||||||
|
|
||||||
// TODO TestNewPullRequest
|
// TODO TestNewPullRequest
|
||||||
|
|
||||||
func TestPullRequestsNewest(t *testing.T) {
|
func testPullRequestsNewest(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
Page: 1,
|
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
|
// Issue ID | Closed At. | Updated At
|
||||||
// 2 | 1707270001 | 1707270001
|
// 2 | 1707270001 | 1707270001
|
||||||
// 3 | 1707271000 | 1707279999
|
// 3 | 1707271000 | 1707279999
|
||||||
@@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
|||||||
{"recentclose", []int64{11, 3, 2}},
|
{"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")
|
_, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
|
_, 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) {
|
func testLoadRequestedReviewers(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
assert.NoError(t, pull.LoadIssue(t.Context()))
|
assert.NoError(t, pull.LoadIssue(t.Context()))
|
||||||
issue := pull.Issue
|
issue := pull.Issue
|
||||||
@@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
|||||||
assert.Empty(t, pull.RequestedReviewers)
|
assert.Empty(t, pull.RequestedReviewers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequestsOldest(t *testing.T) {
|
func testPullRequestsOldest(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
@@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUnmergedPullRequest(t *testing.T) {
|
func testGetUnmergedPullRequest(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), pr.ID)
|
assert.Equal(t, int64(2), pr.ID)
|
||||||
@@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) {
|
|||||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, exist)
|
assert.True(t, exist)
|
||||||
@@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
|||||||
assert.False(t, exist)
|
assert.False(t, exist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, prs, 1)
|
assert.Len(t, prs, 1)
|
||||||
@@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master")
|
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, prs, 1)
|
assert.Len(t, prs, 1)
|
||||||
@@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
|||||||
assert.Equal(t, "master", pr.BaseBranch)
|
assert.Equal(t, "master", pr.BaseBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPullRequestByIndex(t *testing.T) {
|
func testGetPullRequestByIndex(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2)
|
pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(1), pr.BaseRepoID)
|
assert.Equal(t, int64(1), pr.BaseRepoID)
|
||||||
@@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) {
|
|||||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPullRequestByID(t *testing.T) {
|
func testGetPullRequestByID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr, err := issues_model.GetPullRequestByID(t.Context(), 1)
|
pr, err := issues_model.GetPullRequestByID(t.Context(), 1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(1), pr.ID)
|
assert.Equal(t, int64(1), pr.ID)
|
||||||
@@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) {
|
|||||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPullRequestByIssueID(t *testing.T) {
|
func testGetPullRequestByIssueID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2)
|
pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), pr.IssueID)
|
assert.Equal(t, int64(2), pr.IssueID)
|
||||||
@@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) {
|
|||||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequest_UpdateCols(t *testing.T) {
|
func testPullRequestUpdateCols(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr := &issues_model.PullRequest{
|
pr := &issues_model.PullRequest{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
BaseBranch: "baseBranch",
|
BaseBranch: "baseBranch",
|
||||||
@@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) {
|
|||||||
|
|
||||||
// TODO TestAddTestPullRequestTask
|
// TODO TestAddTestPullRequestTask
|
||||||
|
|
||||||
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
func testPullRequestIsWorkInProgress(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
pr.LoadIssue(t.Context())
|
pr.LoadIssue(t.Context())
|
||||||
|
|
||||||
@@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
|||||||
assert.True(t, pr.IsWorkInProgress(t.Context()))
|
assert.True(t, pr.IsWorkInProgress(t.Context()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
pr.LoadIssue(t.Context())
|
pr.LoadIssue(t.Context())
|
||||||
|
|
||||||
@@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
|||||||
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context()))
|
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteOrphanedObjects(t *testing.T) {
|
func testDeleteOrphanedObjects(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{})
|
countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) {
|
|||||||
assert.Equal(t, countBefore, countAfter)
|
assert.Equal(t, countBefore, countAfter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseCodeOwnersLine(t *testing.T) {
|
func testParseCodeOwnersLine(t *testing.T) {
|
||||||
type CodeOwnerTest struct {
|
type CodeOwnerTest struct {
|
||||||
Line string
|
Line string
|
||||||
Tokens []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: `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 @org3`, Tokens: []string{`#path`, "@org3"}},
|
||||||
{Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@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 {
|
for _, g := range given {
|
||||||
@@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetApprovers(t *testing.T) {
|
func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
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})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
|
||||||
// Official reviews are already deduplicated. Allow unofficial reviews
|
// Official reviews are already deduplicated. Allow unofficial reviews
|
||||||
// to assert that there are no duplicated approvers.
|
// to assert that there are no duplicated approvers.
|
||||||
@@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) {
|
|||||||
assert.Equal(t, expected, approvers)
|
assert.Equal(t, expected, approvers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetPullRequestByMergedCommit(t *testing.T) {
|
func testGetPullRequestByMergedCommit(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
|
pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 1, pr.ID)
|
assert.EqualValues(t, 1, pr.ID)
|
||||||
@@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) {
|
|||||||
assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
|
assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrate_InsertPullRequests(t *testing.T) {
|
func testMigrateInsertPullRequests(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
reponame := "repo1"
|
reponame := "repo1"
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
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
|
// GetProperties gets all properties
|
||||||
func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) {
|
func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) {
|
||||||
pps := make([]*PackageProperty, 0, 10)
|
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
|
// GetPropertiesByName gets all properties with a specific name
|
||||||
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
|
func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) {
|
||||||
pps := make([]*PackageProperty, 0, 10)
|
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
|
// UpdateProperty updates a property
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ type RepoFileOptions struct {
|
|||||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
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
|
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||||
|
|
||||||
CurrentRefPath string // eg: "branch/main"
|
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
|
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 {
|
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,
|
"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)
|
rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true)
|
||||||
return rctx
|
return rctx
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,10 +103,20 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ValidateWorkflowContent(content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return events, nil
|
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(
|
func DetectWorkflows(
|
||||||
gitRepo *git.Repository,
|
gitRepo *git.Repository,
|
||||||
commit *git.Commit,
|
commit *git.Commit,
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestIsWorkflow(t *testing.T) {
|
||||||
oldDirs := setting.Actions.WorkflowDirs
|
defer test.MockVariableValue(&setting.Actions.WorkflowDirs)()
|
||||||
defer func() {
|
|
||||||
setting.Actions.WorkflowDirs = oldDirs
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -218,7 +228,7 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
assert.Len(t, evts, 1)
|
assert.Len(t, evts, 1)
|
||||||
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
|
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 {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
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.NoError(t, err)
|
||||||
assert.Len(t, evts, 1)
|
assert.Len(t, evts, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -13,63 +13,45 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var catFileBatchDebugWaitClose atomic.Int64
|
|
||||||
|
|
||||||
type catFileBatchCommunicator struct {
|
type catFileBatchCommunicator struct {
|
||||||
closeFunc func(err error)
|
closeFunc atomic.Pointer[func(err error)]
|
||||||
reqWriter io.Writer
|
reqWriter io.Writer
|
||||||
respReader *bufio.Reader
|
respReader *bufio.Reader
|
||||||
debugGitCmd *gitcmd.Command
|
debugGitCmd *gitcmd.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *catFileBatchCommunicator) Close() {
|
func (b *catFileBatchCommunicator) Close(err ...error) {
|
||||||
if b.closeFunc != nil {
|
if fn := b.closeFunc.Swap(nil); fn != nil {
|
||||||
b.closeFunc(nil)
|
(*fn)(util.OptionalArg(err))
|
||||||
b.closeFunc = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function
|
// 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) (ret *catFileBatchCommunicator) {
|
func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator {
|
||||||
ctx, ctxCancel := context.WithCancelCause(ctx)
|
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()
|
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{
|
ret := &catFileBatchCommunicator{
|
||||||
debugGitCmd: cmdCatFile,
|
debugGitCmd: cmdCatFile,
|
||||||
closeFunc: closeFunc,
|
|
||||||
reqWriter: stdinWriter,
|
reqWriter: stdinWriter,
|
||||||
respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations
|
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)
|
err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
|
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
|
// 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
|
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
|
||||||
closeFunc(err)
|
ret.Close(err)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +60,33 @@ func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFi
|
|||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
||||||
}
|
}
|
||||||
closeFunc(err)
|
ret.Close(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return ret
|
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
|
// catFileBatchParseInfoLine reads the header line from cat-file --batch
|
||||||
// We expect: <oid> SP <type> SP <size> LF
|
// We expect: <oid> SP <type> SP <size> LF
|
||||||
// then leaving the rest of the stream "<contents> LF" to be read
|
// then leaving the rest of the stream "<contents> LF" to be read
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
@@ -39,13 +37,22 @@ func testCatFileBatch(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
simulateQueryTerminated := func(pipeCloseDelay, pipeReadDelay time.Duration) (errRead error) {
|
simulateQueryTerminated := func(t *testing.T, errBeforePipeClose, errAfterPipeClose error) {
|
||||||
catFileBatchDebugWaitClose.Store(int64(pipeCloseDelay))
|
readError := func(t *testing.T, r io.Reader, expectedErr error) {
|
||||||
defer catFileBatchDebugWaitClose.Store(0)
|
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"))
|
batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer batch.Close()
|
defer batch.Close()
|
||||||
_, _ = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
_, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
var c *catFileBatchCommunicator
|
var c *catFileBatchCommunicator
|
||||||
switch b := batch.(type) {
|
switch b := batch.(type) {
|
||||||
case *catFileBatchLegacy:
|
case *catFileBatchLegacy:
|
||||||
@@ -58,24 +65,18 @@ func testCatFileBatch(t *testing.T) {
|
|||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
require.NotEqual(t, errBeforePipeClose == nil, errAfterPipeClose == nil, "must set exactly one of the expected errors")
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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) {
|
t.Run("QueryTerminated", func(t *testing.T) {
|
||||||
err := simulateQueryTerminated(0, 20*time.Millisecond)
|
simulateQueryTerminated(t, io.EOF, nil) // reader is faster
|
||||||
assert.ErrorIs(t, err, os.ErrClosed) // pipes are closed faster
|
simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster
|
||||||
err = simulateQueryTerminated(40*time.Millisecond, 20*time.Millisecond)
|
|
||||||
assert.ErrorIs(t, err, io.EOF) // reader is faster
|
|
||||||
})
|
})
|
||||||
|
|
||||||
batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
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
|
package json
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json" //nolint:depguard // this package wraps it
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDefaultJSONHandler() Interface {
|
func getDefaultJSONHandler() Interface {
|
||||||
return jsonGoccy{}
|
return jsonV1{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||||
|
|||||||
Vendored
+27
-1
@@ -21,7 +21,33 @@ import (
|
|||||||
|
|
||||||
// RegisterRenderers registers all supported third part renderers according settings
|
// RegisterRenderers registers all supported third part renderers according settings
|
||||||
func RegisterRenderers() {
|
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 {
|
for _, renderer := range setting.ExternalMarkupRenderers {
|
||||||
markup.RegisterRenderer(&Renderer{renderer})
|
markup.RegisterRenderer(&Renderer{renderer})
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+95
@@ -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
|
||||||
|
}
|
||||||
Vendored
-84
@@ -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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@@ -43,7 +44,8 @@ type WebThemeInterface interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StandalonePageOptions struct {
|
type StandalonePageOptions struct {
|
||||||
CurrentWebTheme WebThemeInterface
|
CurrentWebTheme WebThemeInterface
|
||||||
|
RenderQueryString string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderOptions struct {
|
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 {
|
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,
|
src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL,
|
||||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
url.PathEscape(ownerName),
|
||||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
url.PathEscape(repoName),
|
||||||
util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]),
|
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
|
||||||
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
|
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
|
||||||
)
|
)
|
||||||
var extraAttrs template.HTML
|
var extraAttrs template.HTML
|
||||||
if opts.ContentSandbox != "" {
|
if opts.ContentSandbox != "" {
|
||||||
extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, 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
|
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 {
|
if externalRender, ok := renderer.(ExternalRenderer); ok {
|
||||||
return externalRender.GetExternalRendererOptions(), true
|
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 {
|
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||||
var extraHeadHTML template.HTML
|
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 {
|
if ctx.RenderOptions.StandalonePageOptions == nil {
|
||||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
// 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
|
// 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()
|
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"
|
// "<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
|
// 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
|
ctx.usedByRender = true
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ func TestRenderIFrame(t *testing.T) {
|
|||||||
|
|
||||||
// the value is read from config RENDER_CONTENT_SANDBOX, empty means "disabled"
|
// the value is read from config RENDER_CONTENT_SANDBOX, empty means "disabled"
|
||||||
ret := render(ctx, ExternalRendererOptions{ContentSandbox: ""})
|
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"})
|
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"`
|
Keywords []string `json:"keywords,omitempty"`
|
||||||
RepositoryURL string `json:"repository_url,omitempty"`
|
RepositoryURL string `json:"repository_url,omitempty"`
|
||||||
License string `json:"license,omitempty"`
|
License string `json:"license,omitempty"`
|
||||||
|
LicenseURL string `json:"license_url,omitempty"`
|
||||||
Author Person `json:"author"`
|
Author Person `json:"author"`
|
||||||
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,8 @@ type SoftwareSourceCode struct {
|
|||||||
Keywords []string `json:"keywords,omitempty"`
|
Keywords []string `json:"keywords,omitempty"`
|
||||||
CodeRepository string `json:"codeRepository,omitempty"`
|
CodeRepository string `json:"codeRepository,omitempty"`
|
||||||
License string `json:"license,omitempty"`
|
License string `json:"license,omitempty"`
|
||||||
Author Person `json:"author"`
|
LicenseURL string `json:"licenseURL,omitempty"`
|
||||||
|
Author *Person `json:"author,omitempty"`
|
||||||
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
|
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
|
||||||
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
|
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 {
|
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Metadata.Description = ssc.Description
|
p.Metadata.Description = ssc.Description
|
||||||
p.Metadata.Keywords = ssc.Keywords
|
p.Metadata.Keywords = ssc.Keywords
|
||||||
p.Metadata.License = ssc.License
|
p.Metadata.License = ssc.License
|
||||||
author := Person{
|
p.Metadata.LicenseURL = ssc.LicenseURL
|
||||||
Name: ssc.Author.Name,
|
if ssc.Author != nil {
|
||||||
GivenName: ssc.Author.GivenName,
|
author := Person{
|
||||||
MiddleName: ssc.Author.MiddleName,
|
Name: ssc.Author.Name,
|
||||||
FamilyName: ssc.Author.FamilyName,
|
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
|
p.Metadata.RepositoryURL = ssc.CodeRepository
|
||||||
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
||||||
p.Metadata.RepositoryURL = ""
|
p.Metadata.RepositoryURL = ""
|
||||||
}
|
}
|
||||||
|
if !validation.IsValidURL(p.Metadata.LicenseURL) {
|
||||||
|
p.Metadata.LicenseURL = ""
|
||||||
|
}
|
||||||
|
|
||||||
p.RepositoryURLs = ssc.RepositoryURLs
|
p.RepositoryURLs = ssc.RepositoryURLs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
package swift
|
package swift
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -18,36 +19,24 @@ const (
|
|||||||
packageVersion = "1.0.1"
|
packageVersion = "1.0.1"
|
||||||
packageDescription = "Package Description"
|
packageDescription = "Package Description"
|
||||||
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
||||||
|
packageLicenseURL = "https://opensource.org/license/mit"
|
||||||
packageAuthor = "KN4CK3R"
|
packageAuthor = "KN4CK3R"
|
||||||
packageLicense = "MIT"
|
packageLicense = "MIT"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParsePackage(t *testing.T) {
|
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) {
|
t.Run("MissingManifestFile", func(t *testing.T) {
|
||||||
data := createArchive(map[string][]byte{"dummy.txt": {}})
|
data := test.WriteZipArchive(map[string]string{"dummy.txt": ""})
|
||||||
|
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||||
p, err := ParsePackage(data, data.Size(), nil)
|
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrMissingManifestFile)
|
assert.ErrorIs(t, err, ErrMissingManifestFile)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ManifestFileTooLarge", func(t *testing.T) {
|
t.Run("ManifestFileTooLarge", func(t *testing.T) {
|
||||||
data := createArchive(map[string][]byte{
|
data := test.WriteZipArchive(map[string]string{
|
||||||
"Package.swift": make([]byte, maxManifestFileSize+1),
|
"Package.swift": strings.Repeat("a", maxManifestFileSize+1),
|
||||||
})
|
})
|
||||||
|
p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil)
|
||||||
p, err := ParsePackage(data, data.Size(), nil)
|
|
||||||
assert.Nil(t, p)
|
assert.Nil(t, p)
|
||||||
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
|
assert.ErrorIs(t, err, ErrManifestFileTooLarge)
|
||||||
})
|
})
|
||||||
@@ -56,12 +45,12 @@ func TestParsePackage(t *testing.T) {
|
|||||||
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
content1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||||
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||||
|
|
||||||
data := createArchive(map[string][]byte{
|
data := test.WriteZipArchive(map[string]string{
|
||||||
"Package.swift": []byte(content1),
|
"Package.swift": content1,
|
||||||
"Package@swift-5.5.swift": []byte(content2),
|
"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.NotNil(t, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WithMetadata", func(t *testing.T) {
|
t.Run("WithMetadata", func(t *testing.T) {
|
||||||
data := createArchive(map[string][]byte{
|
data := test.WriteZipArchive(map[string]string{
|
||||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||||
})
|
})
|
||||||
|
|
||||||
p, err := ParsePackage(
|
p, err := ParsePackage(
|
||||||
data,
|
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||||
data.Size(),
|
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
|
||||||
)
|
)
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
assert.Equal(t, packageDescription, p.Metadata.Description)
|
assert.Equal(t, packageDescription, p.Metadata.Description)
|
||||||
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
|
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
|
||||||
assert.Equal(t, packageLicense, p.Metadata.License)
|
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.Name)
|
||||||
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
|
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
|
||||||
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
||||||
@@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("WithExplicitNameField", func(t *testing.T) {
|
t.Run("WithExplicitNameField", func(t *testing.T) {
|
||||||
data := createArchive(map[string][]byte{
|
data := test.WriteZipArchive(map[string]string{
|
||||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||||
})
|
})
|
||||||
|
|
||||||
authorName := "John Doe"
|
authorName := "John Doe"
|
||||||
p, err := ParsePackage(
|
p, err := ParsePackage(
|
||||||
data,
|
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||||
data.Size(),
|
|
||||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`),
|
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`),
|
||||||
)
|
)
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
@@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) {
|
|||||||
assert.Equal(t, "Doe", p.Metadata.Author.FamilyName)
|
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) {
|
t.Run("NameFieldGeneration", func(t *testing.T) {
|
||||||
data := createArchive(map[string][]byte{
|
data := test.WriteZipArchive(map[string]string{
|
||||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
"Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test with only individual name components - Name should be auto-generated
|
// Test with only individual name components - Name should be auto-generated
|
||||||
p, err := ParsePackage(
|
p, err := ParsePackage(
|
||||||
data,
|
bytes.NewReader(data.Bytes()), int64(data.Len()),
|
||||||
data.Size(),
|
|
||||||
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
|
strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`),
|
||||||
)
|
)
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ func parseManifest(data []byte) (map[string]string, map[string]string) {
|
|||||||
paths[key] = entry.File
|
paths[key] = entry.File
|
||||||
names[entry.File] = entry.Name
|
names[entry.File] = entry.Name
|
||||||
// Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css"
|
// 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 {
|
for _, css := range entry.CSS {
|
||||||
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
|
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
|
||||||
paths[cssKey] = css
|
paths[cssKey] = css
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var (
|
|||||||
ReverseProxyAuthEmail string
|
ReverseProxyAuthEmail string
|
||||||
ReverseProxyAuthFullName string
|
ReverseProxyAuthFullName string
|
||||||
ReverseProxyLimit int
|
ReverseProxyLimit int
|
||||||
|
ReverseProxyLogoutRedirect string
|
||||||
ReverseProxyTrustedProxies []string
|
ReverseProxyTrustedProxies []string
|
||||||
MinPasswordLength int
|
MinPasswordLength int
|
||||||
ImportLocalPaths bool
|
ImportLocalPaths bool
|
||||||
@@ -124,6 +125,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
|||||||
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
|
ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME")
|
||||||
|
|
||||||
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
|
ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1)
|
||||||
|
ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String()
|
||||||
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
|
ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",")
|
||||||
if len(ReverseProxyTrustedProxies) == 0 {
|
if len(ReverseProxyTrustedProxies) == 0 {
|
||||||
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
|
ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) {
|
|||||||
if HasInstallLock(rootCfg) {
|
if HasInstallLock(rootCfg) {
|
||||||
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
currentUser, match := IsRunUserMatchCurrentUser(RunUser)
|
||||||
if !match {
|
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 (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"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)
|
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 {
|
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{}
|
buf := &bytes.Buffer{}
|
||||||
var cw io.WriteCloser
|
var cw io.WriteCloser
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/glob"
|
"code.gitea.io/gitea/modules/glob"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"gitea.com/go-chi/binding"
|
"gitea.com/go-chi/binding"
|
||||||
@@ -31,8 +33,23 @@ const (
|
|||||||
ErrInvalidBadgeSlug = "InvalidBadgeSlug"
|
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
|
// AddBindingRules adds additional binding rules
|
||||||
func AddBindingRules() {
|
func AddBindingRules() {
|
||||||
|
binding.JSONProvider = jsonProvider{}
|
||||||
addGitRefNameBindingRule()
|
addGitRefNameBindingRule()
|
||||||
addValidURLListBindingRule()
|
addValidURLListBindingRule()
|
||||||
addValidURLBindingRule()
|
addValidURLBindingRule()
|
||||||
|
|||||||
@@ -269,7 +269,7 @@
|
|||||||
"install.lfs_path": "Git LFS Root Path",
|
"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.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": "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": "Server Domain",
|
||||||
"install.domain_helper": "Domain or host address for the server.",
|
"install.domain_helper": "Domain or host address for the server.",
|
||||||
"install.ssh_port": "SSH Server Port",
|
"install.ssh_port": "SSH Server Port",
|
||||||
@@ -316,7 +316,6 @@
|
|||||||
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
|
"install.invalid_db_table": "The database table \"%s\" is invalid: %v",
|
||||||
"install.invalid_repo_path": "The repository root path 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.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.internal_token_failed": "Failed to generate internal token: %v",
|
||||||
"install.secret_key_failed": "Failed to generate secret key: %v",
|
"install.secret_key_failed": "Failed to generate secret key: %v",
|
||||||
"install.save_config_failed": "Failed to save configuration: %v",
|
"install.save_config_failed": "Failed to save configuration: %v",
|
||||||
|
|||||||
+2
-3
@@ -59,7 +59,7 @@
|
|||||||
"pdfobject": "2.3.1",
|
"pdfobject": "2.3.1",
|
||||||
"perfect-debounce": "2.1.0",
|
"perfect-debounce": "2.1.0",
|
||||||
"postcss": "8.5.8",
|
"postcss": "8.5.8",
|
||||||
"rollup-plugin-license": "3.7.0",
|
"rolldown-license-plugin": "2.2.0",
|
||||||
"sortablejs": "1.15.7",
|
"sortablejs": "1.15.7",
|
||||||
"swagger-ui-dist": "5.32.1",
|
"swagger-ui-dist": "5.32.1",
|
||||||
"tailwindcss": "3.4.19",
|
"tailwindcss": "3.4.19",
|
||||||
@@ -73,8 +73,7 @@
|
|||||||
"vite-string-plugin": "2.0.2",
|
"vite-string-plugin": "2.0.2",
|
||||||
"vue": "3.5.31",
|
"vue": "3.5.31",
|
||||||
"vue-bar-graph": "2.2.0",
|
"vue-bar-graph": "2.2.0",
|
||||||
"vue-chartjs": "5.3.3",
|
"vue-chartjs": "5.3.3"
|
||||||
"wrap-ansi": "10.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint-community/eslint-plugin-eslint-comments": "4.7.1",
|
"@eslint-community/eslint-plugin-eslint-comments": "4.7.1",
|
||||||
|
|||||||
Generated
+8
-361
@@ -185,9 +185,9 @@ importers:
|
|||||||
postcss:
|
postcss:
|
||||||
specifier: 8.5.8
|
specifier: 8.5.8
|
||||||
version: 8.5.8
|
version: 8.5.8
|
||||||
rollup-plugin-license:
|
rolldown-license-plugin:
|
||||||
specifier: 3.7.0
|
specifier: 2.2.0
|
||||||
version: 3.7.0(picomatch@4.0.4)(rollup@4.60.1)
|
version: 2.2.0
|
||||||
sortablejs:
|
sortablejs:
|
||||||
specifier: 1.15.7
|
specifier: 1.15.7
|
||||||
version: 1.15.7
|
version: 1.15.7
|
||||||
@@ -230,9 +230,6 @@ importers:
|
|||||||
vue-chartjs:
|
vue-chartjs:
|
||||||
specifier: 5.3.3
|
specifier: 5.3.3
|
||||||
version: 5.3.3(chart.js@4.5.1)(vue@3.5.31(typescript@6.0.2))
|
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:
|
devDependencies:
|
||||||
'@eslint-community/eslint-plugin-eslint-comments':
|
'@eslint-community/eslint-plugin-eslint-comments':
|
||||||
specifier: 4.7.1
|
specifier: 4.7.1
|
||||||
@@ -1235,144 +1232,6 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.0-rc.2':
|
'@rolldown/pluginutils@1.0.0-rc.2':
|
||||||
resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
|
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':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
@@ -1892,10 +1751,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
ansi-styles@6.2.3:
|
|
||||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
ansi_up@6.0.6:
|
ansi_up@6.0.6:
|
||||||
resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==}
|
resolution: {integrity: sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==}
|
||||||
|
|
||||||
@@ -1916,10 +1771,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
asciinema-player@3.15.1:
|
||||||
resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==}
|
resolution: {integrity: sha512-agVYeNlPxthLyAb92l9AS7ypW0uhesqOuQzyR58Q4Sj+MvesQztZBgx86lHqNJkB8rQ6EP0LeA9czGytQUBpYw==}
|
||||||
|
|
||||||
@@ -2113,9 +1964,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==}
|
resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
commenting@1.1.0:
|
|
||||||
resolution: {integrity: sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==}
|
|
||||||
|
|
||||||
compare-versions@6.1.1:
|
compare-versions@6.1.1:
|
||||||
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
||||||
|
|
||||||
@@ -3385,9 +3233,6 @@ packages:
|
|||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
moment@2.30.1:
|
|
||||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
|
||||||
|
|
||||||
moo@0.5.3:
|
moo@0.5.3:
|
||||||
resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==}
|
resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==}
|
||||||
|
|
||||||
@@ -3478,10 +3323,6 @@ packages:
|
|||||||
package-manager-detector@1.6.0:
|
package-manager-detector@1.6.0:
|
||||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
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:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3706,22 +3547,14 @@ packages:
|
|||||||
robust-predicates@3.0.3:
|
robust-predicates@3.0.3:
|
||||||
resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
|
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:
|
rolldown@1.0.0-rc.12:
|
||||||
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
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:
|
roughjs@4.6.6:
|
||||||
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
|
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
|
||||||
|
|
||||||
@@ -3805,27 +3638,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
spectral-cli-bundle@1.0.7:
|
||||||
resolution: {integrity: sha512-vIUC0nwv9tYxWV1xHdR3CTVDOEEtLKaDCcQpARZgO0Db7VmSpSWJ4xrnVPNSmO59hBtGwW2CVzHf0OimJBaKAA==}
|
resolution: {integrity: sha512-vIUC0nwv9tYxWV1xHdR3CTVDOEEtLKaDCcQpARZgO0Db7VmSpSWJ4xrnVPNSmO59hBtGwW2CVzHf0OimJBaKAA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -4256,10 +4068,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
write-file-atomic@7.0.1:
|
||||||
resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
|
resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==}
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
engines: {node: ^20.17.0 || >=22.9.0}
|
||||||
@@ -5205,81 +5013,6 @@ snapshots:
|
|||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-rc.2': {}
|
'@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': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@scarf/scarf@1.4.0': {}
|
'@scarf/scarf@1.4.0': {}
|
||||||
@@ -5925,8 +5658,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
|
|
||||||
ansi-styles@6.2.3: {}
|
|
||||||
|
|
||||||
ansi_up@6.0.6: {}
|
ansi_up@6.0.6: {}
|
||||||
|
|
||||||
any-promise@1.3.0: {}
|
any-promise@1.3.0: {}
|
||||||
@@ -5942,8 +5673,6 @@ snapshots:
|
|||||||
|
|
||||||
aria-query@5.3.2: {}
|
aria-query@5.3.2: {}
|
||||||
|
|
||||||
array-find-index@1.0.2: {}
|
|
||||||
|
|
||||||
asciinema-player@3.15.1:
|
asciinema-player@3.15.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.2
|
'@babel/runtime': 7.29.2
|
||||||
@@ -6112,8 +5841,6 @@ snapshots:
|
|||||||
|
|
||||||
comment-parser@1.4.6: {}
|
comment-parser@1.4.6: {}
|
||||||
|
|
||||||
commenting@1.1.0: {}
|
|
||||||
|
|
||||||
compare-versions@6.1.1: {}
|
compare-versions@6.1.1: {}
|
||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
@@ -7556,8 +7283,6 @@ snapshots:
|
|||||||
pkg-types: 1.3.1
|
pkg-types: 1.3.1
|
||||||
ufo: 1.6.3
|
ufo: 1.6.3
|
||||||
|
|
||||||
moment@2.30.1: {}
|
|
||||||
|
|
||||||
moo@0.5.3: {}
|
moo@0.5.3: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
@@ -7627,8 +7352,6 @@ snapshots:
|
|||||||
|
|
||||||
package-manager-detector@1.6.0: {}
|
package-manager-detector@1.6.0: {}
|
||||||
|
|
||||||
package-name-regex@2.0.6: {}
|
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -7815,6 +7538,8 @@ snapshots:
|
|||||||
|
|
||||||
robust-predicates@3.0.3: {}
|
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):
|
rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.122.0
|
'@oxc-project/types': 0.122.0
|
||||||
@@ -7839,51 +7564,6 @@ snapshots:
|
|||||||
- '@emnapi/core'
|
- '@emnapi/core'
|
||||||
- '@emnapi/runtime'
|
- '@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:
|
roughjs@4.6.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
hachure-fill: 0.5.2
|
hachure-fill: 0.5.2
|
||||||
@@ -7958,33 +7638,6 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
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:
|
spectral-cli-bundle@1.0.7:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
@@ -8453,12 +8106,6 @@ snapshots:
|
|||||||
|
|
||||||
word-wrap@1.2.5: {}
|
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:
|
write-file-atomic@7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|||||||
@@ -198,6 +198,23 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
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{})
|
setResponseHeaders(ctx.Resp, &headers{})
|
||||||
|
|
||||||
@@ -220,18 +237,14 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||||||
Keywords: metadata.Keywords,
|
Keywords: metadata.Keywords,
|
||||||
CodeRepository: metadata.RepositoryURL,
|
CodeRepository: metadata.RepositoryURL,
|
||||||
License: metadata.License,
|
License: metadata.License,
|
||||||
|
LicenseURL: metadata.LicenseURL,
|
||||||
|
Author: author,
|
||||||
ProgrammingLanguage: swift_module.ProgrammingLanguage{
|
ProgrammingLanguage: swift_module.ProgrammingLanguage{
|
||||||
Type: "ComputerLanguage",
|
Type: "ComputerLanguage",
|
||||||
Name: "Swift",
|
Name: "Swift",
|
||||||
URL: "https://swift.org",
|
URL: "https://swift.org",
|
||||||
},
|
},
|
||||||
Author: swift_module.Person{
|
RepositoryURLs: repositoryURLs,
|
||||||
Type: "Person",
|
|
||||||
Name: metadata.Author.String(),
|
|
||||||
GivenName: metadata.Author.GivenName,
|
|
||||||
MiddleName: metadata.Author.MiddleName,
|
|
||||||
FamilyName: metadata.Author.FamilyName,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) {
|
|||||||
|
|
||||||
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
|
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
|
||||||
userCache := make(map[string]*user_model.User)
|
userCache := make(map[string]*user_model.User)
|
||||||
|
|
||||||
for i := 0; i < len(compareInfo.Commits); i++ {
|
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{
|
convert.ToCommitOptions{
|
||||||
Stat: true,
|
Stat: true,
|
||||||
Verification: verification,
|
Verification: verification,
|
||||||
Files: files,
|
Files: files,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@@ -1429,7 +1430,11 @@ func GetPullRequestCommits(ctx *context.APIContext) {
|
|||||||
} else {
|
} else {
|
||||||
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
|
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)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
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.
|
// 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",
|
// 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.
|
// then frontend needs this delegate to redirect to the new location with hash correctly.
|
||||||
redirect := req.PostFormValue("redirect")
|
redirect := req.FormValue("redirect")
|
||||||
if !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) {
|
if req.Method != http.MethodPost || !httplib.IsCurrentGiteaSiteURL(req.Context(), redirect) {
|
||||||
resp.WriteHeader(http.StatusBadRequest)
|
http.Error(resp, "Bad Request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.Header().Add("Location", redirect)
|
// no OpenRedirect, the "redirect" is validated by "IsCurrentGiteaSiteURL" above
|
||||||
|
resp.Header().Set("Location", redirect)
|
||||||
resp.WriteHeader(http.StatusSeeOther)
|
resp.WriteHeader(http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/user"
|
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
@@ -87,15 +86,7 @@ func Install(ctx *context.Context) {
|
|||||||
form.AppName = setting.AppName
|
form.AppName = setting.AppName
|
||||||
form.RepoRootPath = setting.RepoRootPath
|
form.RepoRootPath = setting.RepoRootPath
|
||||||
form.LFSRootPath = setting.LFS.Storage.Path
|
form.LFSRootPath = setting.LFS.Storage.Path
|
||||||
|
form.RunUser = setting.RunUser
|
||||||
// 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.Domain = setting.Domain
|
form.Domain = setting.Domain
|
||||||
form.SSHPort = setting.SSH.Port
|
form.SSHPort = setting.SSH.Port
|
||||||
form.HTTPPort = setting.HTTPPort
|
form.HTTPPort = setting.HTTPPort
|
||||||
@@ -272,13 +263,6 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
return
|
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.
|
// Check logic loophole between disable self-registration and no admin account.
|
||||||
if form.DisableRegistration && len(form.AdminName) == 0 {
|
if form.DisableRegistration && len(form.AdminName) == 0 {
|
||||||
ctx.Data["Err_Services"] = true
|
ctx.Data["Err_Services"] = true
|
||||||
|
|||||||
@@ -493,12 +493,17 @@ func SignOut(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildSignOutRedirectURL(ctx *context.Context) string {
|
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 ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 {
|
||||||
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
|
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
|
||||||
return 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 + "/"
|
return setting.AppSubURL + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
|||||||
workflows = append(workflows, workflow)
|
workflows = append(workflows, workflow)
|
||||||
continue
|
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
|
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.
|
// 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
|
hasJobWithoutNeeds := false
|
||||||
@@ -315,6 +320,10 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
|||||||
if !job.Status.IsWaiting() {
|
if !job.Status.IsWaiting() {
|
||||||
continue
|
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
|
hasOnlineRunner := false
|
||||||
for _, runner := range runners {
|
for _, runner := range runners {
|
||||||
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
|
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ func RenderFile(ctx *context.Context) {
|
|||||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||||
}).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{
|
}).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)
|
renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -450,12 +450,21 @@ func MatrixHooksEditPost(ctx *context.Context) {
|
|||||||
editWebhook(ctx, matrixHookParams(ctx))
|
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 {
|
func matrixHookParams(ctx *context.Context) webhookParams {
|
||||||
form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
|
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{
|
return webhookParams{
|
||||||
Type: webhook_module.MATRIX,
|
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,
|
ContentType: webhook.ContentTypeJSON,
|
||||||
HTTPMethod: http.MethodPut,
|
HTTPMethod: http.MethodPut,
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookForm: form.WebhookForm,
|
||||||
|
|||||||
@@ -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/git/attribute"
|
||||||
"code.gitea.io/gitea/modules/highlight"
|
"code.gitea.io/gitea/modules/highlight"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func prepareLatestCommitInfo(ctx *context.Context) bool {
|
func prepareLatestCommitInfo(ctx *context.Context) bool {
|
||||||
@@ -78,14 +77,17 @@ func handleFileViewRenderMarkup(ctx *context.Context, prefetchBuf []byte, utf8Re
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader)
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, utf8Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Render", err)
|
ctx.ServerError("Render", err)
|
||||||
return true
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,8 +186,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("actions.GetContentFromEntry: %v", err)
|
log.Error("actions.GetContentFromEntry: %v", err)
|
||||||
}
|
}
|
||||||
_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
|
if workFlowErr := actions.ValidateWorkflowContent(content); workFlowErr != nil {
|
||||||
if workFlowErr != nil {
|
|
||||||
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
|
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
|
||||||
}
|
}
|
||||||
} else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
|
} 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:
|
case fInfo.blobOrLfsSize >= setting.UI.MaxDisplayFileSize:
|
||||||
ctx.Data["IsFileTooLarge"] = true
|
ctx.Data["IsFileTooLarge"] = true
|
||||||
case handleFileViewRenderMarkup(ctx, buf, contentReader):
|
case handleFileViewRenderMarkup(ctx, buf, contentReader):
|
||||||
// it also sets ctx.Data["FileContent"] and more
|
|
||||||
ctx.Data["IsMarkup"] = true
|
|
||||||
case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader):
|
case handleFileViewRenderSource(ctx, attrs, fInfo, contentReader):
|
||||||
// it also sets ctx.Data["FileContent"] and more
|
// it also sets ctx.Data["FileContent"] and more
|
||||||
ctx.Data["IsDisplayingSource"] = true
|
ctx.Data["IsDisplayingSource"] = true
|
||||||
|
|||||||
@@ -195,16 +195,16 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
|||||||
}).WithRelativePath(readmeFullPath)
|
}).WithRelativePath(readmeFullPath)
|
||||||
renderer := rctx.DetectMarkupRenderer(buf)
|
renderer := rctx.DetectMarkupRenderer(buf)
|
||||||
if renderer != nil {
|
if renderer != nil {
|
||||||
ctx.Data["IsMarkup"] = true
|
ctx.Data["RenderAsMarkup"] = "markup-inplace"
|
||||||
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
ctx.Data["MarkupType"] = rctx.RenderOptions.MarkupType
|
||||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
|
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRenderToHTML(ctx, rctx, renderer, rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
|
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
|
ctx.Data["IsPlainText"] = true
|
||||||
content, err := io.ReadAll(rd)
|
content, err := io.ReadAll(rd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -655,6 +655,9 @@ func ShowSSHKeys(ctx *context.Context) {
|
|||||||
// "authorized_keys" file format: "#" followed by comment line per key
|
// "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")
|
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 {
|
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(keys[i].OmitEmail())
|
||||||
buf.WriteString("\n")
|
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) {
|
func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (jobs, updatedJobs []*actions_model.ActionRunJob, err error) {
|
||||||
checkedConcurrencyGroup := make(container.Set[string])
|
checkedConcurrencyGroup := make(container.Set[string])
|
||||||
|
|
||||||
// check run (workflow-level) concurrency
|
collect := func(concurrencyGroup string) error {
|
||||||
if run.ConcurrencyGroup != "" {
|
concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, concurrencyGroup)
|
||||||
concurrentRun, err := findBlockedRunByConcurrency(ctx, run.RepoID, run.ConcurrencyGroup)
|
|
||||||
if err != nil {
|
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 {
|
if concurrentRun != nil && !concurrentRun.NeedApproval {
|
||||||
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
js, ujs, err := checkJobsOfRun(ctx, concurrentRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return err
|
||||||
}
|
}
|
||||||
jobs = append(jobs, js...)
|
jobs = append(jobs, js...)
|
||||||
updatedJobs = append(updatedJobs, ujs...)
|
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
|
// check job concurrency
|
||||||
@@ -177,22 +184,12 @@ func checkRunConcurrency(ctx context.Context, run *actions_model.ActionRun) (job
|
|||||||
if !job.Status.IsDone() {
|
if !job.Status.IsDone() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if job.ConcurrencyGroup == "" && checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) {
|
if job.ConcurrencyGroup == "" || checkedConcurrencyGroup.Contains(job.ConcurrencyGroup) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
concurrentRun, err := findBlockedRunByConcurrency(ctx, job.RepoID, job.ConcurrencyGroup)
|
if err := collect(job.ConcurrencyGroup); err != nil {
|
||||||
if err != nil {
|
return nil, nil, err
|
||||||
return nil, nil, 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
|
|
||||||
}
|
|
||||||
jobs = append(jobs, js...)
|
|
||||||
updatedJobs = append(updatedJobs, ujs...)
|
|
||||||
}
|
|
||||||
checkedConcurrencyGroup.Add(job.ConcurrencyGroup)
|
|
||||||
}
|
}
|
||||||
return jobs, updatedJobs, nil
|
return jobs, updatedJobs, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
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"
|
"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"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
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/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/webhook"
|
"code.gitea.io/gitea/services/webhook"
|
||||||
@@ -523,16 +525,49 @@ func (f *InitializeLabelsForm) Validate(req *http.Request, errs binding.Errors)
|
|||||||
type MergePullRequestForm struct {
|
type MergePullRequestForm struct {
|
||||||
// required: true
|
// required: true
|
||||||
// enum: ["merge","rebase","rebase-merge","squash","fast-forward-only","manually-merged"]
|
// 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)"`
|
Do string `json:"do" binding:"Required;In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged)"`
|
||||||
MergeTitleField string
|
MergeTitleField string `json:"merge_title_field,omitempty"`
|
||||||
MergeMessageField string
|
MergeMessageField string `json:"merge_message_field,omitempty"`
|
||||||
MergeCommitID string // only used for manually-merged
|
MergeCommitID string `json:"merge_commit_id,omitempty"` // only used for manually-merged
|
||||||
HeadCommitID string `json:"head_commit_id,omitempty"`
|
HeadCommitID string `json:"head_commit_id,omitempty"`
|
||||||
ForceMerge bool `json:"force_merge,omitempty"`
|
ForceMerge bool `json:"force_merge,omitempty"`
|
||||||
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
||||||
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,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
|
// Validate validates the fields
|
||||||
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||||
ctx := context.GetValidateContext(req)
|
ctx := context.GetValidateContext(req)
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ package forms
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
||||||
@@ -37,3 +40,48 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
|||||||
assert.Equal(t, v.expected, v.form.HasEmptyContent())
|
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`}`];
|
return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
extend: {
|
||||||
|
zIndex: {'1': '1'},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
plugin(({addUtilities}) => {
|
plugin(({addUtilities}) => {
|
||||||
|
|||||||
@@ -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}}">
|
<input id="lfs_root_path" name="lfs_root_path" value="{{.lfs_root_path}}">
|
||||||
<span class="help">{{ctx.Locale.Tr "install.lfs_path_helper"}}</span>
|
<span class="help">{{ctx.Locale.Tr "install.lfs_path_helper"}}</span>
|
||||||
</div>
|
</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>
|
<label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label>
|
||||||
<input id="run_user" name="run_user" value="{{.run_user}}" readonly>
|
<input id="run_user" name="run_user" value="{{.run_user}}" readonly>
|
||||||
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
|
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
{{$isMember := .IsOrganizationMember}}
|
{{$isMember := .IsOrganizationMember}}
|
||||||
{{range .Members}}
|
{{range .Members}}
|
||||||
{{if or $isMember (call $.IsPublicMember .ID)}}
|
{{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}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<div class="ui right">
|
<div class="ui right">
|
||||||
{{if .Team.IsMember ctx $.SignedUser.ID}}
|
{{if .Team.IsMember ctx $.SignedUser.ID}}
|
||||||
<form>
|
<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-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
|
||||||
data-name="{{.Team.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
data-name="{{.Team.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
||||||
</form>
|
</form>
|
||||||
{{else if .IsOrganizationOwner}}
|
{{else if .IsOrganizationOwner}}
|
||||||
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join">
|
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join">
|
||||||
<input type="hidden" name="page" value="team">
|
<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>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,23 +17,23 @@
|
|||||||
<div class="ui top attached header">
|
<div class="ui top attached header">
|
||||||
<a class="tw-text-text" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
|
<a class="tw-text-text" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
|
||||||
<div class="ui right">
|
<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}}
|
{{if .IsMember ctx $.SignedUser.ID}}
|
||||||
<form>
|
<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-url="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
|
||||||
data-name="{{.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
data-name="{{.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
|
||||||
</form>
|
</form>
|
||||||
{{else if $.IsOrganizationOwner}}
|
{{else if $.IsOrganizationOwner}}
|
||||||
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/join">
|
<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>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment members">
|
<div class="ui attached segment members">
|
||||||
{{range .Members}}
|
{{range .Members}}
|
||||||
{{template "shared/user/avatarlink" dict "user" .}}
|
{{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="ui bottom attached header">
|
<div class="ui bottom attached header">
|
||||||
|
|||||||
@@ -25,49 +25,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-container-main">
|
<div class="flex-container-main">
|
||||||
<div class="ui top attached header tw-flex tw-items-center tw-justify-between">
|
<div class="ui top attached header flex-text-block tw-flex-wrap 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>
|
<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 tw-flex tw-items-center tw-m-0">
|
<div class="ui secondary filter menu flex-text-block tw-m-0">
|
||||||
<!-- Actor -->
|
<!-- Actor -->
|
||||||
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
|
||||||
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div class="ui icon search input">
|
<div class="ui icon search input">
|
||||||
<i class="icon">{{svg "octicon-search"}}</i>
|
<i class="icon">{{svg "octicon-search"}}</i>
|
||||||
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.actor"}}">
|
||||||
</div>
|
</div>
|
||||||
<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
|
<a class="item{{if not $.CurActor}} active{{end}}" href="?workflow={{$.CurWorkflow}}&status={{$.CurStatus}}&actor=0">
|
||||||
{{ctx.Locale.Tr "actions.runs.actors_no_select"}}
|
{{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}}
|
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{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>
|
</div>
|
||||||
</div>
|
<!-- Status -->
|
||||||
<!-- Status -->
|
<div class="ui dropdown jump item">
|
||||||
<div class="ui dropdown jump item">
|
<span class="text">{{ctx.Locale.Tr "actions.runs.status"}}</span>
|
||||||
<span class="text">{{ctx.Locale.Tr "actions.runs.status"}}</span>
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
<div class="menu">
|
||||||
<div class="menu">
|
<div class="ui icon search input">
|
||||||
<div class="ui icon search input">
|
<i class="icon">{{svg "octicon-search"}}</i>
|
||||||
<i class="icon">{{svg "octicon-search"}}</i>
|
<input type="text" placeholder="{{ctx.Locale.Tr "actions.runs.status"}}">
|
||||||
<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>
|
</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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .AllowDisableOrEnableWorkflow}}
|
{{if .AllowDisableOrEnableWorkflow}}
|
||||||
<button class="ui jump dropdown btn interact-bg tw-p-2">
|
<button class="ui jump dropdown btn interact-bg tw-p-2">
|
||||||
{{svg "octicon-kebab-horizontal"}}
|
{{svg "octicon-kebab-horizontal"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,13 @@
|
|||||||
{{template "repo/actions/status" (dict "status" $run.Status.String)}}
|
{{template "repo/actions/status" (dict "status" $run.Status.String)}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-item-main">
|
<div class="flex-item-main">
|
||||||
<a class="flex-item-title" title="{{$run.Title}}" href="{{$run.Link}}">
|
<span class="flex-item-title" title="{{$run.Title}}">
|
||||||
{{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
|
{{if $run.Title}}
|
||||||
</a>
|
{{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">
|
<div class="flex-item-body">
|
||||||
<span><b>{{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}</b>:</span>
|
<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">
|
{{/* "z-index" is used to maintain continuous attached styling and keep the colored border-bottom visible (pre-existing fomantic issue with negative margins) */}}
|
||||||
<span class="ui text middle">{{ctx.Locale.Tr "actions.workflow.has_workflow_dispatch"}}</span>
|
<div class="ui blue info attached message flex-text-block tw-flex-wrap tw-justify-between tw-z-1">
|
||||||
<button class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
|
<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>
|
||||||
<div id="runWorkflowDispatchModal" class="ui tiny modal">
|
<div id="runWorkflowDispatchModal" class="ui tiny modal">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
@@ -45,10 +45,8 @@
|
|||||||
<div class="blame-avatar">
|
<div class="blame-avatar">
|
||||||
{{$row.Avatar}}
|
{{$row.Avatar}}
|
||||||
</div>
|
</div>
|
||||||
<div class="blame-message">
|
<div class="blame-message muted-links" title="{{$row.CommitMessage}}">
|
||||||
<a class="suppressed tw-text-text" href="{{$row.CommitURL}}" title="{{$row.CommitMessage}}">
|
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $row.CommitMessage $row.CommitURL $.Repository}}
|
||||||
{{$row.CommitMessage}}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="blame-time not-mobile">
|
<div class="blame-time not-mobile">
|
||||||
{{$row.CommitSince}}
|
{{$row.CommitSince}}
|
||||||
|
|||||||
@@ -13,22 +13,16 @@
|
|||||||
{{ctx.Locale.Tr "action.compare_commits_general"}}
|
{{ctx.Locale.Tr "action.compare_commits_general"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</h2>
|
</h2>
|
||||||
{{$BaseCompareName := $.BaseName -}}
|
{{$BaseCompareName := $.Repository.FullName -}}
|
||||||
{{- $HeadCompareName := $.HeadRepo.OwnerName -}}
|
{{$HeadCompareName := $.HeadRepo.FullName -}}
|
||||||
{{- if and (eq $.BaseName $.HeadRepo.OwnerName) (ne $.Repository.Name $.HeadRepo.Name) -}}
|
{{$OwnForkCompareName := "" -}}
|
||||||
{{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}}
|
{{if $.OwnForkRepo -}}
|
||||||
{{- end -}}
|
{{$OwnForkCompareName = $.OwnForkRepo.FullName -}}
|
||||||
{{- $OwnForkCompareName := "" -}}
|
{{end -}}
|
||||||
{{- if .OwnForkRepo -}}
|
{{$RootRepoCompareName := "" -}}
|
||||||
{{- $OwnForkCompareName = .OwnForkRepo.OwnerName -}}
|
{{if $.RootRepo -}}
|
||||||
{{- end -}}
|
{{$RootRepoCompareName = $.RootRepo.FullName -}}
|
||||||
{{- $RootRepoCompareName := "" -}}
|
{{end -}}
|
||||||
{{- if .RootRepo -}}
|
|
||||||
{{- $RootRepoCompareName = .RootRepo.OwnerName -}}
|
|
||||||
{{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}}
|
|
||||||
{{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
<div class="ui segment choose branch">
|
<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>
|
<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" .}}
|
{{template "repo/code/upstream_diverging_info" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/view_list" .}}
|
{{template "repo/view_list" .}}
|
||||||
{{if and .ReadmeExist (or .IsMarkup .IsPlainText)}}
|
{{if and .ReadmeExist (or .RenderAsMarkup .IsPlainText)}}
|
||||||
{{template "repo/view_file" .}}
|
{{template "repo/view_file" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -40,7 +40,9 @@
|
|||||||
{{if .IsRepresentableAsText}}
|
{{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>
|
<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}}
|
{{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>
|
<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>
|
</div>
|
||||||
{{if not .ReadmeInList}}
|
{{if not .ReadmeInList}}
|
||||||
<div class="ui buttons tw-mr-1">
|
<div class="ui buttons tw-mr-1">
|
||||||
@@ -90,15 +92,15 @@
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="ui bottom attached table unstackable segment">
|
<div class="ui bottom attached table unstackable segment">
|
||||||
{{if not .IsMarkup}}
|
{{if not .RenderAsMarkup}}
|
||||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||||
{{end}}
|
{{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}}
|
{{if .IsFileTooLarge}}
|
||||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||||
{{else if not .FileSize}}
|
{{else if not .FileSize}}
|
||||||
{{template "shared/fileisempty"}}
|
{{template "shared/fileisempty"}}
|
||||||
{{else if .IsMarkup}}
|
{{else if .RenderAsMarkup}}
|
||||||
{{.FileContent}}
|
{{.FileContent}}
|
||||||
{{else if .IsPlainText}}
|
{{else if .IsPlainText}}
|
||||||
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
|
<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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Gitea API</title>
|
<title>Gitea API</title>
|
||||||
{{/* HINT: SWAGGER-OPENAPI-VIEWER: another place is "modules/markup/external/openapi.go" */}}
|
|
||||||
<link rel="stylesheet" href="{{ctx.CurrentWebTheme.PublicAssetURI}}">
|
<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"}}">
|
<link rel="stylesheet" href="{{AssetURI "css/swagger.css"}}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+20
-16
@@ -26771,10 +26771,14 @@
|
|||||||
"description": "MergePullRequestForm form for merging Pull Request",
|
"description": "MergePullRequestForm form for merging Pull Request",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"Do"
|
"do"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"Do": {
|
"delete_branch_after_merge": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "DeleteBranchAfterMerge"
|
||||||
|
},
|
||||||
|
"do": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"merge",
|
"merge",
|
||||||
@@ -26783,20 +26787,8 @@
|
|||||||
"squash",
|
"squash",
|
||||||
"fast-forward-only",
|
"fast-forward-only",
|
||||||
"manually-merged"
|
"manually-merged"
|
||||||
]
|
],
|
||||||
},
|
"x-go-name": "Do"
|
||||||
"MergeCommitID": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"MergeMessageField": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"MergeTitleField": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"delete_branch_after_merge": {
|
|
||||||
"type": "boolean",
|
|
||||||
"x-go-name": "DeleteBranchAfterMerge"
|
|
||||||
},
|
},
|
||||||
"force_merge": {
|
"force_merge": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -26806,6 +26798,18 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "HeadCommitID"
|
"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": {
|
"merge_when_checks_succeed": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"x-go-name": "MergeWhenChecksSucceed"
|
"x-go-name": "MergeWhenChecksSucceed"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect, test} from '@playwright/test';
|
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}) => {
|
test('external file', async ({page, request}) => {
|
||||||
const repoName = `e2e-external-render-${randomString(8)}`;
|
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`));
|
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
|
||||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||||
await expect(frame.locator('p')).toContainText('rendered content');
|
await expect(frame.locator('p')).toContainText('rendered content');
|
||||||
|
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||||
await assertNoJsError(page);
|
await assertNoJsError(page);
|
||||||
} finally {
|
} finally {
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
await apiDeleteRepo(request, owner, repoName);
|
||||||
@@ -31,13 +32,28 @@ test('openapi file', async ({page, request}) => {
|
|||||||
login(page),
|
login(page),
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
const spec = 'openapi: "3.0.0"\ninfo:\n title: Test API\n version: "1.0"\npaths: {}\n';
|
const title = 'Test <API> & "quoted"';
|
||||||
await apiCreateFile(request, owner, repoName, 'openapi.yaml', spec);
|
const spec = JSON.stringify({
|
||||||
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.yaml`);
|
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');
|
const iframe = page.locator('iframe.external-render-iframe');
|
||||||
await expect(iframe).toBeVisible();
|
await expect(iframe).toBeVisible();
|
||||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
|
||||||
await expect(frame.locator('#swagger-ui .swagger-ui')).toBeVisible();
|
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);
|
await assertNoJsError(page);
|
||||||
} finally {
|
} finally {
|
||||||
await apiDeleteRepo(request, owner, repoName);
|
await apiDeleteRepo(request, owner, repoName);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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/');
|
||||||
|
});
|
||||||
+17
-1
@@ -1,6 +1,6 @@
|
|||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import {expect} from '@playwright/test';
|
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. */
|
/** Generate a random alphanumeric string. */
|
||||||
export function randomString(length: number): string {
|
export function randomString(length: number): string {
|
||||||
@@ -67,6 +67,13 @@ export async function apiCreateFile(requestContext: APIRequestContext, owner: st
|
|||||||
}), 'apiCreateFile');
|
}), '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) {
|
export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) {
|
||||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, {
|
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, {
|
||||||
headers: apiHeaders(),
|
headers: apiHeaders(),
|
||||||
@@ -115,6 +122,15 @@ export async function assertNoJsError(page: Page) {
|
|||||||
await expect(page.locator('.js-global-error')).toHaveCount(0);
|
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) {
|
export async function logout(page: Page) {
|
||||||
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
|
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
|
|||||||
DecodeJSON(t, resp, &apiNewReaction)
|
DecodeJSON(t, resp, &apiNewReaction)
|
||||||
|
|
||||||
// Add existing reaction
|
// 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
|
// Blocked user can't react to comment
|
||||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||||
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
|
|||||||
DecodeJSON(t, resp, &apiNewReaction)
|
DecodeJSON(t, resp, &apiNewReaction)
|
||||||
|
|
||||||
// Add existing reaction
|
// 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
|
// Get end result of reaction list of issue #1
|
||||||
req = NewRequest(t, "GET", urlStr).
|
req = NewRequest(t, "GET", urlStr).
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
|
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
@@ -35,9 +36,26 @@ func TestPackageSwift(t *testing.T) {
|
|||||||
packageID := packageScope + "." + packageName
|
packageID := packageScope + "." + packageName
|
||||||
packageVersion := "1.0.3"
|
packageVersion := "1.0.3"
|
||||||
packageVersion2 := "1.0.4"
|
packageVersion2 := "1.0.4"
|
||||||
|
packageVersion3 := "1.0.5"
|
||||||
packageAuthor := "KN4CK3R"
|
packageAuthor := "KN4CK3R"
|
||||||
packageDescription := "Gitea Test Package"
|
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"
|
contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||||
contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.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": contentManifest1,
|
||||||
"Package@swift-5.6.swift": contentManifest2,
|
"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)
|
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.Len(t, metadata.Manifests, 2)
|
||||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||||
assert.Len(t, pd.VersionProperties, 1)
|
assert.Len(t, pd.VersionProperties, 3)
|
||||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||||
|
|
||||||
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
|
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -234,7 +252,7 @@ func TestPackageSwift(t *testing.T) {
|
|||||||
"Package.swift": contentManifest1,
|
"Package.swift": contentManifest1,
|
||||||
"Package@swift-5.6.swift": contentManifest2,
|
"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)
|
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.Len(t, metadata.Manifests, 2)
|
||||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||||
assert.Len(t, pd.VersionProperties, 1)
|
assert.Len(t, pd.VersionProperties, 3)
|
||||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||||
|
|
||||||
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
|
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -354,8 +372,11 @@ func TestPackageSwift(t *testing.T) {
|
|||||||
assert.Equal(t, packageVersion, result.Metadata.Version)
|
assert.Equal(t, packageVersion, result.Metadata.Version)
|
||||||
assert.Equal(t, packageDescription, result.Metadata.Description)
|
assert.Equal(t, packageDescription, result.Metadata.Description)
|
||||||
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
|
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.Name)
|
||||||
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
|
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)).
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
|
||||||
AddBasicAuth(user.Name)
|
AddBasicAuth(user.Name)
|
||||||
@@ -364,6 +385,41 @@ func TestPackageSwift(t *testing.T) {
|
|||||||
assert.Equal(t, body, resp.Body.String())
|
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) {
|
t.Run("DownloadManifest", func(t *testing.T) {
|
||||||
manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
|
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/")
|
req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
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)
|
SetHeader("Accept", swift_router.AcceptJSON)
|
||||||
resp = MakeRequest(t, req, http.StatusOK)
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func TestAPIPrivateServ(t *testing.T) {
|
|||||||
assert.Empty(t, results)
|
assert.Empty(t, results)
|
||||||
|
|
||||||
// Add reading deploy key
|
// 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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Can pull from repo we're a deploy key for
|
// Can pull from repo we're a deploy key for
|
||||||
@@ -106,17 +106,17 @@ func TestAPIPrivateServ(t *testing.T) {
|
|||||||
assert.Empty(t, results)
|
assert.Empty(t, results)
|
||||||
|
|
||||||
// Cannot pull from a private repo we're not associated with
|
// 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.Error(t, extra.Error)
|
||||||
assert.Empty(t, results)
|
assert.Empty(t, results)
|
||||||
|
|
||||||
// Cannot pull from a public repo we're not associated with
|
// 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.Error(t, extra.Error)
|
||||||
assert.Empty(t, results)
|
assert.Empty(t, results)
|
||||||
|
|
||||||
// Add writing deploy key
|
// 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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Cannot push to a private repo with reading key
|
// Cannot push to a private repo with reading key
|
||||||
|
|||||||
@@ -5,46 +5,60 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPICompareBranches(t *testing.T) {
|
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})
|
t.Run("CompareBranches", func(t *testing.T) {
|
||||||
// Login as User2.
|
defer tests.PrintCurrentTest(t)()
|
||||||
session := loginUser(t, user.Name)
|
|
||||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
||||||
|
|
||||||
t.Run("CompareBranches", func(t *testing.T) {
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2)
|
||||||
defer tests.PrintCurrentTest(t)()
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token)
|
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
assert.Equal(t, 2, apiResp.TotalCommits)
|
||||||
|
assert.Len(t, apiResp.Commits, 2)
|
||||||
|
})
|
||||||
|
|
||||||
var apiResp *api.Compare
|
t.Run("CompareCommits", func(t *testing.T) {
|
||||||
DecodeJSON(t, resp, &apiResp)
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
assert.Equal(t, 2, apiResp.TotalCommits)
|
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2)
|
||||||
assert.Len(t, apiResp.Commits, 2)
|
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) {
|
t.Run("CompareForkOnlyCommit", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(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)
|
|
||||||
|
|
||||||
var apiResp *api.Compare
|
user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
|
||||||
DecodeJSON(t, resp, &apiResp)
|
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)
|
_, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"})
|
||||||
assert.Len(t, apiResp.Commits, 1)
|
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
|
// default sandbox in sub page response
|
||||||
assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy"))
|
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
|
// 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) {
|
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)
|
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"))
|
assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
@@ -16,13 +17,29 @@ import (
|
|||||||
func TestSignOut(t *testing.T) {
|
func TestSignOut(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(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")
|
req := NewRequest(t, "GET", "/user/logout")
|
||||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
assert.Equal(t, "/", resp.Header().Get("Location"))
|
||||||
|
|
||||||
// try to view a private repo, should fail
|
// logged out, try to view a private repo, should fail
|
||||||
req = NewRequest(t, "GET", "/user2/repo2")
|
req = NewRequest(t, "GET", "/user2/repo2")
|
||||||
session.MakeRequest(t, req, http.StatusNotFound)
|
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"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
@@ -22,9 +24,20 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestViewUser(t *testing.T) {
|
func TestUser(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(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")
|
req := NewRequest(t, "GET", "/user2")
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
@@ -32,12 +45,28 @@ func TestViewUser(t *testing.T) {
|
|||||||
resp := MakeRequest(t, req, http.StatusOK)
|
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.
|
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=
|
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())
|
`, resp.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenameUsername(t *testing.T) {
|
func testRenameUsername(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||||
"name": "newUsername",
|
"name": "newUsername",
|
||||||
@@ -50,9 +79,7 @@ func TestRenameUsername(t *testing.T) {
|
|||||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
// user 22 is a limited visibility org
|
// user 22 is a limited visibility org
|
||||||
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||||
req := NewRequest(t, "GET", "/"+org22.Name)
|
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
|
session.MakeRequest(t, req, http.StatusTemporaryRedirect) // login user2 can visit private visibility user via old name
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenameInvalidUsername(t *testing.T) {
|
func testRenameInvalidUsername(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
invalidUsernames := []string{
|
invalidUsernames := []string{
|
||||||
"%2f*",
|
"%2f*",
|
||||||
"%2f.",
|
"%2f.",
|
||||||
@@ -166,9 +191,7 @@ func TestRenameInvalidUsername(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenameReservedUsername(t *testing.T) {
|
func testRenameReservedUsername(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
reservedUsernames := []string{
|
reservedUsernames := []string{
|
||||||
// ".", "..", ".well-known", // The names are not only reserved but also invalid
|
// ".", "..", ".well-known", // The names are not only reserved but also invalid
|
||||||
"api",
|
"api",
|
||||||
@@ -198,8 +221,7 @@ func TestRenameReservedUsername(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExportUserGPGKeys(t *testing.T) {
|
func testExportUserGPGKeys(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
testExportUserGPGKeys := func(t *testing.T, user, expected string) {
|
testExportUserGPGKeys := func(t *testing.T, user, expected string) {
|
||||||
session := loginUser(t, user)
|
session := loginUser(t, user)
|
||||||
t.Logf("Testing username %s export gpg keys", user)
|
t.Logf("Testing username %s export gpg keys", user)
|
||||||
@@ -284,9 +306,7 @@ GrE0MHOxUbc9tbtyk0F1SuzREUBH
|
|||||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserRss(t *testing.T) {
|
func testGetUserRss(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
user34 := "the_34-user.with.all.allowedChars"
|
user34 := "the_34-user.with.all.allowedChars"
|
||||||
req := NewRequestf(t, "GET", "/%s.rss", user34)
|
req := NewRequestf(t, "GET", "/%s.rss", user34)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
@@ -306,9 +326,7 @@ func TestGetUserRss(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusNotFound)
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListStopWatches(t *testing.T) {
|
func testUserListStopWatches(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||||
|
|
||||||
@@ -329,8 +347,7 @@ func TestListStopWatches(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserLocationMapLink(t *testing.T) {
|
func testUserLocationMapLink(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||||
|
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
|
|||||||
+33
-33
@@ -1,13 +1,12 @@
|
|||||||
import {build, defineConfig} from 'vite';
|
import {build, defineConfig} from 'vite';
|
||||||
import vuePlugin from '@vitejs/plugin-vue';
|
import vuePlugin from '@vitejs/plugin-vue';
|
||||||
import {stringPlugin} from 'vite-string-plugin';
|
import {stringPlugin} from 'vite-string-plugin';
|
||||||
|
import {licensePlugin, wrap} from 'rolldown-license-plugin';
|
||||||
import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs';
|
import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs';
|
||||||
import path, {basename, join, parse} from 'node:path';
|
import path, {basename, join, parse} from 'node:path';
|
||||||
import {env} from 'node:process';
|
import {env} from 'node:process';
|
||||||
import tailwindcss from 'tailwindcss';
|
import tailwindcss from 'tailwindcss';
|
||||||
import tailwindConfig from './tailwind.config.ts';
|
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 type {InlineConfig, Plugin, Rolldown} from 'vite';
|
||||||
import {camelize} from 'vue';
|
import {camelize} from 'vue';
|
||||||
|
|
||||||
@@ -39,10 +38,6 @@ const webComponents = new Set([
|
|||||||
'text-expander',
|
'text-expander',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function formatLicenseText(licenseText: string) {
|
|
||||||
return wrapAnsi(licenseText || '', 80).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonRolldownOptions: Rolldown.RolldownOptions = {
|
const commonRolldownOptions: Rolldown.RolldownOptions = {
|
||||||
checks: {
|
checks: {
|
||||||
eval: false, // htmx needs eval
|
eval: false, // htmx needs eval
|
||||||
@@ -158,9 +153,15 @@ function iifePlugin(sourceFileName: string): Plugin {
|
|||||||
if (!entry) throw new Error('IIFE build produced no output');
|
if (!entry) throw new Error('IIFE build produced no output');
|
||||||
|
|
||||||
const manifestPath = join(outDir, '.vite', 'manifest.json');
|
const manifestPath = join(outDir, '.vite', 'manifest.json');
|
||||||
const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
try {
|
||||||
manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true};
|
const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||||
writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
|
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/index.',
|
||||||
'js/iife.',
|
'js/iife.',
|
||||||
'js/swagger.',
|
'js/swagger.',
|
||||||
|
'js/external-render-frontend.',
|
||||||
'js/external-render-helper.',
|
'js/external-render-helper.',
|
||||||
'js/eventsource.sharedworker.',
|
'js/eventsource.sharedworker.',
|
||||||
];
|
];
|
||||||
@@ -257,8 +259,10 @@ export default defineConfig(commonViteOpts({
|
|||||||
manifest: true,
|
manifest: true,
|
||||||
rolldownOptions: {
|
rolldownOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
// FIXME: INCORRECT-VITE-MANIFEST-PARSER: the "css importing" logic in backend is wrong
|
||||||
index: join(import.meta.dirname, 'web_src/js/index.ts'),
|
index: join(import.meta.dirname, 'web_src/js/index.ts'),
|
||||||
swagger: join(import.meta.dirname, 'web_src/js/swagger.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'),
|
'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/eventsource.sharedworker.ts'),
|
||||||
devtest: join(import.meta.dirname, 'web_src/css/devtest.css'),
|
devtest: join(import.meta.dirname, 'web_src/css/devtest.css'),
|
||||||
...themes,
|
...themes,
|
||||||
@@ -314,33 +318,29 @@ export default defineConfig(commonViteOpts({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
isProduction ? licensePlugin({
|
isProduction ? licensePlugin({
|
||||||
thirdParty: {
|
done(deps, context) {
|
||||||
output: {
|
const line = '-'.repeat(80);
|
||||||
file: join(import.meta.dirname, 'public/assets/licenses.txt'),
|
const goLicenses = JSON.parse(readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8'));
|
||||||
template(deps) {
|
const combined: Record<string, string> = {};
|
||||||
const line = '-'.repeat(80);
|
for (const {name, licenseText} of goLicenses) {
|
||||||
const goJson = readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8');
|
combined[name] = wrap(licenseText || '', 80).trim();
|
||||||
const goModules = JSON.parse(goJson).map(({name, licenseText}: {name: string, licenseText: string}) => {
|
}
|
||||||
return {name, body: formatLicenseText(licenseText)};
|
for (const {name, version, licenseText} of deps) {
|
||||||
});
|
combined[`${name}@${version}`] = wrap(licenseText, 80).trim();
|
||||||
const jsModules = deps.map((dep) => {
|
}
|
||||||
return {name: dep.name, version: dep.version, body: formatLicenseText(dep.licenseText ?? '')};
|
const content = Object.entries(combined)
|
||||||
});
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name));
|
.map(([title, body]) => `${line}\n${title}\n${line}\n${body}`).join('\n');
|
||||||
return modules.map(({name, version, body}: {name: string, version?: string, body: string}) => {
|
context.emitFile({type: 'asset', fileName: 'licenses.txt', source: content});
|
||||||
const title = version ? `${name}@${version}` : name;
|
},
|
||||||
return `${line}\n${title}\n${line}\n${body}`;
|
match: /^((UN)?LICEN(S|C)E|COPYING).*$/i, // also defined in build/generate-go-licenses.go
|
||||||
}).join('\n');
|
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);
|
||||||
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 ?? '');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}) : {
|
}) : {
|
||||||
name: 'dev-licenses-stub',
|
name: 'dev-licenses-stub',
|
||||||
closeBundle() {
|
configureServer() {
|
||||||
writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development');
|
writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,14 +6,6 @@
|
|||||||
overflow-x: auto;
|
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 {
|
.runner-container .task-status-success {
|
||||||
background-color: var(--color-green);
|
background-color: var(--color-green);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
|||||||
@@ -643,10 +643,6 @@ overflow-menu .ui.label {
|
|||||||
color: var(--color-primary-contrast);
|
color: var(--color-primary-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.archived-icon {
|
|
||||||
color: var(--color-secondary-dark-2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oauth2-authorize-application-box {
|
.oauth2-authorize-application-box {
|
||||||
margin-top: 3em !important;
|
margin-top: 3em !important;
|
||||||
}
|
}
|
||||||
@@ -670,10 +666,6 @@ overflow-menu .ui.label {
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lines-num span.bottom-line::after {
|
|
||||||
border-bottom: 1px solid var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lines-num span::after {
|
.lines-num span::after {
|
||||||
content: attr(data-line-number);
|
content: attr(data-line-number);
|
||||||
line-height: var(--line-height-code) !important;
|
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 */
|
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 {
|
.migrate .svg.gitea-git {
|
||||||
color: var(--color-git);
|
color: var(--color-git);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
height: auto;
|
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;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,12 +154,6 @@ In markup content, we always use bottom margin for all elements */
|
|||||||
padding-inline-start: 2em;
|
padding-inline-start: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markup ul.no-list,
|
|
||||||
.markup ol.no-list {
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markup .task-list-item {
|
.markup .task-list-item {
|
||||||
list-style-type: none;
|
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);
|
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 code,
|
||||||
.markup tt {
|
.markup tt {
|
||||||
padding: 0.2em 0.4em;
|
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 {
|
.external-render-iframe {
|
||||||
|
display: block; /* removes the inline baseline gap below the iframe */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: max(300px, 80vh);
|
height: max(300px, 80vh);
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markup-content-iframe {
|
.markup-content-iframe {
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ textarea:focus,
|
|||||||
color: var(--color-input-text);
|
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 {
|
.ui.input {
|
||||||
color: var(--color-input-text);
|
color: var(--color-input-text);
|
||||||
}
|
}
|
||||||
@@ -198,7 +205,6 @@ textarea:focus,
|
|||||||
background-color: var(--color-error-bg);
|
background-color: var(--color-error-bg);
|
||||||
border-color: var(--color-error-border);
|
border-color: var(--color-error-border);
|
||||||
color: var(--color-error-text);
|
color: var(--color-error-text);
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
.ui.form .field.error textarea:focus,
|
.ui.form .field.error textarea:focus,
|
||||||
.ui.form .field.error select:focus,
|
.ui.form .field.error select:focus,
|
||||||
|
|||||||
@@ -84,15 +84,6 @@
|
|||||||
border-color: var(--color-warning-border);
|
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 {
|
.ui.message > .close.icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -159,19 +159,11 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling.dimmable.dimmed {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrolling.dimmable > .dimmer {
|
.scrolling.dimmable > .dimmer {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrolling.dimmable.dimmed > .dimmer {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modals.dimmer .ui.scrolling.modal {
|
.modals.dimmer .ui.scrolling.modal {
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,10 +287,6 @@ td .commit-summary {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.view.issue .instruct-toggle {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* issue title & meta & edit */
|
/* issue title & meta & edit */
|
||||||
.issue-title-header {
|
.issue-title-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1463,11 +1459,6 @@ tbody.commit-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.commit-list .commit-status-link {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-body {
|
.commit-body {
|
||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
@import "../../node_modules/swagger-ui-dist/swagger-ui.css";
|
@import "../../node_modules/swagger-ui-dist/swagger-ui.css";
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
html body,
|
html body,
|
||||||
html .swagger-ui,
|
html .swagger-ui,
|
||||||
@@ -15,27 +11,3 @@ html .swagger-ui .scheme-container {
|
|||||||
html.dark-mode .swagger-ui table.headers td {
|
html.dark-mode .swagger-ui table.headers td {
|
||||||
color: var(--color-text) !important;
|
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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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) {
|
if (isDarkTheme) {
|
||||||
document.documentElement.setAttribute('data-gitea-theme-dark', String(isDarkTheme));
|
document.documentElement.setAttribute('data-gitea-theme-dark', String(isDarkTheme));
|
||||||
}
|
}
|
||||||
|
|
||||||
const backgroundColor = url.searchParams.get('gitea-iframe-bgcolor');
|
const backgroundColor = queryParams.get('gitea-iframe-bgcolor');
|
||||||
if (backgroundColor) {
|
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
|
// 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');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
:root {
|
:root {
|
||||||
--gitea-iframe-bgcolor: ${backgroundColor};
|
--gitea-iframe-bgcolor: ${backgroundColor};
|
||||||
}
|
}
|
||||||
|
html, body { margin: 0; padding: 0 }
|
||||||
body { background: ${backgroundColor}; }
|
body { background: ${backgroundColor}; }
|
||||||
`;
|
`;
|
||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeId = url.searchParams.get('gitea-iframe-id');
|
const iframeId = queryParams.get('gitea-iframe-id');
|
||||||
if (iframeId) {
|
if (iframeId) {
|
||||||
// iframe is in different origin, so we need to use postMessage to communicate
|
// iframe is in different origin, so we need to use postMessage to communicate
|
||||||
const postIframeMsg = (cmd: string, data: Record<string, any> = {}) => {
|
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) {
|
if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showGlobalErrorMessage(`Your ROOT_URL in app.ini is "${appUrl}", it's unlikely matching the site you are visiting.
|
showGlobalErrorMessage(`The detected web site URL is "${appUrl}", it's unlikely matching the site config.
|
||||||
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in.`, 'warning');
|
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() {
|
export function checkAppUrlScheme() {
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
import type {FileRenderPlugin} from '../render/plugin.ts';
|
import type {InplaceRenderPlugin} from '../render/plugin.ts';
|
||||||
import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts';
|
import {newInplacePluginPdfViewer} from '../render/plugins/inplace-pdf-viewer.ts';
|
||||||
import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts';
|
|
||||||
import {registerGlobalInitFunc} from '../modules/observer.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 {html} from '../utils/html.ts';
|
||||||
import {basename} from '../utils.ts';
|
import {basename} from '../utils.ts';
|
||||||
|
|
||||||
const plugins: FileRenderPlugin[] = [];
|
const inplacePlugins: InplaceRenderPlugin[] = [];
|
||||||
|
|
||||||
function initPluginsOnce(): void {
|
function initInplacePluginsOnce(): void {
|
||||||
if (plugins.length) return;
|
if (inplacePlugins.length) return;
|
||||||
plugins.push(newRenderPlugin3DViewer(), newRenderPluginPdfViewer());
|
inplacePlugins.push(newInplacePluginPdfViewer());
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null {
|
function findInplaceRenderPlugin(filename: string, mimeType: string): InplaceRenderPlugin | null {
|
||||||
return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null;
|
return inplacePlugins.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?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderRawFileToContainer(container: HTMLElement, rawFileLink: string, mimeType: string) {
|
async function renderRawFileToContainer(container: HTMLElement, rawFileLink: string, mimeType: string) {
|
||||||
@@ -32,7 +22,7 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str
|
|||||||
|
|
||||||
let rendered = false, errorMsg = '';
|
let rendered = false, errorMsg = '';
|
||||||
try {
|
try {
|
||||||
const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType);
|
const plugin = findInplaceRenderPlugin(basename(rawFileLink), mimeType);
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
container.classList.add('is-loading');
|
container.classList.add('is-loading');
|
||||||
container.setAttribute('data-render-name', plugin.name); // not used yet
|
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 {
|
export function initRepoFileView(): void {
|
||||||
registerGlobalInitFunc('initRepoFileView', async (elFileView: HTMLElement) => {
|
registerGlobalInitFunc('initRepoFileView', async (elFileView: HTMLElement) => {
|
||||||
initPluginsOnce();
|
initInplacePluginsOnce();
|
||||||
const rawFileLink = elFileView.getAttribute('data-raw-file-link')!;
|
const rawFileLink = elFileView.getAttribute('data-raw-file-link')!;
|
||||||
const mimeType = elFileView.getAttribute('data-mime-type') || ''; // not used yet
|
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 = findInplaceRenderPlugin(basename(rawFileLink), mimeType);
|
||||||
const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType);
|
|
||||||
if (!plugin) return;
|
if (!plugin) return;
|
||||||
|
|
||||||
const renderContainer = elFileView.querySelector<HTMLElement>('.file-view-render-container');
|
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);
|
if (renderContainer) await renderRawFileToContainer(renderContainer, rawFileLink, mimeType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
@@ -68,6 +68,15 @@ interface Window {
|
|||||||
turnstile: any,
|
turnstile: any,
|
||||||
hcaptcha: 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
|
// 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 {initMarkupCodeCopy} from './codecopy.ts';
|
||||||
import {initMarkupRenderAsciicast} from './asciicast.ts';
|
import {initMarkupRenderAsciicast} from './asciicast.ts';
|
||||||
import {initMarkupTasklist} from './tasklist.ts';
|
import {initMarkupTasklist} from './tasklist.ts';
|
||||||
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
|
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
|
||||||
import {initMarkupRenderIframe} from './render-iframe.ts';
|
import {initExternalRenderIframe} from './render-iframe.ts';
|
||||||
import {initMarkupRefIssue} from './refissue.ts';
|
import {initMarkupRefIssue} from './refissue.ts';
|
||||||
import {toggleElemClass} from '../utils/dom.ts';
|
import {toggleElemClass} from '../utils/dom.ts';
|
||||||
|
|
||||||
// code that runs for all markup content
|
// code that runs for all markup content
|
||||||
export function initMarkupContent(): void {
|
export function initMarkupContent(): void {
|
||||||
|
registerGlobalInitFunc('initExternalRenderIframe', initExternalRenderIframe);
|
||||||
registerGlobalSelectorFunc('.markup', (el: HTMLElement) => {
|
registerGlobalSelectorFunc('.markup', (el: HTMLElement) => {
|
||||||
if (el.matches('.truncated-markup')) {
|
if (el.matches('.truncated-markup')) {
|
||||||
// when the rendered markup is truncated (e.g.: user's home activity feed)
|
// when the rendered markup is truncated (e.g.: user's home activity feed)
|
||||||
@@ -25,7 +26,6 @@ export function initMarkupContent(): void {
|
|||||||
initMarkupCodeMermaid(el);
|
initMarkupCodeMermaid(el);
|
||||||
initMarkupCodeMath(el);
|
initMarkupCodeMath(el);
|
||||||
initMarkupRenderAsciicast(el);
|
initMarkupRenderAsciicast(el);
|
||||||
initMarkupRenderIframe(el);
|
|
||||||
initMarkupRefIssue(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 {isDarkTheme} from '../utils.ts';
|
||||||
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
|
||||||
function safeRenderIframeLink(link: any): string | null {
|
function safeRenderIframeLink(link: any): string | null {
|
||||||
try {
|
try {
|
||||||
@@ -41,7 +42,7 @@ function getRealBackgroundColor(el: HTMLElement) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRenderIframeContent(iframe: HTMLIFrameElement) {
|
export async function initExternalRenderIframe(iframe: HTMLIFrameElement) {
|
||||||
const iframeSrcUrl = iframe.getAttribute('data-src')!;
|
const iframeSrcUrl = iframe.getAttribute('data-src')!;
|
||||||
if (!iframe.id) iframe.id = generateElemId('gitea-iframe-');
|
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-is-dark-theme', String(isDarkTheme()));
|
||||||
u.searchParams.set('gitea-iframe-id', iframe.id);
|
u.searchParams.set('gitea-iframe-id', iframe.id);
|
||||||
u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe));
|
u.searchParams.set('gitea-iframe-bgcolor', getRealBackgroundColor(iframe));
|
||||||
iframe.src = u.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initMarkupRenderIframe(el: HTMLElement) {
|
// It must use "srcdoc" here, because our backend always sends CSP sandbox directive for the rendered content
|
||||||
queryElemChildren(el, 'iframe.external-render-iframe', loadRenderIframeContent);
|
// (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', () => {
|
test('showGlobalErrorMessage', () => {
|
||||||
document.body.innerHTML = '<div class="page-content"></div>';
|
document.body.innerHTML = '<div class="page-content"></div>';
|
||||||
|
|||||||
@@ -23,11 +23,19 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
|
|||||||
msgContainer.prepend(msgDiv);
|
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) {
|
export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
|
||||||
const err = error ?? reason;
|
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
|
// `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:
|
// 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
|
// - 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
|
// - https://github.com/go-gitea/gitea/issues/20240
|
||||||
if (!err) {
|
if (!err) {
|
||||||
if (message) console.error(new Error(message));
|
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
|
// Filter out errors from browser extensions or other non-Gitea scripts.
|
||||||
// from a browser extension or inline script. Do not show such errors in production.
|
if (!isGiteaError(filename ?? '', err?.stack ?? '')) return;
|
||||||
if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
|
|
||||||
|
|
||||||
let msg = err?.message ?? message;
|
let msg = err?.message ?? message;
|
||||||
if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
|
if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`;
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
export type FileRenderPlugin = {
|
// there are 2 kinds of plugins:
|
||||||
// unique plugin name
|
// * "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;
|
name: string;
|
||||||
|
|
||||||
// test if plugin can handle a specified file
|
|
||||||
canHandle: (filename: string, mimeType: string) => boolean;
|
canHandle: (filename: string, mimeType: string) => boolean;
|
||||||
|
|
||||||
// render file content
|
|
||||||
render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>;
|
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